1. 项目概述与核心思路拆解
1.1 为什么我要做这个记账系统
记账这件事,本身不新鲜。市面上随手一搜就是一堆记账App,随手记、鲨鱼记账、MoneyWiz,功能一个比一个全,图表一个比一个好看。但我个人记账三年多,始终有一种“被工具绑架”的感觉:数据在别人服务器上,想导出自定义报表费老大劲;想加一个“跟对象AA分摊”的功能,提需求排期到天荒地老;年费订阅还不便宜。
所以我决定自己动手,做一个真正属于自己的全栈记账系统。这个项目我用了大概两个月业余时间,从零开始搭了一套完整的前后端分离架构,还顺手做了移动端适配。技术栈选择了 Vue 3 + Golang + Uniapp 这个组合,覆盖了 Web 端、移动端和服务端三个层面。后来又接了 AI 接口做智能分类,算是把“全栈”两个字落实到了实处。
先说说这套系统解决了什么问题。个人记账最核心的需求就四个:记一笔账要快、查账要方便、统计要直观、数据要安全。市面上的 App 在前两点上做得还不错,但后两点往往受限于平台。自己做的系统,数据全在自己手里,想怎么分析都行;想给家里人开个子账号,写段代码就完事;想接入某个银行账单自动导入,只要对方开放接口,我这边随时能加。
这个项目适合谁参考?如果你是刚工作一两年、处在前端转全栈阶段,或者后端想补一补前端知识的同学,这个项目的技术选型和代码组织方式都有一定的借鉴价值。我会从数据库设计、后端 API、前端页面、移动端适配到 AI 智能分类,把整个实现过程的关键节点和踩过的坑都讲清楚。
1.2 全栈架构的整体设计思路
整个系统最核心的设计原则就八个字:轻量敏捷,按需扩展。我不打算做一个面面俱到的商业产品,所以一开始就明确了边界——能砍的功能全部砍掉,只保留记账、分类、统计、预算提醒这几个核心模块。
架构上我选择了前后端完全分离的方案。后端用 Golang + Gin 框架提供 RESTful API,前端 Web 端用 Vue 3 + Vite + Element Plus,移动端用 Uniapp 打包成 H5 和小程序,这样一套后端代码可以同时服务两个前端。数据库用的 MySQL 8.0,缓存用的 Redis,主要存会话和热点统计数据。部署上就是一台轻量云服务器,用 Docker Compose 一键拉起所有服务。
这里要特别说一下为什么选 Golang 而不是 Node.js 或 Java。我个人判断标准很简单:一是编译型语言的部署优势,打出一个二进制文件往服务器上一扔就能跑,没有运行时依赖的心智负担;二是 Goroutine 对付并发读写绰绰有余,记账系统这种 IO 密集场景,Golang 的并发模型天然契合;三是我当时正好在系统学习 Golang,用项目驱动学习效率最高。如果你的 Java 或 Node 基础更好,完全可以用自己熟悉的语言,架构思路是通用的。
前端选 Vue 3 是因为组合式 API 写业务逻辑比选项式 API 清晰太多。特别是记账这种表单密集、状态管理复杂的场景,setup 语法糖配合 Pinia 做全局状态,比 Vue 2 时代省了一半代码量。Uniapp 则是为了“一套代码多端运行”,虽然性能和原生体验有差距,但对个人项目来说,能用 20% 的成本覆盖 80% 的场景,这笔账怎么算都值。
2. 数据库设计与核心数据模型
2.1 五张核心表的设计思路
数据库设计是整个项目的基石,这一步要是走错了,后面写多少代码都白搭。我花了整整一个周末把表结构定下来,前后推翻了三次,最终收敛成下面这五张表。
第一张是用户表 user,字段包括 id、用户名、密码哈希、邮箱、创建时间。密码存储用了 bcrypt 加盐哈希,绝对不允许明文。用户表没什么好说的,重点在后面的业务表。
第二张是账户表 account,所谓账户就是指你的钱放在哪里——现金、银行卡、支付宝、微信,都算一个账户。字段有 id、用户 ID、账户名称、账户类型、初始余额、当前余额、排序值。这里有个小设计:初始余额和当前余额分开存,因为你需要知道这个账户从开始到现在总共进了多少钱,而不是只知道一个总数。
第三张是分类表 category,包括支出分类和收入分类。字段有 id、用户 ID、分类名称、分类类型(1 支出 / 2 收入)、父分类 ID、图标、排序值。分类做成树形结构,因为“餐饮”下面有“早餐”“午餐”“晚餐”,你需要支持二级分类。系统内置了一套默认分类,用户首次注册时自动初始化到他的名下,这样上手成本最低。
第四张是交易流水表 transaction,这是整个系统的核心表。字段包括 id、用户 ID、账户 ID、分类 ID、交易类型(1 支出 / 2 收入 / 3 转账)、金额(单位分)、交易时间、备注、创建时间、更新时间。金额用整数分存储,绝不用浮点数——这是金融类系统的铁律,浮点数在加减运算中的精度问题会让你在对账时怀疑人生。
第五张是预算表 budget,字段包括 id、用户 ID、分类 ID(可以只针对某个分类做预算,也可以全类别一个总预算)、预算周期(按月或按年)、预算金额、创建时间。
这五张表的关系很简单:用户一对多账户、一对多分类、一对多流水、一对多预算;流水关联账户和分类。没有搞复杂的多对多,因为记账场景本质上就是一条流水记录,简单直接才是王道。实际表结构里还有个细节,就是每张表都加了 index 索引,特别是 transaction 表做了 (user_id, transaction_time) 的联合索引,这是查询统计最常用的路径。
2.2 为什么金额要用分存储而不是元
这是新手最容易踩的坑,我在这里专门说一下。记账系统里的金额计算非常密集,每天都在加减乘除。如果你用 MySQL 的 DECIMAL 或者直接用浮点类型存金额,表面上看没什么问题,但到了月底算总支出的时候,你会发现很多诡异的数字,比如某项支出显示的是 0.30000000000000004 这种。
根本原因在于计算机底层用二进制表示浮点数,而 0.1 在二进制里是一个无限循环小数,存进去就已经不精确了。逐条流水累加时,误差会不断累积。所以我在项目里统一用整数类型存储金额,单位是分。用户输入 12.34 元,前端把它转成 1234 传给后端,后端存 1234,查询返回时再除以 100 转回元。这个转换逻辑封装在一个公共方法里,前后端各写一次,约定好就永远不会乱。
这样设计还有一个好处,就是统计报表里做各种聚合运算时,全部走整数运算,速度快且绝对精确。比如你要算这个月餐饮花了多少钱,一条 SQL 把 amount 按分类分组求和,结果全是整数分,再除以 100 就是精确到分的人民币金额,不会出现一分钱的误差。对记账系统来说,账目精确是基本底线,我在这个点上吃过亏,希望大家别再踩。
2.3 分类管理与默认数据的初始化策略
分类这个东西看似简单,实际上做起来有不少门道。一开始我的分类表就是简单的两级结构,后来发现一个问题:不同用户的分类习惯差异很大。有人喜欢粗粒度,一个“餐饮”就完事;有人喜欢细到“奶茶”“咖啡”单独列一项。强制所有用户共用一套分类是不现实的。
所以我的方案是:系统内置一套默认分类模板,用户注册成功后在事务里自动复制一份到该用户名下的 category 表。用户之后可以自由增删改自己的分类,完全不影响其他用户。这套默认分类要覆盖主流场景:支出端有餐饮、交通、居住、购物、娱乐、医疗、教育、人情、其他;收入端有工资、奖金、理财、兼职、红包、其他。每个分类都配了图标和颜色,前端展示时直接用。
数据库初始化这里有个小技巧:分类的数据量不大,但是业务上频繁查询。所以我做了一个分类版本的缓存策略,搞了个简单的 Redis 缓存,用户修改分类时更新缓存版本号,前端在进入记账页时检查版本号,变了就重新拉取分类列表。实测下来,记账页的打开速度从平均 300ms 降到了 80ms 左右,体感明显提升。做个人项目也要有这个意识,性能优化不是上线以后的事,而要从一开始就考虑数据访问路径。
3. 后端服务实现:Golang + Gin 的实战记录
3.1 项目结构与依赖选型
后端项目直接用了 Gin 框架,这是 Golang 社区最流行的 Web 框架,性能好、中间件生态丰富。项目结构遵循了标准的 MVC 分层,但做了一点个人化的调整,按业务模块分包而不是按技术层次分包,这样后期维护时找代码更方便。
bookkeeping-server/ ├── main.go // 入口文件,启动服务 ├── config/ // 配置加载 │ ├── config.go │ └── config.yaml ├── router/ // 路由注册 │ └── router.go ├── middleware/ // 中间件 │ ├── auth.go // JWT鉴权 │ ├── cors.go // 跨域处理 │ └── logger.go // 请求日志 ├── controller/ // 控制器层,处理HTTP请求 │ ├── user_controller.go │ ├── transaction_controller.go │ ├── category_controller.go │ └── statistics_controller.go ├── service/ // 业务逻辑层 │ ├── user_service.go │ ├── transaction_service.go │ └── statistics_service.go ├── model/ // 数据模型 │ ├── user.go │ ├── transaction.go │ └── category.go ├── repository/ // 数据访问层 │ ├── user_repo.go │ └── transaction_repo.go └── common/ // 公共工具 ├── response.go // 统一响应封装 └── jwt.go依赖方面我用的都是比较主流的选择:gin 做 HTTP 框架,gorm 做 ORM,golang-jwt 做 Token 鉴权,bcrypt 做密码加密,go-playground/validator 做参数校验,viper 做配置管理,redis 客户端用的 go-redis。这些都是社区成熟方案,遇到问题搜一下就有答案,适合个人项目快速推进。
3.2 JWT 鉴权与中间件链路
鉴权方案我直接选了 JWT(JSON Web Token),没有用传统的 Session-Cookie 机制。原因有两方面:一是前后端完全分离后,前端可能是 Web 也有可能是小程序,Cookie 在部分场景下处理起来比较麻烦,而 JWT 只要在请求头里带一个 Authorization 字段就行,跨端通用;二是无状态服务不用存 Session,后面想横向扩展多实例部署时不用考虑 Session 同步问题。
JWT 的签发逻辑在用户登录时完成。用户提交用户名密码,service 层校验通过后生成一个有效期为 7 天的 Token,Token 里包含用户 ID 和用户名两个声明,然后用项目配置的密钥签名。之后每次请求,中间件从 Authorization 头取出 Token,验证签名和过期时间,再把用户 ID 注入到请求上下文里,后续的 service 层就能直接拿到当前用户。
这里有个安全细节必须强调:JWT 是签名但不是加密的,Payload 部分用 Base64 编码,任何人拿到都能解码看到里面的内容。所以千万不要往 Token 里塞密码、手机号这类敏感信息。我一般只放用户 ID 和过期时间,需要用户信息时再用 ID 查库或查缓存。另外密钥要足够长且随机,项目里我放在 config.yaml 中,生产环境通过环境变量注入,绝不硬编码到代码里,也绝不提交到 Git 仓库。
中间件链路我按这个顺序注册:Logger 中间件 -> CORS 中间件 -> Recovery 中间件 -> JWT 鉴权中间件。Logger 记录每个请求的方法、路径、状态码、耗时;CORS 处理跨域,开发环境下放行本地的前端调试端口;Recovery 捕获 panic,避免一个异常导致整个服务崩溃;JWT 中间件保护所有需要登录的接口,一些公开接口(登录、注册)通过路由分组绕开。
3.3 记账核心 API 的封装实践
记账系统的核心 API 就三个:记一笔账、查流水列表、统计汇总。我逐个说说实现要点。
记一笔账的接口接收的参数包括:账户 ID、分类 ID、交易类型、金额(单位分)、交易时间、备注。service 层做的处理比较关键:先校验账户归属是否正确,这个账户必须是当前登录用户名下的,防止越权操作;再校验分类归属,同理;然后在同一个数据库事务里执行两步操作——向 transaction 表插入流水记录、更新 account 表的当前余额。这两个操作必须原子化,否则就会出现流水记上了但余额没变的情况。Gorm 提供了 Transaction 方法,传入一个闭包函数,要么全部提交要么全部回滚。
查流水列表我用的是分页查询,参数有页码、每页条数、起止时间、分类 ID、交易类型。返回的数据结构里除了流水本身的字段,还要带出账户名称和分类名称,这样前端列表展示时不用再发请求去组装数据。这个联查在 SQL 层面用 LEFT JOIN 实现,查一次就把关联信息全部拿到,效率远高于在内存里循环查库。
统计汇总接口是报表页的数据来源,我分了三个维度:按天汇总、按分类汇总、按账户汇总。按天汇总就是查某个月每天的总收入和总支出,返回 30 条左右的数据,前端画折线图;按分类汇总就是查某个月各分类的支出占比,前端画饼图;按账户汇总就是查各账户的余额及变动情况。这些统计 SQL 全部在数据库层面用 GROUP BY 完成,后端不参与计算,性能开销很小。做了一次 EXPLAIN 分析后,给关键的过滤条件加了联合索引,一个月几千条流水的情况下,所有查询都稳定在 100ms 以内。
3.4 AI 智能分类的实现思路
AI 全栈是现在很热的方向,我在这个项目里也接了一个 AI 功能:智能识别记账备注,自动推荐分类。比如你输入“楼下老王那家兰州拉面,牛肉面加蛋 22 元”,系统能自动给你推荐“餐饮”分类。
实现思路不复杂。用户在记账页输入备注后,前端把备注文本传给后端一个专用接口,后端调用大模型 API,把备注内容和当前用户的分类列表一起放进 Prompt,让模型输出最匹配的分类 ID。这里的数据出入和成本控制要考虑清楚,我做了三级降级策略:先查本地关键词库,命中直接返回;没命中再调 AI 接口;AI 接口异常就返回一个默认分类并提示用户手动修改。实测下来,本地关键词库命中率能达到 60% 左右,剩下 40% 交给 AI,整体准确率能到 85% 以上,日常使用足够了。
给 AI 的 Prompt 设计也有一些经验。直接把用户的分类列表转换成 JSON 格式塞进上下文,让模型从中选择一个返回,返回格式约定为纯 JSON,方便直接解析。为了省 Token,分类列表做了精简,只传分类名称和 ID 的映射,不传图标、颜色这类展示字段。另外在消费侧做了一个简单的 Redis 缓存,同一个备注文本 24 小时内重复调用直接命中缓存,不重复计费。
4. 前端与移动端:Vue 3 + Uniapp 的双端实践
4.1 Web 管理后台的技术要点
Web 端的定位是管理后台,主要场景是数据录入后的深度分析、预算管理、账单导出,以及基础的账户配置。技术栈是 Vue 3 + TypeScript + Vite + Pinia + Element Plus,这个组合在 2024 到 2025 年的时间节点上依然是社区最成熟的方案之一。
项目里最值得说是记账表单的设计。记账是高频操作,用户体验直接决定你愿不愿意用这个系统。我花了很多心思在表单交互上:金额输入框默认聚焦,数字键盘直接弹出;分类选择用网格布局展示图标和名称,点一下选中;备注输入框旁放一个 AI 识别按钮,点一下自动填充分类。整套流程从打开页面到记完一笔账,目标控制在 10 秒以内。实测下来,在手机上用浏览器访问 H5 版本,最快 8 秒完成一笔记录,还是比较满意的。
统计报表页用的 ECharts 做可视化,折线图展示每日收支趋势,饼图展示分类占比,柱状图展示账户余额。有一个细节经验:不要一页展示太多图表,移动端会卡。我把统计页拆成了四个小的 Tab——每日趋势、分类占比、账户分布、月度对比,每切一个 Tab 懒加载对应的图表实例,数据量大的月份页面也能保持流畅滚动。
4.2 Uniapp 移动端的多端适配经验
移动端我选择了 Uniapp,从 Vue 3 编译成 H5、微信小程序和 App。这套方案最大的优势就是复用 Vue 的组件化思维,大部分业务逻辑和 Web 端几乎一致,差异主要在于 UI 层和一些平台能力调用。我在选型前有心理准备:Uniapp 的性能上限和原生开发有差距,但个人项目能一套代码跑三个平台,省下的时间远远超过偶尔的适配成本。
实际开发中遇到最多的坑是平台差异化处理。比如日期选择器,Web 端用 Element Plus 的 el-date-picker,小程序里要换成 uniapp 自带的 picker 组件;金额键盘,H5 上可以直接用 input 的 type=number,小程序里为了弹出纯数字键盘要设置 confirm-type 和调整 input 模式。我的处理方式是把这类差异封装成自定义组件,内部判断运行平台,外部暴露统一接口,业务页面不感知平台差异。
还有一个教训是关于字体和布局的。小程序和 H5 的默认字体渲染差距很大,同样的 margin 在两端看起来不一样。后来我把所有尺寸都基于设计稿用 rpx 单位定义,小程序端无缝适配,H5 端通过 postcss 插件把 rpx 换算成 rem,总算解决了两端视觉不一致的问题。这套适配方案折腾了两三天,但一次解决后续所有页面的问题,回过头看是值得的。
4.3 前后端联调与接口文档管理
全栈开发里最容易被忽略的就是接口文档,但这个环节做得好不好,直接影响前后端联调效率。我没有用 Postman 的手动文档,而是选择了 Apifox 这类工具,后端把 OpenAPI 规范导入后,自动生成接口文档和 Mock 数据,前端直接拿 Mock 数据开发,不用等后端接口写完。
联调过程中我发现一个经验:接口的返回结构一定要统一。我的返回格式定为{ code: 0, message: "success", data: {...} },code 为 0 表示成功,非 0 表示业务错误。前端封装了一个 request 工具函数,拦截所有响应,code 非 0 时统一弹错误提示,业务代码里只需要关心正常流程。这样处理下来,全项目接近 50 个接口,前端请求逻辑只写了一次,后续所有页面都是调用同一个 request 函数,代码量大幅减少。
跨域问题在前端联调时也踩过。开发环境下 Vue 的 Vite 配置了 proxy 代理,把/api开头的请求转发到后端 8080 端口,后端 CORS 中间件放行本地开发地址。生产环境用 Nginx 做反向代理,前端静态文件和后端 API 都挂在同一个域名下,由路径区分,天然解决了跨域,还省了 HTTPS 证书配多个域名的麻烦。
5. 部署上线与性能优化实录
5.1 Docker Compose 一键部署方案
部署环节我选择了 Docker Compose,把整个项目打包成三个容器:前端 Nginx 容器、后端 Golang 容器、MySQL 容器,Redis 因为是轻量服务直接装在宿主机上,少一层调度开销。Docker Compose 的好处是配置一目了然,一条docker-compose up -d就能把整套环境拉起来,换服务器迁移也方便。
这里有个部署顺序要注意:MySQL 容器要先起,然后等待健康检查通过再启动后端,否则后端启动时连不上数据库会直接退出。我在 Compose 文件里给后端加了 depends_on 条件和 healthcheck,确保 MySQL 初始化完成后再拉起后端。前端容器则简单一些,构建时先跑npm run build生成静态文件,然后打进 Nginx 镜像,用官方 nginx:alpine 镜像作为基础,把构建产物 COPY 到/usr/share/nginx/html目录。
Nginx 的配置里除了静态文件服务,还有一个关键的反向代理配置,把/api路径的请求转发到后端的 8080 端口。另外我配了 gzip 压缩,前端静态资源的传输体积直接降了 60% 左右。移动端应用如果要上架微信小程序,还需要在小程序后台配置服务器域名,这里用 HTTPS,所以给域名配了免费的 SSL 证书,并在 Nginx 里强制跳转 HTTPS。这套部署方案跑了大半年,服务器负载常年低于 20%,个人项目绰绰有余。
5.2 数据库索引优化与慢查询排查
项目上线一段时间后,流水表数据积累到几万条,我注意到统计页某个接口偶发变慢。排查下来是慢查询导致的,用 MySQL 的慢查询日志定位到一条统计 SQL,在没有走索引的情况下了扫了全表。问题出在复合索引的字段顺序上,我之前建的是 (user_id, category_id),但实际查询更多是按 (user_id, transaction_time) 过滤的,索引没有命中。
修复方案很简单:把索引调整为 (user_id, transaction_time, category_id) 的联合索引,覆盖了时间和分类两个维度的查询。同时给 account_id 单独建了一个普通索引,因为账户余额查询也走这条路。优化完重新跑了一次 EXPLAIN,type 从 ALL 变成了 ref,扫描行数从几万降到了几十,查询耗时从 800ms 降到了 20ms 以内。这个体验让我深刻理解了索引设计不是一步到位的,而是要依据实际查询模式持续调整。
还有一个细节是关于分页深度的。传统的 LIMIT 分页在数据量大时会有性能隐患,因为 MySQL 要把前面所有数据都扫描一遍再丢弃。记账系统里用户翻到第几十页的场景不多,但我还是做了优化:把基于页码的分页改成了基于游标的分页,请求参数带上上一页最后一条记录的 ID,SQL 里用WHERE id < ?来获取下一页。这样不管翻多少页,查询效率都是恒定的。不过要说明,这种方案只适合列表按时间倒序排列的场景,不能用于排序条件频繁变化的场景。
5.3 从开发到上线的完整流程回顾
整个项目从立项到上线,我大致安排了这样的节奏:第一周做需求梳理和数据库设计,周末集中时间把表结构敲定;第二周到第三周做后端核心接口,每天下班后写两三个小时,先把记账、分类、账户三个基础模块打通;第四周做前端 Web 页面的开发,同时把后端接口联调好;第五周做移动端 Uniapp 移植和 AI 智能分类;第六周集中部署、压测和修复 bug。
这个节奏实际上前松后紧,到第五周的时候有点赶了。回头看,主要原因是对 Uniapp 的适配工作量预估不足。如果重新排计划,我会把移动端的时间多留一周,或者考虑直接把 Web 端做响应式适配先上线,移动端作为二期再迭代。这也是个人项目的一个常见问题——总觉得自己能更快,实际上每个技术细节都会消耗超出预期的时间。好在最终项目还是顺利上线了,从第六周末开始正式使用,到现在已经跑了将近四个月,累计记账 5000 多笔,系统稳定运行没有出现过大故障。
6. 常见问题与排坑经验速查表
6.1 数据库与后端典型问题实录
这里整理一份我在开发过程中遇到的最典型的几个问题,按出现频率排序。第一个是 MySQL 连接数爆掉的问题。Gorm 默认的连接池配置比较保守,并发请求一多就会报too many connections。解决方法是在 Gorm 初始化时手动设置连接池参数:SetMaxOpenConns 设置为 20,SetMaxIdleConns 设置为 10,SetConnMaxLifetime 设置为一小时。记住一个原则:连接数不是越多越好,重点是复用,连接池里的连接要尽量长期存活,避免频繁创建和销毁。
第二个问题是 Gin 框架下参数校验的坑。Gin 自带的 ShouldBindJSON 在字段类型不匹配时返回的错误信息比较晦涩,全是英文的长串。我在中间件里封装了一层错误翻译,把校验错误转换成中文提示返回给前端。比如金额字段传了非数字,提示“金额格式不正确”,这样用户体验好很多。这个封装大概花了半小时,收益是长期的,强烈建议大家做。
第三个问题是时间处理的一致性。Golang 的时间格式和前端 JavaScript 的 Date 对象在处理时区时的行为不同,一开始我直接用time.Now()存数据库,前端拿到 UTC 时间后不转换直接展示,结果差了 8 个小时。后来统一约定:所有时间戳都用整数存储,记录的是 Unix 时间戳,展示层的时区转换由前端负责。这样后端不关心时区,前端按自己运行环境的时区解析,问题彻底解决。记账系统的时间精度到秒就够,不需要毫秒级的时间戳。
6.2 前端与移动端的典型问题实录
前端踩的坑主要集中在这几个地方。第一个是 Vue 3 响应式数据的丢失问题。从接口拿到数据后直接赋值给 reactive 对象,然后修改这个对象的某个属性,界面不刷新。排查半天,发现是我在定义数据时没有把完整的数据结构先声明出来,后续动态添加的属性不是响应式的。解决方法很简单:初始化时把所有可能用到的字段先声明成默认值,或者用 ref 包裹整个对象。这个坑几乎每个 Vue 3 新手都会踩,虽然文档里写了,但实际遇到时很容易忽视。
第二个问题是 ECharts 图表在 Tab 切换后不渲染或渲染异常。原因是图表容器在隐藏状态下宽度为 0,ECharts 初始化时拿到错误的尺寸。解决办法是在 Tab 切换到图表页时,调用chart.resize()方法重新计算尺寸,或者在初始化前用nextTick等待 DOM 完成渲染。另外销毁图表实例时要用chart.dispose(),释放内存,否则在 SPA 中反复切换 Tab 会导致内存持续增长。
第三个是 Uniapp 小程序端的请求封装差异。小程序的 request API 和浏览器 XMLHttpRequest 的差异导致我在封装 request 工具函数时需要判断运行环境,分别处理。具体来说,小程序的uni.request成功回调里的statusCode需要单独判断,而浏览器里通常用response.ok。为了统一,我在封装的工具函数里强制把非 200 的状态码都 reject 出来,这样业务代码里统一的 try-catch 就能覆盖两个端。
6.3 一套可以直接抄的检查清单
如果你也想做类似的个人全栈项目,我整理了一份上线前检查清单,按照这个顺序过一遍,能避免大量上线后的临时抢修。
后端检查项:数据库索引是否覆盖了核心查询路径;敏感配置是否已通过环境变量注入而不是硬编码;JWT 密钥是否足够长且已更换为随机值;接口是否做了参数校验和越权检查;日志是否记录了请求耗时和错误堆栈;是否设置了连接池参数避免连不上数据库。
前端检查项:表单提交是否有 loading 状态防止重复提交;接口异常是否有统一的错误提示;金额输入是否有格式校验和精度处理;列表页是否有空状态和加载失败状态;路由切换是否在组件销毁时清理定时器和图表实例。
部署检查项:Docker 容器是否有健康检查;MySQL 数据是否有定时备份任务;Nginx 是否配置了 gzip 和 HTTPS;日志是否有收集和轮转策略;服务器磁盘空间是否有监控报警。
我这套系统经历了 20 多个版本的迭代,每次上线前都过一遍这份清单,踩坑率大大降低。建议你也根据自己的技术栈维护一份属于自己的检查清单,这是从“能跑”到“靠谱”的关键一步。
7. 一些深入的使用技巧和个人经验总结
7.1 预算提醒功能的实现细节
预算提醒是我自己用了两周后强烈想加的一个功能。记账的意义不只是事后统计,更重要的是事中控制。我的实现思路是:用户在预算表里设定每个月的分类预算金额,比如餐饮预算 3000 元。每次记完一笔支出后,系统计算这个月该分类的累计支出和预算的比值,如果超过 80%,就返回一个提醒字段,前端弹一个轻量提示“本月餐饮预算已使用 80%”。
这个计算如果每次记账都实时做,会有一点额外的 SQL 开销。我的优化方案是:预算使用率的计算拆成一个独立的接口,不在记账主流程里同步执行,而是记账成功后由前端异步拉取一次。这样主流程响应时间不受影响,预算提醒是增强体验的功能,允许有几秒钟的延迟。这个设计思路可以推广到其他非核心功能——永远不要让增强功能拖慢核心路径。
7.2 数据导入导出的工程化实践
个人记账系统用久了,数据的安全和可迁移性就变得很重要。我给系统加了 CSV 导入导出功能。导出比较简单,把流水列表按查询条件导出成 CSV 文件,前端拿到 Blob 对象后触发浏览器下载。导入则相对复杂,需要考虑字段映射、格式校验、重复检测。用户上传 CSV 后,后端逐行解析,校验金额格式、日期格式、分类是否存在,有错误的行要返回具体行号和错误原因,让用户修正后再导入。
这里有个细节值得分享:导出的 CSV 文件在 Excel 中打开时可能会乱码,原因是 Excel 默认用 GBK 编码读取 CSV,而程序生成的是 UTF-8。解决办法是在 CSV 文件开头加上 UTF-8 BOM 头(\xEF\xBB\xBF),Excel 就能正确识别。这个坑看起来不起眼,但真到使用的时候能省去大量“为什么乱码”的困惑。
7.3 这套架构后续还能怎么演进
最后聊一点对技术演进的想法。当前的架构已经能很好地支撑个人记账场景了,但它的可扩展性其实比想象中要强。后端所有模块都是独立 service 层的设计,如果以后要加一个“多人共享账本”的功能,只需要新增一个账本表和成员关系表,在 service 层处理好权限校验就行,不会动到现有代码的核心逻辑。数据量如果增长到百万级,可以把统计接口改成离线预计算,每天凌晨定时生成统计快照,查询时直接读快照,性能依然能保持稳定。
AI 方向也有很大的延展空间。目前只做了智能分类推荐,后续可以接入消费习惯分析——让 AI 定期总结这个月的消费情况,给出改善建议;还可以做语音记账,说话直接转文字,再通过 AI 解析成结构化账单。这些能力在现在的 API 体系下都已经具备,主要的工作是在业务层做适配和打磨交互体验。对我个人来说,这个项目的意义不仅是掌握了一套全栈技术栈,更是体会到了「从需求出发选技术、从用户视角打磨产品」的完整流程。如果你也正在考虑做自己的第一个全栈项目,我的建议很简单:找一个你真正每天都会用的小需求,动手做,坚持用,然后持续迭代。技术都是在解决真实问题的过程中进步的,比单纯看教程效率高得多。