ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue3个人理财系统实战:从数据库设计到部署

SpringBoot+Vue3个人理财系统实战:从数据库设计到部署 做个人理财系统这个选题其实是个很经典的练手项目但经典不等于简单。它既要处理用户的账目数据牵扯到账户、分类、预算、账单这些实体关系又要保证页面交互直观、图表统计好看还涉及金额精度、权限隔离这类细节。正好你手头的这套源码用的是 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 的组合前后端分离、主流技术栈、还带了文档拿来学习或者二次开发都很合适。这篇文章我就按照自己折腾这类项目时的思路从整体设计、数据库建模、后端 CRUD 落地、前端页面实现到环境部署完整拆一遍这套系统的核心脉络。你如果正准备跑起来或者想弄明白每个模块为什么这么写按着下面的路线走基本不会迷路。1. 项目定位与整体设计思路1.1 个人理财系统到底在解决什么问题先说需求。个人理财系统的用户画像很清晰一个人要记录自己的收入、支出知道钱花到哪儿去了月底一看账单能明白哪些开销是不必要的。所以它的核心功能不外乎这几块用户注册登录区分不同账号的数据不能让 A 看到 B 的账。账户管理比如现金、银行卡、支付宝、微信零钱每个账户独立记账。收支流水每一笔钱是收入还是支出属于哪个分类备注是什么发生在哪个账户。分类管理餐饮、交通、购物、工资、理财收益分类可以自行增删改。预算控制给某个分类或者整体设置月度上限超支给提示。数据统计用图表展示月度收支趋势、分类占比方便复盘。你自己对照着市面上那些记账 App 想一下就明白了个人理财系统的本质就是一个带统计功能的 CRUD 系统。但“带统计”三个字让它的难度上了一个台阶你不能只写简单的增删改查还要考虑时间范围筛选、聚合查询、日期分组、换算账户金额这一套下来对 MyBatis-Plus 的高级用法和 MySQL 的聚合函数都是一个实打实的考验。1.2 技术栈选型背后的取舍逻辑这套源码选型是 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0看第一眼可能会选型担心说 Spring Boot 3 都出来了怎么还在 SpringBoot2 这边折腾放在企业开发场景里SpringBoot2 的项目存量还非常大稳定性和第三方生态都经过考验学习资料也最全。你用这套源码练手后面换 SpringBoot3 其实迁移成本很低核心差别无非是 javax 换成 jakarta、一些自动配置类的变化业务代码几乎不用动。后端 SpringBoot2负责提供 RESTful API内置 Tomcat打 jar 包就能跑不需要额外部署容器。配合拦截器做登录校验配合全局异常处理统一返回结构开发效率比 SSH 时代高太多。前端 Vue3用组合式 API 组织业务逻辑配合 Vite 构建开发体验清爽。个人理财这种表单密集、表格密集、图表密集的页面Vue3 的双向绑定和响应式处理非常顺手。MyBatis-Plus这是 MyBatis 的增强工具最大的价值是单表 CRUD 不用写 SQL。你自己建实体类继承 BaseMapper 之后insert、deleteById、selectById、updateById 直接就能用省掉一大半重复的 XML 映射文件。复杂统计查询再用注解 SQL 或者 Wrapper 自定义条件补齐。MySQL 8.0默认字符集 utf8mb4支持窗口函数、CTE日期处理也更规范。理财数据涉及金额、日期、分类统计MySQL 8.0 的 JSON 函数和窗口函数都能派上用场。你拿到这套源码之后第一步不要急着跑起来先把 pom.xml 和 package.json 打开扫一眼确认依赖版本之间的兼容关系。我见过不少人直接把 SpringBoot 版本升上去然后把 MyBatis-Plus 的依赖拉旧了启动直接报错回头还以为是代码问题。2. 数据库设计理财系统的地基怎么打2.1 核心表结构与字段设计理财系统的数据模型并不复杂但每一个字段都值得推敲。这里把常见的核心表给你拆出来源码里的设计大概率也是这个路子用户表sys_user用户表存的是登录凭据和基础信息。id 用自增主键就行username 必须加唯一索引password 存的是 BCrypt 加密后的密文。这里特别提醒密码一定不要明文存储哪怕你只是自己练手也要养成加密的习惯。另外加一个 status 字段控制账号是否禁用加一个 create_time 记录注册时间。账户表account账户表属于资产管理维度字段包括账户名称、账户类型现金/银行卡/第三方支付、初始余额、当前余额、所属用户 id。当前余额这个字段有两种维护方式一是每次流水变动时同步加减二是只记录初始余额通过汇总流水计算实时余额。前者查询快但容易出现数据不一致后者一致性高但统计时要多算一步。这套系统我建议用第一种同时配合定时对账任务做修正因为个人理财系统通常不拆太复杂的服务同步更新成本可控。分类表category分类表的作用是给每一笔流水打标。设计上要注意父级分类的概念收入分类里可以有“工资”“理财”“兼职”支出分类里可以有“餐饮”“交通”“购物”你也可以做成两级结构比如“餐饮”下面再拆“早餐”“外卖”“聚餐”。字段上至少包含分类名称、类型INCOME/EXPENSE、父级 id、排序值、所属用户 id。交易流水表transaction这是整个系统最核心的表每一笔账都记在这里。字段包括用户 id隔离数据归属账户 id这笔钱从哪个账户进出分类 id属于哪个分类交易类型收入还是支出金额用 decimal(10,2)绝不用 float 或 double交易时间注意和创建时间区分备注这里有两个坑想专门点一下。第一金额字段在 Java 里对应 BigDecimal在 MySQL 里对应 decimal任何前端传过来的金额在入库前都要做精度校验和格式化。第二交易时间和创建时间一定要分开用户可能补记几天前的账你不能把创建时间当作交易时间去做统计。预算表budget预算表常见的设计是设置一个周期按月关联分类或账户包含预算金额、周期起始日期、周期结束日期、实际已用金额。实际已用金额也可以用统计查询实时计算但为了列表展示方便维护一个冗余字段也可以。2.2 外键、索引与数据隔离的实操建议很多人在设计表结构时喜欢加物理外键个人理财系统这种规模我建议不要加外键约束保留逻辑外键就够了。理由很简单外键约束在删除和更新时会引入额外的锁开销而且你很容易触碰到删除顺序的问题。比如你要删一个分类但流水表里还有记录引用它直接物理删除会失败。更好的做法是加一个 deleted 字段做逻辑删除或者在删除分类前把相关流水的分类置为“未知”。索引是另一个重点。交易流水表是查询最频繁的表下面几个索引值得加(user_id, transaction_time)用户的流水按时间范围统计(user_id, category_id)按分类汇总支出(user_id, account_id)按账户查流水MySQL 8.0 里索引默认使用 B 树InnoDB 引擎下主键用聚簇索引所以主键设计用自增 id 而不是 UUID 是有性能考虑的。UUID 做主键会导致页分裂和索引碎片自增主键顺序写入性能更好。如果你后续要分布式部署那时再考虑雪花 ID单机个人项目自增完全够用。写 SQL 时还建议遵守一个习惯所有查询都带 user_id 作为硬条件。不要指望拦截器帮你自动拼接多写一个 where 条件既防止数据越权也倒逼自己理顺业务逻辑。3. 后端核心实现从通用 CRUD 到复杂统计3.1 基于 MyBatis-Plus 的通用 CRUD 服务MyBatis-Plus 最大的亮点就是通用 CRUD 服务。你在源码里会看到 service 层一般继承 ServiceImplMapper, Entity然后 Mapper 接口继承 BaseMapper 。这样写完之后基础的 insert、delete、update、selectById、selectList 方法就都有现成的了根本不需要自己写 SQL。资金流水新增和编辑属于高频操作我的习惯是在 Service 层封装一个统一的 saveOrUpdate 方法里面做三件事校验参数、组装实体、调用 MyBatis-Plus 的 saveOrUpdate。注意 MyBatis-Plus 的 saveOrUpdate 是依据主键是否为空来判断走 insert 还是 update所以前端新增时不要传 id编辑时必须带 id。这个逻辑如果你自己没注意很容易出现新增记录把已有记录覆盖掉的情况。查询列表时推荐用 LambdaQueryWrapper 来构造条件例如LambdaQueryWrapperTransaction wrapper new LambdaQueryWrapper(); wrapper.eq(Transaction::getUserId, userId) .eq(StringUtils.hasText(categoryId), Transaction::getCategoryId, categoryId) .between(startDate ! null endDate ! null, Transaction::getTransactionTime, startDate, endDate) .orderByDesc(Transaction::getTransactionTime);这样写的好处是代码可读性强而且条件可以动态拼接。注意 eq 方法可以带一个 Condition 参数条件不满足时自动忽略这个条件这就是无状态增删改查的思路调用方传什么条件Wrapper 就拼什么条件逻辑干净又灵活。3.2 子模块拆解分类、账户、交易、预算后端模块如果按业务域拆最少也要拆出用户、分类、账户、交易、预算、统计六块。下面把每一块的关键点列一下用户模块用户模块主要处理注册、登录、获取当前用户信息。登录成功后我建议返回一个 token前端存到 localStorage 里后续每次请求在 header 里带上 Authorization。后端用拦截器解析 token 并设置当前用户上下文。JWT 是个轻量选择但是注意 JWT 一旦签发就不能服务端主动失效所以如果要踢人下线或者强制退出还需要额外引入 Redis 黑名单机制。个人项目可以先不加但要意识到这个限制。分类模块分类模块就是一套基于父级 id 的树形结构管理。查询时可以一次性查出当前用户全部分类在内存里组装成树返回给前端。为什么不在 SQL 里递归查因为数据量很小一次查出来内存组装更方便控制排序和层级。账户模块账户模块涉及余额变动要处理好两个账户之间的转账场景。转账本质是两条流水转出账户记一笔支出转入账户记一笔收入两者金额一致关联一个相同的转账单号。如果是在事务里操作必须保证这两条流水同生共死。我建议在数据库设计时加一个 relation_no 字段存储关联单号方便排查。交易模块交易模块的增删改查本身不复杂复杂的是与账户余额联动。新增收入时账户余额要增加新增支出时账户余额要减少删除或修改交易时还要还原之前的余额影响。所以交易操作和账户余额更新一定要放在同一个事务里否则就会出现账实不符。预算模块预算模块本质是“固定周期 实时统计”。前端选择月份后端根据月份计算该月起始和结束日期再关联分类查询该分类下流水总额和预算金额做对比。这里要注意 MySQL 的日期边界如果交易时间字段是 datetime查询某月数据时条件要写成 2026-03-01 00:00:00 AND 2026-04-01 00:00:00而不是用 between 包住 3 月 31 日 23:59:59否则会漏掉最后一秒的记录。3.3 权限拦截与全局异常处理这种系统不该让每个 Controller 里都重复写权限校验。拦截器统一处理的思路是这样的注册拦截器拦截除了登录、注册以外的所有 /api/** 请求。拦截器里从 header 取 token解析出 userId放入 ThreadLocal。Controller 层用一个工具类获取当前登录用户 id。写到这里额外提醒一句ThreadLocal 用完一定要 remove否则在高并发复用线程时会出现数据串号。虽然个人项目不太容易暴露这个问题但习惯要养好。全局异常处理也要做。我的方案是定义一个统一的 Result 返回结构包含 code、message、data 三个字段然后配合 RestControllerAdvice 捕获业务异常和系统异常。这样前端 axios 拦截器只需要判断 code 是否为 200处理逻辑非常简单。4. 前端 Vue3 实现页面、状态与图表4.1 工程结构与页面路由设计前端工程用 Vite 创建 Vue3 项目目录通常长这样src/api封装 axios 请求src/router路由配置src/storePinia 状态管理src/views页面组件src/components公共组件路由设计上建议做一套带嵌套布局的结构外层是主布局侧边栏 顶部栏内部是各个功能页面。比如/dashboard、/transaction/list、/transaction/create、/category/index、/account/index、/budget/index、/profile/index。为了做登录拦截路由守卫里要判断有没有 token没有就跳到/login。4.2 Composition API 组织业务逻辑Vue3 相比 Vue2 最大的变化就是 Composition API。你会在源码里看到所有业务组件都写成script setup的格式然后按照逻辑把代码组织成几个功能块数据声明、生命周期、方法。给你一个交易列表页的伪思路参考const queryParams reactive({ pageNum: 1, pageSize: 10, categoryId: , startDate: , endDate: }) const loading ref(false) const tableData ref([]) const total ref(0) async function fetchData() { loading.value true try { const res await getTransactionList(queryParams) tableData.value res.data.records total.value res.data.total } finally { loading.value false } }reactive 适合定义嵌套对象ref 适合定义基本类型和独立变量。用错也能跑但语义和性能会有区别。页面里如果只是管一个字符串状态用 ref 就完了没必要动不动 reactive 套一个对象。对了很多人问 Vue3 和 Vue2 有什么区别拿这个项目里最常见的场景举例Vue2 用 options API 把 data、methods、watch 分开功能越多代码越散Vue3 用 setup 把同一块业务的响应式变量和操作函数放一起改一个功能时不需要上下翻文件。二三十个页面的管理后台这个优势非常明显。4.3 状态管理与其他生态组件搭配个人理财系统这种项目全局状态一般就两个用户信息和系统配置。用 Pinia 管理用户信息就够了不需要把每一页的表单数据都放到 store 里。用户登录成功后把用户信息写入 store同时持久化到 localStorage页面刷新后重新拉取用户信息。UI 库方面目前后台管理项目首选还是 Element Plus。表格用 el-table表单用 el-form日期选择用 el-date-picker弹窗用 el-dialog再加上 v-loading 指令这套组合基本能覆盖理财系统的全部交互场景。图表部分我见过用 ECharts 的也有用 AntV G2Plot 的。统计页面的月度趋势图建议用折线图分类占比建议用饼图账户余额变化用面积图。ECharts 接入 Vue3 时注意用 ref 绑定容器 DOM在 onMounted 里初始化实例组件卸载时调用 dispose 释放资源避免内存泄漏。5. MySQL 8.0 环境准备与部署落地5.1 MySQL 8.0 安装与字符集时区配置不管你是本地开发还是服务器部署MySQL 8.0 装完之后有几件事必须立刻做。字符集方面MySQL 8.0 默认字符集是 utf8mb4这比 5.7 时代默认 latin1 要省心很多。你建库时最好再显式指定一次CREATE DATABASE finance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;字符串比较排序用 utf8mb4_unicode_ci 或 utf8mb4_general_ci 都行。理财系统涉及中文分类名用 unicode 系列更稳。注意表结构里的 varchar 字段长度也要给足分类名称、备注字段建议 255别扣扣搜搜给 50后面用户写长备注直接报错。时区方面个人理财系统的交易时间统计非常依赖时区。MySQL 8.0 默认时区可能不是东八区你在连接串里一定要显式指定jdbc:mysql://localhost:3306/finance?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue这里几个参数挨个解释一下。serverTimezone 解决 Java 与 MySQL 之间的时间偏差问题allowPublicKeyRetrieval 解决 MySQL 8.0 使用 caching_sha2_password 认证插件时客户端首次连接需要获取服务端公钥的问题useSSL 在本地开发时设 false 免得握手报错。5.2 运行数据库脚本与初始化数据拿到源码后一般会附带 SQL 脚本按顺序执行建库建表即可。如果没有初始化脚本你自己手工建表时建议把基础分类数据作为初始数据写进去省得用户登录后一堆分类要自己做。执行脚本有个细节想分享如果脚本文件里有中文注释或中文初始数据用命令行执行时可以指定默认字符集mysql -u root -p --default-character-setutf8mb4 finance.sql不做这一步很可能中文全变成乱码到时候排查半天发现是客户端字符集问题而不是数据问题。5.3 后端打包与前端构建的注意事项后端打包用 Maven 的 package 命令会生成一个可执行 jar。运行的时候用java -jar finance-server.jar --spring.profiles.activeprod如果你有多套环境配置推荐用 application-dev.yml、application-prod.yml 这种命名通过 profile 切换。数据库密码不要写死在配置里开发环境写明文图省事可以理解但部署到服务器上至少配合环境变量注入。前端构建就更简单了npm install npm run build构建产物在 dist 目录里。如果你前后端部署在同一个服务器可以用 Nginx 托管静态文件同时把 /api 路径反代到后端服务端口。注意前端请求的 baseURL 要和部署路径匹配不要开发时写死 localhost上线后又忘了改。6. 常见问题与排查实录实际跑项目时踩过的坑6.1 启动类报错与依赖冲突我见过太多人拿到这种多模块项目一启动就报spring.main.allow-circular-references或者 MyBatis-Plus 的Invalid bound statement (not found)。前者多半是 SpringBoot2.6 以上对循环依赖默认禁止导致的后者多半是 Mapper 接口没有被扫描到。排查方式按顺序来看启动类有没有加MapperScan包路径是否覆盖到 mapper 接口。看 mapper 接口的 XML 文件是否放在 resources 目录且路径与 namespace 对应。看 pom.xml 里是否少了 mybatis-plus-boot-starter 依赖。如果是循环依赖先别急着在配置里放开更多人是因为 Service 之间互相注入导致的重新设计一下调用关系把重复依赖拆出去才是正解。6.2 前端跨域与请求返回格式不一致前后端分离开发时跨域是最常见的问题。Vite 开发服务器可以配置代理解决server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }把/api开头的请求代理到后端端口后端就不需要额外开启 CORS生产环境 Nginx 同样用这种反向代理思路。还有一种情况是后端接口返回结构不一致有的接口返回 Result有的接口直接返回裸数据前端 axios 拦截器写起来就很别扭。我的建议是后端所有接口统一走 Result 包装校验失败时返回 code400业务异常时 code500前端拦截器统一处理。6.3 金额精度与日期边界问题金额精度问题一旦踩到基本就是大坑。Java 后端接收金额时用 BigDecimal前端展示时用 Number 类型中间经过 JSON 序列化高精度小数很可能被转成科学计数法或者丢失精度。解决办法是在后端实体类的金额字段上加注解例如JsonFormat(pattern 0.00) private BigDecimal amount;前端也要注意输入框里用户可能填 100.101 这种三位小数必须在前端校验两位小数否则入库不成功用户以为系统出 bug 了。日期边界问题前面提过一次查询某月流水时如果用 wrapper.between 传入当月第一天和当月最后一天很容易因为时间精度丢记录。正确做法是算起止边界起始是当月第一天 00:00:00结束是下月第一天 00:00:00用左闭右开区间。6.4 常见问题速查表现象大概率原因处理方法启动报 401 或跳登录页token 过期或未携带检查 axios 拦截器是否统一添加 Authorization header中文乱码数据库连接串缺字符集参数连接串加 characterEncodingutf8时间差 8 小时数据库时区和 JVM 时区不一致连接串加 serverTimezoneAsia/Shanghai接口返回的金额多了很多位JSON 序列化精度问题BigDecimal 字段加 JsonFormat 或统一配置MyBatis-Plus 分页不生效缺少分页插件配置配置 PaginationInnerInterceptor前端页面刷新就 404路由使用 history 模式但 Nginx 未配置Nginx 加 try_files 回退到 index.html用户删除后数据还在逻辑删除字段未配置实体类加 TableLogic 注解7. 复盘与延展围绕这套源码还能怎么玩按我个人的经验接手任何一套源码最高效的路径是先把流程跑通再画一张架构图最后挑一个模块做深度阅读。这套个人理财系统源码虽然核心功能在我看来都是标准实现但正因为标准它才适合用来验证整套 Web 开发的知识体系。拿到源码后我建议你做这几件事不要急着改代码先启动起来把注册、登录、记一笔账、看统计图这条主流程走一遍体会系统如何把数据从浏览器页面一步步带到 MySQL 表里。打开数据库手动插几条测试流水然后刷新页面看统计数字变化理解统计模块背后的 SQL 聚合逻辑。找一个你不满意的点动手改比如增加一个“债务管理”模块或者“周期性账单”功能从需求分析、建表、后端接口、前端页面全链路走一遍这个训练价值比单纯看代码高得多。如果后续想扩展还可以考虑接入 Redis 做 token 缓存使用 RabbitMQ 异步处理账目导入或者把部署方式改成 Docker Compose。不管怎么动这套源码给你打的底子都是扎实的SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0 这套组合本身就是目前中小型管理系统里非常主流的配置毕业设计、个人作品集、公司内部小工具都能直接往上套。最后再分享一个小技巧如果你在本地跑这套系统时发现数据库连接失败先别怀疑代码。打开任务管理器看 MySQL 服务有没有起来然后用命令行mysql -u root -p实测能不能连上。我调试过不少项目一半以上的数据库连接问题都是服务没启动、端口被占用或者密码不对真正代码写错的反而很少。先把环境问题倒干净再看报错日志效率会高很多。
返回列表