SpringBoot+Vue+MyBatis个人理财系统实战:从数据库设计到前后端落地 后台经常有人问我SpringBoot加Vue做个人理财系统到底怎么落地真正动手的时候问题全冒出来了——表怎么设计、收支分类怎么搞、预算怎么判断超没超、MyBatis的SQL到底写在哪个文件里。这篇我把这个项目从思路到实操的完整过程捋一遍Java MySQL MyBatis这套组合一套能跑通前后端的需求、设计、编码、部署全流程特别适合正在做课程设计、毕业设计或者想给自己写一个记账工具的同学参考。项目不算大但麻雀虽小五脏俱全账本、账户、预算、报表这些核心模块都有理解了它你就能顺藤摸瓜摸清大多数企业级管理系统的基本套路。1. 系统整体拆解为什么是这套技术组合1.1 技术选型背后的取舍逻辑先说结论这套组合不是最时髦的但一定是最稳妥、最容易跑通的。后端用Java SpringBoot这个几乎不用解释。SpringBoot把Spring那一套繁琐的XML配置全部自动化了内嵌Tomcat一个jar包就能起服务。个人理财系统这种体量用SpringBoot绰绰有余而且网上资料最丰富遇到问题随手一搜就有答案。持久层选MyBatis而不是JPA这个值得展开说说。JPAHibernate虽然写CRUD省事但一旦遇到复杂统计SQL比如按月份分组统计支出、按分类汇总金额你在注解里拼JPQL或者原生SQL的样子真的很难看。MyBatis把SQL完全交给你控制动态SQL能应对绝大部分查询条件组合而且项目里SQL看得见摸得着面试时也更好解释。MyBatis的另一个隐藏优势是学习曲线平缓对一个要交毕设或者刚入行的新人来说能明确知道这条数据是怎么查出来的比被框架魔法蒙在鼓里强得多。前端用Vue道理类似。Vue2的模板语法对后端出身的人极其友好你会写HTML就会写Vue模板。配合Element UI组件库表格、表单、弹窗、日期选择器全是现成的不用自己写样式。Vue Router管页面跳转Vuex管全局状态axios管HTTP请求这套组合是前后端分离项目里最成熟稳定的配方之一。数据库自然是MySQL。个人理财系统的数据量撑死几十万条MySQL完全能扛住而且它在Windows和Linux上都能一键安装导出SQL脚本就能分发对本地开发极其友好。1.2 单体架构下的模块划分与数据流向有人可能会问现在不都流行微服务吗这个系统要不要拆成用户服务、账单服务、报表服务我的回答永远是不要。个人理财系统的用户量和使用场景决定了它压根不需要分布式。你拆三个微服务光服务注册发现、配置中心、网关这些基础设施就够你喝一壶。单体应用把所有模块放在一个工程里分层清晰、部署简单、调试方便这才是最合理的架构选型。整个系统的数据流向是这样的前端Vue页面发起HTTP请求 → SpringBoot的Controller接收参数 → Service层处理业务逻辑比如校验预算、计算余额 → MyBatis的Mapper接口调用XML里的SQL → MySQL执行并返回结果 → 数据逐层封装回传给前端 → 前端用Vue绑定渲染页面。模块划分上我把它拆成六块用户模块注册、登录、用户信息维护账户模块管理银行卡、微信、支付宝、现金等账户分类模块支出分类和收入分类比如餐饮、交通、工资账单模块日常记账包括收支金额、所属账户、所属分类、备注预算模块设定每个分类的月度预算上限报表模块按时间和分类维度统计收支生成图表这六个模块相互独立又彼此关联。账单记账时要扣减账户余额报表统计时要关联分类名称预算判断时要汇总当月账单金额。每个模块都通过Service层串联不会出现前端一个请求直接穿透到Mapper的情况这是保证后期可维护性的底线。2. 数据库设计与核心表结构2.1 从记账场景反推五张表数据库设计是整个项目的地基这步做不好后面代码再漂亮也白搭。我的习惯是站在用户视角反推用户打开APP第一件事是登录登录后要看到账户列表、最近账单、本月预算和统计图表。把这些动作逐个拆开就能推出表结构。第一张表是用户表t_user。字段很简单主键id、用户名username唯一、密码password、创建时间create_time。密码必须加密存储不能明文存用bcrypt之类的哈希算法。第二张表是账户表t_account。用户不可能只有一个账户工资打进银行卡日常消费用微信闲置资金放余额宝。所以我设计了id、user_id、账户名name、账户类型type1现金、2储蓄卡、3信用卡、4第三方支付、余额balance、图标icon、创建时间。这里有个细节账户余额和账单之间有关系记账时都会同步更新账户余额。第三张表是分类表t_category。收入分类有工资、奖金、理财收益支出分类有餐饮、交通、购物、居住、娱乐。字段是id、user_id、分类名name、类型type1收入、0支出、父分类parent_id、创建时间。预留parent_id是给二级分类扩展用的比如餐饮下面可以挂早午餐外卖。第四张表是账单表t_bill这是整个系统的核心。字段id、user_id、account_id关联账户、category_id关联分类、amount金额、type类型1收入、0支出、occur_date发生时间、note备注、create_time创建时间。第五张表是预算表t_budget。字段id、user_id、category_id、month预算月份、amount预算金额。注意month用2025-06这种格式方便直接比较。五张表加在一起不到十个字段每张看起来简单但每一列都是从业务动作倒推出来的没有一个是凑数加的。2.2 建表要点与金额精度陷阱这里必须强调一个坑金额字段绝对不能使用double或者float必须用decimal。我第一次做这个系统的时候就吃了大亏。Java代码里写double money 19.99;看着没问题但你把一毛钱反复加减几次结果可能是0.30000000000000004。个人理财系统每天和钱打交道精度丢失是致命的。decimal(10,2)表示最长10位数、小数点后保留2位足够覆盖大多数个人用户的记账需求。建表SQL里有一个细节很容易被新手忽略字符集必须显式指定为utf8mb4。MySQL老版本的utf8是utf8mb3它存不了emoji表情比如账单备注里写个直接报错。utf8mb4才是真正的四字节UTF-8编码。我再把核心建表SQL贴出来方便你直接对照CREATE TABLE t_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码加密存储, create_time datetime NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE t_bill ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, account_id bigint(20) NOT NULL, category_id bigint(20) NOT NULL, amount decimal(10,2) NOT NULL COMMENT 金额, type tinyint(4) NOT NULL COMMENT 1-收入 0-支出, occur_date datetime NOT NULL COMMENT 发生时间, note varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime NOT NULL, PRIMARY KEY (id), KEY idx_user_date (user_id, occur_date), KEY idx_user_category (user_id, category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT账单表; CREATE TABLE t_budget ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, category_id bigint(20) NOT NULL, month varchar(7) NOT NULL COMMENT 月份格式yYYY-MM, amount decimal(10,2) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_user_cat_month (user_id, category_id, month) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预算表;2.3 索引与事务边界设计索引设计这块我的原则是索引不是越多越好但要覆盖高频查询条件。本系统高频查询有两个一是展示某个月份的账单列表通常带着user_id和时间范围条件所以在t_bill上建了(user_id, occur_date)联合索引二是统计某个分类的月度支出会用到user_id和category_id所以建了(user_id, category_id)联合索引。这里有个排序问题联合索引的最左前缀原则要求等值条件放前面范围条件放后面所以user_id永远放在索引最前面occur_date这种范围查询放后面这样索引命中率最高。关于什么时候删t_bill不受限制是个常见疑问。我的建议是如果账单经常按月份批量清理可以按时间做物理删除如果需要保留历史审计记录就加个deleted字段做逻辑删除。个人项目为了简单物理删除完全够用。事务边界我定在记账这个动作上插入一条账单、更新对应账户余额、检查当月预算这三个操作必须全部成功或者全部回滚不能出现账单写进去了但余额没更新这种数据不一致的情况。用Transactional注解标记Service方法即可回滚条件是rollbackFor Exception.class因为Spring默认只回滚RuntimeException遇到普通异常不会回滚这个坑必须主动规避。3. 后端接口与MyBatis实操落地3.1 三层结构与统一响应封装后端代码我按经典的Controller-Service-Mapper三层来组织。实体类放在entity包Mapper接口放在mapper包XML文件放在resources下的mapper目录Service接口和实现类放service和service.impl包Controller放controller包DTO和VO分别放dto和vo包还有公共的common包放统一响应体、全局异常处理器、工具类。这里特别说明common包里的统一响应体。每个接口都返回一个ResultT对象结构是code、message、data三个字段比如{ code: 200, message: success, data: { ... } }前端拿到响应后先看code再决定是渲染数据还是弹错误提示。这样做的最大好处是前后端解耦任何接口的错误处理逻辑都相同前端封装一个axios拦截器就能全局捕获。全局异常处理器也用RestControllerAdvice标注业务异常比如余额不足抛一个自定义的BizException全局异常处理器统一捕获并转成Result.error()返回。这一套落地后Service层代码会非常干净只需专心写业务不用到处写try-catch。3.2 账单查询的动态SQL与分页账单查询是本项目里最典型的MyBatis场景没有之一。用户在前端筛选条件可能包含账户、分类、类型收/支、起始日期、结束日期这些条件任意组合用Java代码拼接SQL低效又容易出错用MyBatis动态SQL则非常巧妙。核心的Mapper方法是这么设计的select idselectBillPage resultTypecom.demo.finance.entity.Bill SELECT * FROM t_bill where if testuserId ! null AND user_id #{userId} /if if testaccountId ! null AND account_id #{accountId} /if if testcategoryId ! null AND category_id #{categoryId} /if if testtype ! null AND type #{type} /if if teststartDate ! null AND occur_date gt; #{startDate} /if if testendDate ! null AND occur_date lt; #{endDate} /if /where ORDER BY occur_date DESC, id DESC /select注意这里有两个细节。第一where标签会自动去掉第一个多余的AND所以每个条件写AND开头没问题第二XML里和会被当成标签语法解析必须写成gt;和lt;或者用![CDATA[ occur_date #{startDate} ]]包起来。我建议比较操作符都用转义或者花一个周末把所有动态SQL都重构用CDATA两种方式都能避免编译时报The content of elements must consist of well-formed character data错误。分页我用的是PageHelper插件。用法极其傻瓜查询前一行PageHelper.startPage(pageNum, pageSize);紧接着调用Mapper查询方法再用new PageInfo(list)拿到分页信息。但注意一个致命坑startPage之后必须紧跟第一个数据库查询中间不能夹杂其他查询操作否则分页会落在错的查询上。3.3 预算校验与余额更新的事务处理预算校验是后端业务逻辑里最值得写的一个方法。完整流程是这样的用户新增一笔支出前端传过来账单DTO里面含accountId、categoryId、amount。后端Service执行四步第一步查询该用户当月该分类的总支出。SQL用DATE_FORMAT(occur_date, %Y-%m)把时间格式化成月份字符串再和#{month}比对。注意这里DATE_FORMAT会导致索引失效但如果账单表对user_id和occur_date建了联合索引MySQL优化器一般会先走索引缩小user_id范围再对少量数据做格式化性能损耗可以接受。第二步查该用户当月该分类的预算金额。如果没设预算直接跳过如果本月支出加上本次新账单金额超过了预算我选择抛出该分类本月预算已超支是否继续的提示让前端用confirm弹窗让用户确认。第三步插入账单记录。第四步更新账户余额。如果是支出账户余额减去amount如果是收入加上amount。这四步全部包在Transactional里。第2步的校验和业务规则有关千万别把它放在前端做前端校验只是体验优化后端校验才是安全底线。3.4 报表统计中的典型SQL写法报表模块最常用的是两个统计按月统计总收入和总支出按分类统计支出分布。这两个都是group by聚合也是MyBatis里最容易写错的地方。按月统计思路是按DATE_FORMAT(occur_date, %Y-%m)分组然后分别SUM收入金额和SUM支出金额。我选择用一条SQL同时求出两类数据SELECT DATE_FORMAT(occur_date, %Y-%m) AS month, SUM(CASE WHEN type 1 THEN amount ELSE 0 END) AS income, SUM(CASE WHEN type 0 THEN amount ELSE 0 END) AS expense FROM t_bill WHERE user_id #{userId} GROUP BY DATE_FORMAT(occur_date, %Y-%m) ORDER BY month DESC按分类统计支出则是把t_bill通过category_id关联t_category取出分类名称再汇总金额SELECT c.name AS categoryName, SUM(b.amount) AS totalAmount FROM t_bill b LEFT JOIN t_category c ON b.category_id c.id WHERE b.user_id #{userId} AND b.type 0 AND DATE_FORMAT(b.occur_date, %Y-%m) #{month} GROUP BY c.name ORDER BY totalAmount DESC关于MySQL的ONLY_FULL_GROUP_BY模式MySQL 5.7以上默认开启要求SELECT后面的非聚合列必须出现在GROUP BY子句里。我这里的写法里c.name直接放在GROUP BY中所以没有触发这个限制。如果你改了SQL又报this is incompatible with sql_modeonly_full_group_by的错误要么把非聚合列加到GROUP BY要么查一下当前sql_mode配置改回去。4. 前端Vue页面与交互实现4.1 前端工程搭建与路由规划前端我用Vue CLI脚手架创建选Vue 2.6。为什么不选Vue 3这个项目既然是配SpringBoot的后台管理系统Vue 2 Element UI这套组合的组件生态更完整教程一抓一大把踩坑成本低。Vue 3当然也好但配合Element Plus和一些老组件库时会有兼容问题对项目受阻的信心是一种打击。安装命令一条龙npm install -g vue/cli vue create finance-web # 选Manually select features勾选Router、Vuex cd finance-web npm install element-ui axios echarts路由规划按页面维度拆分登录页/login、仪表盘/dashboard、账单管理/bill、分类管理/category、预算管理/budget、报表统计/report。每个大路由下再挂子路由。Vue Router我用了懒加载写法让每个页面组件按需加载首屏加载更快const routes [ { path: /, redirect: /dashboard }, { path: /login, component: () import(/views/Login.vue) }, { path: /dashboard, component: () import(/views/Dashboard.vue) }, { path: /bill, component: () import(/views/Bill.vue) } ]路由守卫不能省。我在全局前置守卫里判断如果访问的路径不是/login且本地没有token就redirect到登录页。这虽然是前后端分离项目里的常规做法但每次都要在新的项目里配一遍容易忘记。4.2 账单录入与表单校验账单录入页面是整个前端里交互最复杂的。表单字段账户下拉框、收支类型单选、分类下拉框根据收入/支出动态切换、金额输入框、日期选择器、备注输入框。这里有一个联动细节很常见用户选了支出分类下拉框的数据源就要切换到支出分类选了收入则切换到收入分类。这个动态联动我用Vue的computed实现根据表单里的type值自动从分类列表里过滤出合适的数据不需要手动监听change事件再赋值。表单校验用Element UI的rules。金额字段必填且必须是大于0的数字日期必填且不能是未来日期分类必选。Element UI的rules底层是async-validator常见的校验规则比如{ required: true, message: 请输入金额, trigger: blur }和{ pattern: /^\d(\.\d{1,2})?$/, message: 金额最多两位小数 }直接用就行。这里分享一个表格分页的经验后端分页接口传回来的是PageInfo结构前端表格组件要配pagination区域把current-change事件绑定到页号变化后重新拉取数据的方法。有一个小坑是搜索条件和分页的耦合每次搜索必须把pageNum重置为1不然用户从第3页发起搜索却搜出第3页的结果会一脸懵。4.3 ECharts报表可视化的数据对接报表页面我用ECharts画两个图一个饼图展示本月各类支出占比一个折线图展示最近6个月收支趋势。ECharts的使用套路是固定的npm install echarts页面里import * as echarts from echartsmounted时echarts.init(domId)然后把option塞进去。真正的难点在于把后端返回的数据转换成ECharts需要的结构。比如饼图后端接口返回的是[{categoryName: 餐饮, totalAmount: 1200.5}, ...]而ECharts的series data需要[{name: 餐饮, value: 1200.5}, ...]我一般直接在拿到响应后map一下或者由后端直接构造好前端要的结构。个人项目我推荐后端直接返回前端想要的VO结构少一层转换就少一个出错机会。折线图需要按月返回收入和支出两个序列。后端返回monthList: [2025-01, 2025-02...], incomeList: [...], expenseList: [...]前端设置两条series分别绑定。这里要注意一个echarts细节如果不设置grid属性图表默认会占满整个容器标题和轴标签经常被截断最好显式设置grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true }尤其是有多个图表的页面这个属性能让图表区域自动适配容器。5. 完整源码结构与本机启动全流程5.1 项目目录结构与关键文件说明后端工程结构大概是这样的finance-server ├── pom.xml ├── src/main/java/com/demo/finance │ ├── FinanceApplication.java │ ├── controller │ │ ├── UserController.java │ │ ├── BillController.java │ │ ├── AccountController.java │ │ ├── CategoryController.java │ │ ├── BudgetController.java │ │ └── ReportController.java │ ├── service │ │ └── impl │ ├── mapper │ ├── entity │ ├── dto │ ├── vo │ └── common │ ├── Result.java │ └── GlobalExceptionHandler.java └── src/main/resources ├── application.yml └── mapper ├── UserMapper.xml ├── BillMapper.xml ├── AccountMapper.xml └── ...前端工程结构finance-web ├── package.json ├── vue.config.js ├── src │ ├── main.js │ ├── router/index.js │ ├── store/index.js │ ├── utils/request.js │ ├── api │ │ ├── bill.js │ │ ├── category.js │ │ └── report.js │ └── views │ ├── Login.vue │ ├── Dashboard.vue │ ├── Bill.vue │ ├── Category.vue │ ├── Budget.vue │ └── Report.vue两个目录对照看前后端模块一一对应后端有多少个Controller前端就至少有多少个api模块。这也是我推荐的开发节奏定义完接口文档前后端各拿一份各做各的最后联调。5.2 数据库初始化与后端配置数据库初始化分为两步。第一步用Navicat或者命令行执行建库脚本第二步把后端用到的测试数据也一起导入比如默认的分类数据。application.yml里最核心的数据库配置如下server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/finance?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 你的密码 mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true这里面有两个关键点。第一driver-class-name在不同MySQL版本下不一样MySQL 5.x用com.mysql.jdbc.DriverMySQL 8.x必须用com.mysql.cj.jdbc.Driver。如果你的环境是MySQL 8却配了5.x的驱动启动时会直接报找不到驱动类。第二map-underscore-to-camel-case: true这行很重要。数据库字段是occur_dateJava实体属性是occurDate不开启驼峰映射查出来的数据全是null。这个配置还能配合Mapper里的resultType实现自动映射省去写resultMap的体力活。日期序列化也要注意。MySQL的datetime字段查出来是2025-06-01 12:30:00默认的Jackson序列化结果是数组格式前端根本没法直接用。我在yaml里配置了spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这样接口返回的时间字符串就是正常格式了。前端启动前还需要在vue.config.js里配跨域代理module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }这样做的好处是前端请求/api/bill/list时Vue开发服务器会把它转发到http://localhost:8080/api/bill/list绕开了浏览器跨域限制。生产环境部署再通过Nginx同一域名转发开发和部署密度都很顺。5.3 前端启动与联调验证全部代码就位后启动顺序是这样的先确保MySQL服务是启动状态再启动SpringBoot后端最后启动前端。后端启动看控制台日志出现Started FinanceApplication就说明端口8080被正常占用了。前端运行npm run serve看到App running at: Local: http://localhost:3000就说明编译成功。我第一次联调这个项目时踩过一个很经典的坑前端调后端接口返回401但后端日志显示请求根本没进来。排查半天发现是axios拦截器的问题我封装的request.js里把返回的response.data当成了Result对象但后端的code是200HTTP状态码业务code是20000我没做映射导致前端判断业务code失败直接走错误分支。解决办法其实很简单axios响应拦截器里判断response.data.code 20000就返回数据否则用Message.error提示原因。6. 常见问题与排坑实录6.1 SpringBoot版本相关的坑SpringBoot版本这个问题几乎每个新手都会踩。网上很多教程用的是SpringBoot 2.x但你新建项目时IDEA默认可能给你拉最新版SpringBoot 3.x。SpringBoot 3最低要求JDK 17而你本地装的是JDK 8一启动就报java.lang.UnsupportedClassVersionError或者干脆连Maven编译都过不去。我个人最稳的方案是JDK 8 SpringBoot 2.7.x MyBatis starter 2.3.x。这套组合经过无数项目验证兼容性极好。如果你的SpringBoot版本是3.x那MyBatis的starter要用mybatis-spring-boot-starter3.0以上javax.servlet也要替换成jakarta.servlet这些迁移工作对于新手来说完全是额外负担。6.2 MyBatis与MySQL层面的坑MyBatis的mapper XML扫描不上是我见过频率最高的报错。Invalid bound statement (not found)这个错误至少有四种原因XML文件没有放到配置的mapper-locations路径下XML文件里的namespace写成了别的接口全限定名apper接口方法名和XML里的id对不上XML文件没被Maven打包进去resources目录下的文件被忽略了。排查顺序也是按这个来九成问题出在前两个。还有一个MySQL 8特有的坑SSL连接报错。URL里加useSSLfalse就能解决。另外MySQL 8的时区默认是UTC你的occur_date字段存进去的值查询时可能会和本地时间相差8小时这就是serverTimezoneAsia/Shanghai存在的意义。刚开始没加这个参数我一度以为代码的日期处理逻辑写错了。6.3 Vue与前后端联调层的坑前端最常见的报错是Cannot find module vue-template-compiler。这个是版本不一致导致的vue-template-compiler的版本必须和vue保持一致否则Vue CLI编译模板直接报错。解决方案是npm install vue-template-compiler2.6.14 --save-dev具体版本号你打开package.json看vue的版本。Failed to load tsconfig vue/tsconfig/tsconfig.web.json这种错误多发生在用别人的Vue 3 TypeScript项目模板时原因是项目里没有安装vue/tsconfig这个开发依赖或者版本对不上。解决思路是删除node_modules和package-lock.json重新安装或者直接改用Vue CLI创建的标准Vue 2项目模板避开这些新工具链的坑。跨域问题联调时也经常冒出来。前端请求能发出去浏览器控制台报Access-Control-Allow-Origin错误而后端接口用Postman调是通的。这说明后端没加跨域处理。最省事的方案是后端加一个WebMvcConfigurer配置类里面重写addCorsMappings方法允许所有来源但生产环境我更推荐用Nginx反向代理同源访问根本不存在跨域。6.4 高频问题速查表整理一份问题定位速查表遇到报错先对照现象大概率原因解决方向后端启动失败报Driver类找不到MySQL 8用了旧驱动class换成com.mysql.cj.jdbc.Driver查询结果实体字段全为null没开启驼峰映射加map-underscore-to-camel-case: true时间字段返回数组格式Jackson没有配置日期格式yml配置jackson.date-formatMyBatis报Invalid bound statementXML扫描不到或namespace错误检查mapper-locations、namespace金额出现0.30000000000000004用了double存金额改用decimal(10,2)前端跨域请求失败缺少代理或CORS配置vue.config.js配proxy或后端加CORSCannot find module vue-template-compilervue与vue-template-compiler版本不一致安装相同版本PageHelper分页没生效startPage后插入了其他查询startPage后面紧跟目标查询我自己踩过最值的一条还是金额精度问题。第一版项目图省事金额字段全用double结果月末对账时差了0.1元找了一下午才发现是浮点精度问题。从那以后我给自己定了个规矩凡是涉及金额、数量这类需要精确计算的字段Java用BigDecimal、MySQL用decimal、前端最多保留两位小数展示三层一路收敛。这个系统做完之后你对SpringBoot的自动配置、MyBatis的动态SQL、Vue组件化开发这套链路都会有质的理解。后续要扩展也不难加个定时爬虫同步银行卡流水接个邮件服务做账单周报把报表模块换成大屏展示思路全是现成的。先把眼前这套跑通比什么都实在。