ARTICLE DETAIL

资讯详情

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

Spring Boot+Vue个人理财管理系统:从数据库设计到前后端联调实战

Spring Boot+Vue个人理财管理系统:从数据库设计到前后端联调实战 简介这套个人理财管理系统是一份前后端分离的毕业设计源码基于 Java Springboot Vue 开发并使用 MySQL 作为数据库适合计算机相关专业学生用于课程设计、毕业设计或项目练手。系统提供管理员登录注册、账单添加与导出、统计分析、个人信息修改、消费对比等常用功能覆盖个人理财管理的基本业务闭环。资源包共75个文件包含20个Java后端源码、15个JavaScript脚本、12个Vue前端页面组件、10张界面截图以及pom.xml、application配置、README说明等配套材料压缩包约2.98MB结构清晰便于按模块查阅。预览中的功能说明与目录划分也能帮助读者快速定位代码位置。目前已有109人学习下载可作为从零了解Springboot与Vue整合开发、理解前后端交互流程的参考项目尤其适合需要快速搭建类似管理系统的学生或开发者借鉴。1. 个人理财管理系统一个 Spring Boot Vue 的典型项目到底在解决什么一个基于 Java Spring Boot Vue 的个人理财管理系统源码包拿到手后应该怎么读、怎么跑、怎么改我的建议是别急着跑先想明白它要解决什么问题。记账这件事看起来就是增删改查但真正难的是把账户余额、收支流水、分类汇总、月度预算这几条线串起来任何一个环节对不上账月底对账就成了翻车现场。个人理财管理系统就是把这条链路用 Spring Boot 维护接口、Vue 呈现页面、MySQL 持久化数据最终让你既能手动记账又能按月自动生成报表。这个方向适合两类人一是想学前后端分离的 Java 程序员通过一套完整可运行的项目把开发环境、接口联调、部署链路摸通二是确实不想把记账数据放在第三方 App 里的开发者自己部署一套数据完完全全握在自己手里。下面按我实际做这类项目的经验从数据模型、后端接口、前端页面到联调避坑拆开讲清楚。2. 数据模型与表结构先把收支、预算和账户余额的数据库设计定下来个人理财系统的代码可以写得灵活但数据库表一旦上线再改代价就成倍放大。这类项目最常见的错误是一张流水表打天下结果想查“这个月餐饮花了多少”或者“某张卡余额多少”时不得不写一堆临时逻辑。我一般会把数据拆成五张核心表互相之间有外键关联但又不做过多强制约束保证业务扩展时还能改。2.1 五张核心表用户、账户、交易、分类、预算先看用户表和账户表。个人系统听起来只有一个用户但如果你以后想给家庭成员用用户表就是需要预留的。账户表则对应你实际持有的银行卡、现金、微信零钱甚至是虚拟的“债务账户”。这两张表的建表 SQL 如下CREATE TABLE user ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 用户ID, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE account ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 账户ID, user_id bigint(20) unsigned NOT NULL COMMENT 所属用户ID, name varchar(50) NOT NULL COMMENT 账户名称例如招商银行借记卡, balance decimal(14,2) NOT NULL DEFAULT 0.00 COMMENT 当前余额, currency varchar(10) NOT NULL DEFAULT CNY COMMENT 币种, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT账户表;账户表里的balance不是冗余字段而是冗余结果。它的值来自交易流水累计但保留一个快照字段可以加快页面展示也方便手动调整期初余额。这里你要记住一个原则余额字段只允许在事务里更新不允许在业务代码里直接 set。否则“账户余额”就成了黑匣子账对不上时没人知道源头。交易表是整张系统的核心字段设计直接决定统计查询好不好写。CREATE TABLE transaction ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 流水ID, account_id bigint(20) unsigned NOT NULL COMMENT 账户ID, category_id bigint(20) unsigned NOT NULL COMMENT 分类ID, type tinyint(1) NOT NULL COMMENT 类型1收入2支出, amount decimal(14,2) NOT NULL COMMENT 金额, transaction_time datetime NOT NULL COMMENT 交易发生时间, note varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_account_time (account_id, transaction_time), KEY idx_category_time (category_id, transaction_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT交易流水表;注意我把transaction_time和create_time分开了。交易发生时间可能是用户补录的昨天甚至上周而create_time才是数据录入时间。很多新手只用当前时间做业务时间导致补录一笔昨天的消费时当天统计还是准的月底对账就乱了。amount一律使用decimal(14,2)而不是float或double这是我在项目里吃过亏后养成的习惯。MySQL 的浮点数做求和时会累积误差等你在月底报表里看到一个莫名其妙的多出 0.01 元就已经晚了。分类表用自关联结构因为餐饮、交通、购物这些一级分类下面还会细分。CREATE TABLE category ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 分类ID, parent_id bigint(20) unsigned NOT NULL DEFAULT 0 COMMENT 父分类ID0为顶级, name varchar(50) NOT NULL COMMENT 分类名称, type tinyint(1) NOT NULL COMMENT 类型1收入2支出, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_parent_id (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT分类表;预算表则需要按月份和分类做唯一约束避免同一个月里同一分类出现两条预算记录。CREATE TABLE budget ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 预算ID, user_id bigint(20) unsigned NOT NULL COMMENT 用户ID, category_id bigint(20) unsigned NOT NULL COMMENT 分类ID, month varchar(6) NOT NULL COMMENT 月份例如202503, amount decimal(14,2) NOT NULL COMMENT 预算金额, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_user_category_month (user_id, category_id, month) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预算表;这五张表已经覆盖记账流程的完整闭环用户建账户账户记流水流水挂在分类上预算按月限制分类支出。实际开发中我还会加一张transfer_record来做账户间转账但那是进阶功能第一版先不做。把转账记成一笔支出和一笔收入会导致总收入虚高这是很多个人记账工具的常见坑。2.2 DECIMAL 与 DATETIME 字段的选型边界数据库字段选型是最不该偷懒的环节。金额必须用decimal(14,2)14 位总长度里整数部分最多 12 位这对个人理财系统已经足够。如果你幻想某天余额突破万亿那就把长度加到 16。datetime要不要带时区是个容易忽略的点。MySQL 的datetime不带时区信息写入什么就存什么适合个人单机使用如果系统部署到云上且有多地区访问建议改用timestamp并统一转换为 UTC 存储。不过个人理财系统里我仍选择datetime原因是手动记账时用户期望看到的就是本地时间减少一层时区换算带来的困惑。还有一个隐蔽的坑是utf8mb4与排序规则。utf8mb4_general_ci和utf8mb4_unicode_ci在中文环境下差别不大但如果你需要按备注做模糊查询统一排序规则可以避免索引失效的假象。表结构里的varchar字段建议都带上长度MySQL 的VARCHAR(255)走的是内存临时表路径VARCHAR(5000)则可能落到磁盘临时表性能差别很大。备注字段我给 255够用且安全。2.3 用 MyBatis-Plus 自动生成建表语句的注意点日常项目里我不推荐手工维护所有建表脚本但也不推荐完全依赖自动生成。用 MyBatis-Plus 的实体类注解加CodeGenerator工具生成表结构是常见做法它能把 Java 实体类转成建表 SQL省掉大量重复的CREATE TABLE。不过这个方案有个边界MyBatis-Plus 默认字段类型映射对BigDecimal生成的是decimal(19,2)而不是我们自己约定的decimal(14,2)对LocalDateTime生成的是datetime这没问题。所以自动生成的脚本一定要人工过一遍特别是金额、索引和唯一约束这三类自动工具不会替你考虑业务含义。如果你要保留手工管理 SQL 的习惯建议把所有建表语句写成一个schema.sql放在项目根目录的doc下并带上版本注释。像这样-- schema_v1.0.sql -- 2025-03-01创建用户、账户、交易、分类、预算五张表 -- schema_v1.1.sql -- 2025-03-10交易表新增第三方支付流水号字段 transaction_no这种版本管理方式比让数据库自动同步可靠得多因为你可以随时回看历史变更。数据库的“后悔药”不是哪天写错了点一下恢复而是从一开始就有清晰的版本记录。3. Spring Boot 后端落地从接口分层到金额事务的依赖与配置表结构定好以后后端的骨架就出来了。Spring Boot 项目最怕的是结构混乱Controller 里写 SQL、Service 里塞页面逻辑这种代码刚开始能跑加两个功能就变成泥潭。我一般把项目分成controller、service、mapper、entity、common五个包每层职责固定这样新手接手时也知道去哪找代码。3.1 项目分层与 POM 里最常被忽略的三个依赖pom.xml是后端的“购物清单”。一个常见做法是用 Spring Initializr 生成基础项目再往里加依赖。很多教程只让你加spring-boot-starter-web和mybatis-plus-boot-starter但实际开发中还有三个依赖作用关键却不显眼dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependencymysql-connector-j是数据库驱动没有它 Spring Boot 连不上 MySQL。注意com.mysql.cj.jdbc.Driver姓名的变化旧版是com.mysql.jdbc.Driver在新版 MySQL 8.x 下已经没有这个类了。JWT 依赖则用来做登录状态管理。个人系统不需要复杂的权限框架用 JWT 生成一个令牌丢给前端每次请求带在 Authorization 头里就够了。jjwt有两部分api是编译期需要的impl是运行时需要的两者缺一不可漏掉impl启动就会报错。application.yml里的配置也要跟进server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/finance?characterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: root jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里serverTimezoneAsia/Shanghai是 MySQL 8.0 的必填参数不加会直接报时区错误。map-underscore-to-camel-case让数据库transaction_time自动映射到 Java 字段transactionTime省去写一堆TableField。逻辑删除配置对应实体类里的deleted字段这样删除交易记录只是标记不会真删对账时还能查回来。实体类也会加上 MyBatis-Plus 的注解Data TableName(transaction) public class Transaction { TableId(type IdType.AUTO) private Long id; private Long accountId; private Long categoryId; private Integer type; private BigDecimal amount; private LocalDateTime transactionTime; private String note; private LocalDateTime createTime; }实体类字段命名全部用驼峰配合刚才的映射配置数据库下划线字段自动对齐不需要为每一列写TableField。如果某一天你需要根据 Java 实体类生成建表 SQL让 MyBatis-Plus 的自动生成器跑一遍也可以但最终仍要以手工核对的schema.sql为准。3.2 注册登录接口与 JWT 鉴权的完整实现登录接口是我第一个写的因为后续所有记账接口都依赖当前用户身份。密码存储用 BCrypt不要用 MD5理由不需要我赘述。一个最简注册逻辑如下Service public class UserService { Autowired private UserMapper userMapper; Autowired private PasswordEncoder passwordEncoder; public User register(String username, String rawPassword) { if (userMapper.selectByUsername(username) ! null) { throw new BusinessException(用户名已存在); } User user new User(); user.setUsername(username); user.setPassword(passwordEncoder.encode(rawPassword)); userMapper.insert(user); return user; } }PasswordEncoder来自 Spring Security但项目并不一定需要完整引入 Security。很多初学者不理解这一点只引入spring-security-crypto也可以使用 BCrypt。省掉整个 Security 配置可以避开它默认登录页和过滤链路带来的干扰。登录成功后生成 JWT 令牌核心代码如下public String login(String username, String rawPassword) { User user userMapper.selectByUsername(username); if (user null || !passwordEncoder.matches(rawPassword, user.getPassword())) { throw new BusinessException(用户名或密码错误); } String token Jwts.builder() .setSubject(user.getId().toString()) .claim(username, user.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 24 * 3600 * 1000)) .signWith(SignatureAlgorithm.HS256, your-secret-key) .compact(); return token; }JWT 的subject放用户 IDclaim放非敏感信息过期时间设为 24 小时。签名密钥至少 32 字节不要写在代码里用配置项jwt.secret从application.yml读取。前端拿到这个 token 后每次请求都要把它放到 HTTP Header 的Authorization字段里格式为Bearer token。后端加一个拦截器统一解析 token而不是在每个 Controller 里重复校验。拦截器代码很短但很多人漏了HandlerInterceptor的注册Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String authHeader request.getHeader(Authorization); if (authHeader null || !authHeader.startsWith(Bearer )) { response.setStatus(401); return false; } String token authHeader.substring(7); try { Claims claims Jwts.parser().setSigningKey(your-secret-key).parseClaimsJws(token).getBody(); request.setAttribute(userId, Long.parseLong(claims.getSubject())); return true; } catch (Exception e) { response.setStatus(401); return false; } } }preHandle里把userId放进 request 属性Controller 里就能用RequestAttribute(userId) Long userId拿到当前登录用户避免每张表都存一份用户名副本。拦截器拿到 token 后只做签名和过期时间校验具体用户是否被禁用在个人系统里暂时不处理。3.3 记账接口的事务边界一笔记流水同时更新账户余额记账接口是这个系统的核心事务事务边界必须同时覆盖“插入交易流水”和“更新账户余额”两步。如果只插入流水余额不变页面上的账户余额就成了假数据。如果第二次更新余额失败流水已经写入月底又对不上账。用Transactional可以解决但要注意 rollback 的默认行为只回滚RuntimeException检查异常不会触发回滚。所以在业务代码里抛出运行时业务异常最安全。记账 Service 的代码结构大致如下Service public class TransactionService { Autowired private TransactionMapper transactionMapper; Autowired private AccountMapper accountMapper; Transactional(rollbackFor Exception.class) public void addTransaction(TransactionRequest request, Long userId) { Account account accountMapper.selectById(request.getAccountId()); if (account null || !account.getUserId().equals(userId)) { throw new BusinessException(账户不存在或属于其他用户); } Transaction transaction new Transaction(); transaction.setAccountId(request.getAccountId()); transaction.setCategoryId(request.getCategoryId()); transaction.setType(request.getType()); transaction.setAmount(request.getAmount()); transaction.setTransactionTime(request.getTransactionTime()); transactionMapper.insert(transaction); BigDecimal newBalance account.getBalance(); if (request.getType() 1) { newBalance newBalance.add(request.getAmount()); } else { newBalance newBalance.subtract(request.getAmount()); } account.setBalance(newBalance); accountMapper.updateById(account); } }Transactional(rollbackFor Exception.class)是这里的关键参数。默认 Spring 事务只回滚未检查异常如果不写rollbackFor某些业务异常会导致流水插入成功、余额更新失败。另一个容易被忽略的点是并发更新。如果用户在同一秒内提交了两笔支出两个请求都先查到相同余额然后各自减一笔最后只会保留最后一次更新的结果金额就平白无故少了。解决方式是在account表加一个version乐观锁字段更新时带上version条件UPDATE account SET balance #{newBalance}, version version 1 WHERE id #{id} AND version #{oldVersion}这会让后到的请求更新失败弹给用户一个“操作过于频繁”的错误。个人系统一般不需要悲观锁乐观锁成本低且直观。4. Vue 前端从初始化到页面路由、Axios 拦截器与记账表单的组件化后端接口写完开始搭 Vue 前端。我推荐 Vue 3 Vite因为 Vite 的冷启动速度比 Vue CLI 快很多开发体验好而且它构建出来的静态文件可以直接往后端放部署特别方便。前端项目的几个组成部分是路由、HTTP 封装、页面组件这三块理顺了剩下的都是照着设计稿填充内容。4.1 用 Vite 初始化 Vue3 项目并规划目录执行初始化命令的方式有多种最简单的是使用 npmnpm create vitelatest finance-web -- --template vue cd finance-web npm install npm run devfinance-web是项目名模板选vue代表只安装 Vue 核心库路由和状态管理需要单独装。初始化完成后我会把src目录整理成api、router、views、components、utils五个子目录。很多人喜欢把请求函数直接写在页面里页面一多就很难复用特别是接口前缀修改的时候要全局改一遍。安装路由插件和 HTTP 工具npm install vue-router4 axiosvue-router4对应 Vue 3不能装旧版。axios用来发请求当然你也可以用浏览器自带的fetch但 axios 在拦截器和错误处理方面更顺手团队项目里也更容易统一。4.2 配置 vue-router 与 Axios 拦截器的参数细节路由配置的核心是让登录页与主页面分离。未登录用户访问记账页面时应当跳转到登录页。一个典型配置如下import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /login, component: () import(../views/LoginView.vue) }, { path: /, component: () import(../views/LayoutView.vue), redirect: /dashboard, children: [ { path: dashboard, component: () import(../views/DashboardView.vue) }, { path: transactions, component: () import(../views/TransactionListView.vue) }, ] } ] }) router.beforeEach((to) { const token localStorage.getItem(token) if (!token to.path ! /login) { return /login } }) export default routerbeforeEach是路由守卫在这里做登录检查最干净。注意我用了localStorage保存 token而不是更安全的sessionStorage是因为刷新页面后也要保留登录状态。当然token 放在localStorage有 XSS 风险个人项目里如果页面不被注入脚本风险可控。Axios 拦截器负责给每个请求带上 token并对统一错误码做处理import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response?.status 401) { localStorage.removeItem(token) window.location.href /login } else { ElMessage.error(error.response?.data?.message || 请求失败) } return Promise.reject(error) } ) export default requestbaseURL配成/api开发环境用 Vite 的代理转发到后端生产环境则让 Nginx 把/api转发到 Java 服务避免前端出现跨域问题。timeout设 10 秒如果后端查询报表太慢前端有明确报错而不是一直转圈。拦截器里 401 的处理是必须的否则 token 过期后每跳转一个页面都会弹一次错误。4.3 把记账页面拆成表单组件和分类选择组件记账页面的核心是一个表单选择账户、选择分类、输入金额、选择时间、写备注。我会拆成两个组件一个叫TransactionForm.vue一个叫CategorySelect.vue。这么做是为了复用比如转账页面也要选择分类预算设置页也要展示分类列表。表单组件示例template el-form refformRef :modelform :rulesrules label-width80px el-form-item label账户 el-select v-modelform.accountId placeholder请选择账户 el-option v-foritem in accounts :keyitem.id :labelitem.name :valueitem.id / /el-select /el-form-item el-form-item label分类 category-select v-modelform.categoryId :typeform.type / /el-form-item el-form-item label金额 el-input-number v-modelform.amount :min0.01 :precision2 / /el-form-item el-form-item label时间 el-date-picker v-modelform.transactionTime typedatetime / /el-form-item el-form-item label备注 el-input v-modelform.note typetextarea maxlength255 / /el-form-item el-button typeprimary clicksubmitForm保存/el-button /el-form /template script setup import { reactive } from vue import request from ../utils/request /script组件里直接调用request.post(/api/transactions, form)把数据提交到后端。注意el-input-number的precision设置为 2避免用户输入三位小数导致金额精度出错。分类选择组件通过props.type决定展示收入分类还是支出分类内部加载分类列表并渲染成树形下拉。这样记账逻辑和后端接口耦合度极低只要接口不变前端组件怎么换都不影响。5. 前后端联调和数据库初始化4 个让我返工过的坑联调阶段是整个开发过程里最浪费时间的部分很多问题不是代码逻辑错了而是环境配置没对上。我在几个 Spring Boot Vue 项目里都遇到过下面这些坑每一条都是真实返工过的血泪经验。明白了它们你可以少走差不多一天弯路。5.1 跨域问题开发环境接口被浏览器拦截现象前端页面跑在http://localhost:5173后端接口跑在http://localhost:8080打开页面后所有请求都报Access-Control-Allow-Origin错误网络请求里能看到请求发出去了但响应被浏览器拦住。原因开发环境下前端和后端服务端口不同浏览器基于同源策略拦截了跨域请求。解决最直接的办法是修改后端支持跨域加一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); } }allowedOrigins只允许开发地址不要用*加allowCredentials(true)否则浏览器依然会拒绝而且还有安全风险。另一种更优雅的方式是配置 Vite 代理在vite.config.js里把/api转发到http://localhost:8080这样前端请求的是同源地址后端不需要任何跨域配置。我用开发环境代理生产环境用 Nginx后端跨域配置只属于临时补救。5.2 数据库连接失败时区、编码和驱动类三个隐藏开关现象启动 Spring Boot 后控制台报Unknown initial character set index 255或The server time zone value Öйú±ê׼ʱ¼ä is unrecognized数据库客户端连不上。原因MySQL 8 默认时区与 JDBC 驱动时区不一致同时连接串里没有指定characterEncoding。解决修改application.yml里的 JDBC URL加上serverTimezoneAsia/Shanghai和characterEncodingutf8mb4并且确保driver-class-name是com.mysql.cj.jdbc.Driver。注意utf8mb4要写在characterEncoding里而不是拼在 URL 问号后面。还有一个常识MySQL 数据库本身的表结构也要和连接编码一致否则中文写入以后查询出来是乱码。检查手段是执行SHOW VARIABLES LIKE character_set_server如果不是utf8mb4需要改数据库默认配置。5.3 金额计算出现 0.9999JSON 里的 BigDecimal 被丢了精度现象前端显示一笔 100.5 元的支出后端接口返回的却是100.500000000001或者某个月支出合计比实际多了 0.01 元。原因有些数据库字段用了double或者后端将BigDecimal序列化时没有指定小数位。解决数据库表字段必须用decimalJava 实体类必须用BigDecimal并且在 Jackson 配置里设置序列化精度spring: jackson: generator: write-bigdecimal-as-plain: true如果还需要保留两位小数可以在金额字段上加JsonFormat(shape JsonFormat.Shape.STRING)强制转成字符串返回。这样可以避免前端 JavaScript 浮点运算时把BigDecimal转成不精确数字。5.4 接口返回的字段全是 nullMyBatis 驼峰映射没有打开现象数据库里transaction_time明明有值接口返回的 JSON 里transactionTime却是null但id、amount等字段正常。原因MyBatis 默认没有开启驼峰映射transaction_time无法自动映射到transactionTime。解决在application.yml里设置mybatis-plus.configuration.map-underscore-to-camel-case: true或者在类字段上手动加TableField(transaction_time)。这个坑特别隐蔽因为很多教程项目里碰巧没有下划线字段新手第一次做实际系统时就会踩到。排查方法很简单在 Service 层用System.out.println打印实体对象看看下划线字段对应属性是不是空如果是空先查映射配置而不是查 SQL。6. 进阶用 SQL 聚合生成月度报表并把前端包直接放进 Spring Boot系统跑通以后我建议你优先做两个进阶动作一是用 SQL 聚合生成月度收支报表二是把 Vue 打包产物放进 Spring Boot 的static目录做成一个真正能一键启动的独立应用。这两个动作也是面试里常被追问的细节。月度报表我一般不用 Java 做循环而是直接写 SQL 聚合。在TransactionMapper里加一个自定义查询方法select idsumByMonth resultTypemap SELECT DATE_FORMAT(transaction_time, %Y-%m) AS month, type, SUM(amount) AS total_amount FROM transaction WHERE account_id IN (SELECT id FROM account WHERE user_id #{userId}) GROUP BY month, type ORDER BY month DESC /selectDATE_FORMAT把时间字段格式化成年月字符串GROUP BY month, type让收入和支出分别汇总。返回的ListMapString, Object可以直接给前端做双柱状图。这里要注意SQL 里不能用type作为别名因为它是 MySQL 保留字别名用month也会引发类似问题我用month_key更稳妥。报表接口适合加一个 Redis 缓存个人系统用户量不大但月度报表计算涉及全表扫描合理缓存能让页面秒开。部署方式上我不建议把前后端部署成两个独立服务而是把 Vue 打包后的静态文件放进 Spring Boot 的src/main/resources/static目录。先用npm run build生成dist目录然后把dist里的文件复制到static下再用 Maven 打包整个 Spring Boot 项目最终只有一个可执行 jar。访问时Spring Boot 会自动把静态文件当作默认页面接口和页面同源没有任何跨域问题。这个部署方式特别适合个人项目和给领导演示的场景一条java -jar finance.jar就搞定。需要注意的坑是如果前端用了 history 模式路由直接刷新子页面会 404解决方案是写一个 Controller 把非/api路径转发到index.html。我自己的习惯是先用 SQL 方式把报表算出来再用后端接口封装一层最后验证前端图表数据时会故意改一笔交易的时间跨月检查月份汇总是否跟着变。不这样做等上线后才发现统计口径有误就只能深夜对着数据库掏后悔药了。希望这些思路能帮你在做个人理财管理系统时少走几个坑把时间省下来去优化真正有意思的记账体验。本文还有配套的精品资源点击获取
返回列表