
“在家把账记明白比理财本身更费劲。”这句话我以前当调侃直到自己动手做了这套个人理财系统才发现一套Java SpringBootVue3MyBatis前后端分离的源码里藏着大量从需求到落地的门道。这套系统不追求花哨目标就是让一个人把日常收支、预算规划和统计报表全部握在自己手里数据存进MySQL接口由SpringBoot提供页面由Vue3渲染。这篇文章我会从需求拆解讲到技术选型、数据库设计、后端接口、Vue3前端页面再落到底层部署与常见坑位排查尽量还原我自己实现时的思考过程。适合有Java基础、想完整走一遍全栈项目流程的读者也适合拿来当毕业设计或简历项目的基础骨架。全文所有配置和代码都基于实际跑通的项目照着搭就能起步。1. 项目拆解个人理财系统的核心需求与整体设计1.1 记账、预算、统计个人理财系统的三个核心模块个人理财系统最核心的从来不是技术而是想清楚用户进来到底要干什么。我的理解浓缩成三个动作记录、规划、分析。记录对应收入和支出流水每笔交易要有分类、金额、账户、备注和实际消费时间规划对应预算管理在月初给某类支出设上限月底红线在哪一目了然分析对应统计报表按分类汇总、按月对比、看结余趋势。这三个动作做扎实系统就有实用价值。不要一上来就写注册登录那是基础设施不是业务核心。很多个人项目死在精力分配上——页面样式调得天花乱坠真正负责记账的CRUD反而漏洞百出。我开发时的优先级很明确交易流水第一预算和统计第二用户体系最后。顺着这个顺序排开发计划每阶段都有能演示的成果心态上也会轻松很多。设计时的细节决定了系统能不能日常使用。金额不能为负数删除分类前要检查有没有关联流水预算按自然月结算跨月后自动重置。这些“不算功能但必须处理”的点恰恰是demo和可用系统之间的分水岭。我在整套源码里把这类逻辑全部放在Service层统一处理避免了同一个规则在多个接口里分散实现。1.2 前后端分离的数据流转一次请求走完全链路前后端分离的本质是职责划分不是简单分成两个项目。Vue3负责页面渲染和用户交互通过HTTP请求调用SpringBoot提供的RESTful APIMyBatis负责把Java对象映射成SQL操作MySQL负责最终落盘。一次完整记账请求的链路是这样的前端表单收集数据后Axios发起POST请求到/api/transactions携带JSON体后端Controller接收后调Service做业务校验——金额大于零、分类属于当前用户、账户状态正常Service再调Mapper写入数据库成功后返回新增记录前端拿到响应后刷新表格区并重新拉取统计图数据。这条链路看着简单每一层都有各自的坑。JSON字段用驼峰还是下划线、日期格式传字符串还是时间戳、接口返回结构是否统一都会影响前后端协作效率。我在项目里统一用一个Result对象包装响应三字段结构——code/message/data前端在响应拦截器里统一处理而不是每个接口各自判断成功失败。这套约定一旦定下来后续加接口基本零思考成本。2. 技术选型硬核拆解SpringBoot、Vue3、MyBatis、MySQL各自解决什么问题2.1 SpringBoot把后端开发的配置成本打下来SpringBoot能成为Java Web项目的默认选项不是因为它技术多花哨而是它把Spring生态里大量重复配置固化成了约定。对个人理财系统这种典型CRUD加少量业务规则的项目来说SpringBoot的自动配置能省掉绝大部分XML配置一个starter依赖就能引入Web、MyBatis、数据库连接池等一整条链路。专注业务本身这就是它最大的价值。这里必须提一个我踩过的认知误区很多人喜欢追新直接上SpringBoot 3.x。3.x默认要求JDK17起步部分第三方starter的兼容性也确实不如2.x成熟。如果读者还在用JDK8或者项目要部署在比较老旧的服务器上SpringBoot 2.7.x反而是更稳的选择。个人理财项目对性能没有极端要求选2.7不会带来任何遗憾反而能在依赖选型上少踩一堆坑。2.2 Vue3Element Plus中后台前端的成熟组合Vue3相比Vue2最大的变化是组合式API和响应式原理重构。对个人项目来说用Composition API组织代码可以按业务维度聚拢逻辑——记账页面的数据请求、表单校验、表格刷新全放在同一个函数里不用像Vue2的Options API那样把代码零散分散到data、methods、computed里。配合Vite使用后开发时的热更新速度是肉眼可见的提升启动项目基本秒开。UI组件我选了Element Plus看中的是它在中后台场景的高度成熟。表格、表单、弹窗、日期选择器这些轮子直接拿来用能给开发省下大量手写CSS的时间。个人理财系统的界面不需要多炫酷布局清晰、按钮位置合理、统计图表直观就已经能带来良好的使用体验。组件库选对视觉下限就有了保障。2.3 MyBatisMySQLSQL可控与数据落地的务实搭档新人在选ORM时通常纠结JPA还是MyBatis。我的倾向非常明确像理财系统这种业务有一定复杂度、需要写多表聚合查询的场景MyBatis更合适。它虽然要手写XML或注解SQL但换来的是SQL执行的绝对可控。按月分组统计收支的SQL、分类汇总的近六个月趋势查询都能在XML里精确定义后续调优时也能直接看到完整SQL。MySQL在单用户级别下一台低配云服务器甚至本机就能扛住。这个项目的性能瓶颈几乎不会出现在数据库上真正要关注的是编码、时区、事务这些基础设置。表统一用InnoDB引擎保证事务支持和行级锁金额字段用DECIMAL而不是float/double时间字段建议datetime类型时区统一Asia/Shanghai。这些约定越早定下来后期返工越少。3. 核心表结构设计五张表撑起一套理财系统3.1 表结构设计用户、分类、流水、预算、账户怎么拆我设计的五张核心表分别是用户表、分类表、交易流水表、预算表、账户表。分类表和预算表是理财系统区别于普通CRUD demo的关键缺少它们就只能叫记账本而不是理财系统。用户表保留最简字段id、username、password、nickname、create_time。密码必须用BCrypt加盐哈希存储绝对不能拿MD5直接应付这是安全底线。分类表的结构比较有意思核心字段是user_id加type用type区分收入类和支出类前端用颜色区分后端统计时用case when拆开处理。交易流水表是整库的核心id、user_id、category_id、type、amount、account_id、note、trade_time、create_time缺一不可。我要特别强调trade_time和create_time必须分两个字段——一个是实际消费时间一个是记录创建时间补录旧账时如果混在一起统计报表会彻底失真。预算表按user_id加category_id加month做唯一约束month直接用YYYY-MM字符串避免用时间类型带来的月份格式化问题。账户表的设计服务于多账户场景用户可以有现金、工资卡、余额宝等多个账户流水表通过account_id关联。这个设计在后续扩展转账功能时非常关键。3.2 金额字段为什么必须用DECIMAL浮点精度坑实录这个坑几乎每个做财务相关项目的人都会踩一遍。float和double在Java和MySQL里都是二进制浮点表示0.1加0.2在小数位数多时会出现肉眼可见的精度误差结余计算越算越偏。个人理财系统虽然不是银行核心系统但账目对不上会让人瞬间失去信任。MySQL里金额字段统一用DECIMAL(10,2)Java实体对应BigDecimalSpringBoot返回JSON时默认也能处理成字符串形式前端拿到的就是精确的“12.34”而不是“12.3400000001”。这里有一个很隐蔽的坑BigDecimal比较要用compareTo而不是equals。我用equals比较两个数值相同的BigDecimal时因为两者的精度位数不同返回了false导致数据校验一直报错排查了快一个小时才定位到问题。建议在项目里封装一个金额工具类把加法、减法、比较和格式化的逻辑集中管理。4. 后端实战SpringBootMyBatis从零搭建核心接口4.1 项目初始化与四层结构一个清爽的起步姿势我习惯直接用Maven骨架起项目不太喜欢依赖IDEA默认模板因为模板里塞了很多用不到的依赖。最终的pom.xml非常精简spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-j、jjwt做登录令牌、lombok生成样板代码。个人项目依赖精简化好处是全项目依赖冲突的概率大幅下降构建速度也更快。分层结构是经典的四层controller处理接口和参数校验service承载业务逻辑和事务mapper做数据库访问entity/dto承载数据结构。我强烈建议把请求参数封装成DTO不要直接把用户传的JSON映射到数据库实体否则调用方可以在请求体里塞多余字段MyBatis动态SQL一不留神就会把脏数据带进数据库。application.yml里的配置有一个容易忽略的开关map-underscore-to-camel-case必须设为true。数据库字段是category_idJava实体属性是categoryId如果不开启驼峰映射查询结果会全部变成null。这个配置项一句话就能搞定不然后续每次写resultMap会写到怀疑人生。4.2 JWT登录认证用户体系的安全边界个人理财系统保存着用户的私密财务数据接口不能裸奔。我用JWT做无状态认证流程是登录成功后服务端生成带过期时间的token返回前端前端存localStorage每次请求在Authorization头带上后端拦截器统一校验。拦截器里的逻辑不复杂先放行/api/auth开头的接口再解析其余请求的token把用户ID塞进请求上下文解析失败或过期则返回401。这个机制有个容易被忽视的坑拦截器的执行顺序必须排在CORS跨域配置之后否则浏览器的预检请求会被拦截器提前挡掉前端看到的报错会非常难懂甚至让你误判为跨域配置问题。密码处理别省事。注册时用BCrypt加密登录时用matches校验哪怕后端只面向一个用户也要按规范来。这套源码里用户相关的密码永远不会以明文形式落库或传输这是对用户数据最基本的尊重。4.3 记账接口实现事务、校验与账户同步记账接口是整个系统的业务核心。我定义POST /api/transactions请求参数包括categoryId、amount、type、accountId、note、tradeTime。Service层按顺序处理四件事校验金额大于零确认分类属于当前用户写入交易流水表同步更新对应账户的余额。第四步必须在同一个事务里完成否则账户余额变了流水没写或者流水写了账户没变账目对不上只是时间问题。事务我用Transactional注解默认遇到RuntimeException就回滚。需要特别注意的是MyBatis执行写操作后要拿到自增主键在insert标签里加useGeneratedKeystrue keyPropertyid否则后续业务逻辑拿不到新记录ID。多条件查询是流水列表的高频需求。筛选条件包括日期范围、分类、类型、账户我全部用MyBatis XML动态SQL实现where标签加if标签拼出安全条件。分页直接手写limit参数个人项目完全不必要引入PageHelper插件少一个依赖就少一份版本冲突的风险。4.4 统计报表接口按月分类汇总的SQL写法统计是理财系统最能体现价值的地方。我实现两个核心统计接口按月分类汇总、按月份结余趋势。前者回答“钱花哪了”后者回答“存下了多少”。按月分类汇总SQL的核心是group by category_id和month(trade_time)配合sum聚合和case when区分收支。近六个月收支趋势的数据结构是[{month: 2026-01, income: 12000, expense: 8600}]后端一次查询把两个聚合结果都返回给前端直接喂给ECharts出图。接口返回的key一定要想清楚再定前端要什么格式后端就给什么格式别让前端在JavaScript里做一堆无意义的map转换。统计接口的数据量级在个人场景下很小不需要缓存也不需要额外优化。但SQL里要注意索引设计流水表建议在user_id和trade_time上建联合索引否则随着记录增多按月筛选的查询会越来越慢。这是整套源码里极少数需要未雨绸缪的性能点。5. 前端实战Vue3从脚手架到页面开发5.1 脚手架搭建用Vite初始化Vue3项目并规划目录前端用npm create vitelatest初始化项目模板选择Vue3加JavaScript。个人项目我不建议一开始上TypeScript先把业务架构跑通心智负担低很多。等整体结构稳定后再迁移类型定义也不迟。目录规划遵循中后台项目的通用范式src/api放接口封装src/stores放Pinia状态src/router放路由配置src/views按业务页面划分src/components放可复用组件。这套结构放到任何Vue3项目里都能直接用养成习惯后迁移成本极低。vite.config.js里的开发代理配置是前后端分离的第一步配置。我写server.proxy把/api开头的请求代理到http://localhost:8080同时设置changeOrigin。这个配置同时解决了开发环境的跨域问题也避免了在业务代码里写死接口地址的坏味道。之后部署到生产环境只需要改代理目标前端代码一行都不用动。5.2 Pinia与Axios封装状态和请求的统一入口Pinia是Vue3官方推荐的持久化状态管理方案。我的使用原则是一切从简只把登录用户信息和token放Pinia账目数据不往全局放。流水数据需要频繁更新放在页面组件里反而更容易控制刷新时机全局状态只放跨页面共享的部分。Axios封装是前端工程化最关键的一环。我在src/api/request.js里统一做几件事设置baseURL指向/api请求拦截器自动带Authorization头响应拦截器统一判断code字段——200直接返回data401清理登录状态并跳转登录页其他code弹出ElMessage提示。做完这层封装后每个业务接口文件只需要几行代码就能定义清楚接口变更时的改动范围也会大幅缩小。5.3 账单流水页面表格、筛选、分页三件套账单流水页面是用户使用频率最高的页面。我的布局是“筛选区加表格区加分页区”三段式筛选区包含日期范围、分类下拉、类型单选表格区展示时间、分类、账户、金额、备注金额用颜色区分收入和支出分页统一由后端完成前端只负责传页码和页大小。筛选条件变化时自动重新拉取数据这里需要厘清Vue3响应式细节。用reactive创建查询对象时如果直接修改属性并依赖监听要确保监听的是具体路径。我习惯用一个query对象整体替换来触发更新避免监听不到的变化这会让代码更直白。金额列统一用格式化函数展示后端返回的BigDecimal字符串前端不做任何浮点运算。分类列直接用后端JOIN查询得到分类名不需要在前端拿分类ID去查表。这类数据聚合交给后端处理页面逻辑会干净很多出错的概率也低。5.4 ECharts统计图收支报表的直观呈现统计页面是理财系统的门面我用ECharts做可视化两张核心图分类支出饼图和月度收支柱状图。饼图展示某月支出在分类间的分布柱状图对比近六个月的收入支出变化。ECharts数据更新时有一个特别容易踩的坑直接把响应式对象赋值给option可能导致图表刷新异常。我的做法是用watch监听数据源深拷贝一份再赋给option然后调用chart实例的setOption这样ECharts内部对option的修改就不会被Vue响应式代理干扰。图表容器一定要显式设置宽高。如果初始化时容器宽高为0图表渲染出来就是空白。我的习惯是在组件里固定容器尺寸页面布局变化时调用chart.resize()重新调整。还有一个细节值得注意统计页面的接口请求最好集中在onMounted里统一发起多个图表数据并行加载避免页面出现长时间空白。6. 前后端联调与部署经验跨域、打包、上线一步到位6.1 开发环境的Vite代理与生产环境的Nginx转发跨域问题吓退了很多初次接触前后端分离的开发者其实处理思路很清晰开发环境用Vite代理生产环境用Nginx反向代理或同源托管。开发环境因为前端跑在5173端口后端跑在8080端口天然产生了跨域。Vite的proxy配置解决了这个问题前端请求/api开头的地址全部转发到后端。有的同学喜欢在SpringBoot里加全局CORS配置来绕过跨域我建议开发环境尽量别这么做全局开放跨域等于降低安全门槛。生产环境更没必要讨论跨域只要前端页面和后端接口最终落在同一个域名或者通过Nginx转发浏览器根本感知不到跨域的存在。6.2 两种打包部署方案Vue3放进SpringBoot的实操部署方案有两种我分别试过。第一种是纯粹分离式前端npm run build生成dist目录放到Nginx指定路径后端jar包单独跑在另一个端口Nginx配置/api转发到后端。第二种是把前端构建产物复制到SpringBoot的src/main/resources/static目录让后端自己托管静态文件最终打成一个jar包。个人理财系统这种单用户项目我强烈推荐第二种方案省服务器资源运维成本低一个进程全包。实操上只需要修改Vite的build.outDir指向后端static目录打包时先后端再前端即可。这里还有一个路由模式的坑Vue Router如果用history模式后端需要处理路由回退否则直接访问某条子路径会404。最简单的方式是用hash模式URL里带#虽然不美观但不增加后端复杂度。如果坚持history模式后端需要加一个controller把所有非api请求转发到index.html。7. 实战中踩过的坑排查记录与避坑指南7.1 数据库时区差8小时连接串里的隐藏问题我第一版接口插入数据库的时间比本地时间少了整整8小时排查后发现是MySQL连接串没指定serverTimezoneJDBC默认取了UTC时区。解决办法是jdbc url里加serverTimezoneAsia/Shanghai同时补上useUnicodetrue和characterEncodingutf8解决中文乱码。时间字段的类型选择也要考虑。我统一使用datetime而不是timestamp原因是datetime不参与时区转换存的是什么取出来就是什么个人理财系统根本不需要跨时区能力。Java 8的LocalDateTime映射datetime类型没有任何兼容性问题整套流程用下来非常顺畅。7.2 MyBatis驼峰映射与动态SQL别在字段名上翻车MyBatis最常见的坑就是字段映射。数据库category_id对应实体categoryId不开启map-underscore-to-camel-case查询结果就全是null。我通篇坚持下划线字段名全局开启驼峰映射XML里写resultType就能直接接收不需要维护一堆resultMap。动态SQL的where标签用法值得多说一句。多条件筛选时用where加if拼条件可以避免and开头引发的SQL语法错误。我每次写完一个动态查询都会强制测两种场景一个带满条件的请求一个不带条件的请求确认SQL都不会出错后才算通过。这个习惯帮我挡住了不少低级错误。7.3 Vue3响应式数据丢失reactive与ref要分清Vue3响应式丢失是我前端开发中印象最深的一个坑。用reactive创建数组拿到新数据后直接整体赋值页面死活不更新。原因在于reactive包装的引用类型对象整体替换会失去原有代理的响应性。规避方法很简单列表这种需要整体替换的数据一律用ref声明赋值用.value表单这种细粒度更新的对象用reactive。两个API各管一摊之后我再也没遇到过页面不刷新的诡异问题。新手如果发现数据变了页面没反应优先检查是不是reactive整体赋值。7.4 LocalDateTime序列化前后端日期格式对齐SpringBoot默认把LocalDateTime序列化成类似“2026-01-05T12:00:00”的格式前端日期组件拿到后完全无法直接显示。解决方式是全局配置Jackson的日期格式化规则把LocalDateTime格式化成“yyyy-MM-dd HH:mm:ss”LocalDate格式化成“yyyy-MM-dd”。前端查询日期范围时我习惯传startDate和endDate两个字符串参数后端用DateTimeFormat按指定格式解析。这种方式接口语义直观也不容易出现时区偏移和格式拼接的问题。如果你的项目里有人为了省事传时间戳再被我看到我会建议他立刻改成字符串格式。最后再分享一句我自己的体会。把整套系统写完回头看技术栈其实非常常见真正的成长在于你把一个真实需求从头到尾捋顺的过程表设计里多考虑一个字段统计SQL怎么写才能让前端少做一次转换事务边界画在哪里部署时怎么少维护一个进程——这些都是教程里不会逐字教你的东西只能靠动手做一遍才能真正变成经验。这个项目的边界也完全开放后续可以接定时账单提醒、Excel账单导出、多账户转账流水每一步都是很自然的延伸。希望我踩过的这些坑能帮你把第一版做得比我更稳。