ARTICLE DETAIL

资讯详情

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

Spring Boot+Vue个人理财系统全栈开发实战:从建表到联调避坑

Spring Boot+Vue个人理财系统全栈开发实战:从建表到联调避坑 简介一套基于Java Springboot与Vue的前后端分离个人理财管理系统主要面向毕业设计选题及SpringbootVue全栈学习者。系统实现了管理员端的登录注册、账单添加与导出、统计分析、个人信息修改、消费对比等核心功能覆盖日常收支记录、分类管理与趋势分析场景适合作为课程设计或毕业设计的参考原型。压缩包内含75个文件其中20个Java文件承载后端服务与业务逻辑15个JavaScript和12个Vue文件构建前端交互界面配合数据库脚本、XML配置、说明文档等能够完整支撑项目运行另含部分图片静态资源总大小约2.98MB目录结构清晰便于直接导入开发工具运行与二次开发。已有109人学习浏览适合需要快速搭建完整项目或撰写毕业设计文档的开发者参考。资源附带数据库文件与说明材料可帮助理解前后端分离架构、接口设计及数据表关系是一套值得参考的课程设计与毕业设计案例。1. 个人理财管理系统JavaSpringbootVue 这套组合到底值不值得上手拿到“个人理财管理系统(源码数据库).zip”这个标题第一反应是这不就是毕设/课设里最常见的全家桶吗但真把它当成一个可复用的工程去拆里面能挖的东西远比“增删改查”多。Java 做后端服务、Spring Boot 负责接口自动装配与事务、Vue 做前端交互、MySQL 存账目明细这条链路覆盖了从数据库建模到前后端联调的全流程。对想练手 Spring Boot Vue 全栈、或者需要快速产出可演示系统的人来说这套源码数据库的包是性价比很高的起步材料。本文不评价某个具体压缩包内容而是把这个标题背后的技术栈拆成可直接复现的方案怎么建表、怎么写接口、怎么让 Vue 页面和 Spring Boot 对得上、以及最容易翻车的几个坑。2. 从数据库设计开始个人理财的账目模型比你想的复杂个人理财系统的核心不是页面而是数据库表结构。很多人拿到“数据库”就直接 navicat 导入跑起来但真正要自己理解并二次开发必须先把账目模型理清楚。这类系统最常见的表是用户表、账目类型表收入/支出分类、账单流水表、预算表再加上可选的资产账户表。流水表是核心所有统计报表都从它聚合出来。建表时我习惯用 utf8mb4 字符集排序规则用 utf8mb4_general_ci原因很简单账目描述里可能出现 emoji老 utf8 字符集存不了。账单流水表至少要包含这些字段流水ID、用户ID、类型ID、金额、交易时间、备注、创建时间。金额字段用 decimal(10,2) 而不是 float这是血泪经验——浮点数在合计统计时会出误差比如 0.10.2 变成 0.30000000000000004做报表对账时直接翻车。类型表用树形结构比如父分类“餐饮”子分类“早餐”“午餐”“外卖”用 parent_id 自关联即可不用搞复杂的嵌套集模型。预算表的设计容易被忽略但它是理财系统的灵魂。字段可以设计成用户ID、类型ID、预算金额、统计周期月/周、生效日期。做统计时按“当月1号到当月最后一天”过滤流水再按类型分组求和就能算出每个分类的预算使用率。这里有个坑很多人把预算表和流水表直接关联用流水的时间字段去匹配预算周期结果跨月账目全乱。正确做法是预算表单独存周期起始日统计时用流水时间 JOIN 预算的周期区间而不是依赖数据库的 MONTH() 函数隐式匹配。具体建表 SQL 如下表结构是精简版但足够跑通核心流程CREATE DATABASE IF NOT EXISTS finance DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE finance; CREATE TABLE t_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, nickname VARCHAR(50) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_category ( category_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, parent_id INT DEFAULT 0, category_name VARCHAR(50) NOT NULL, category_type TINYINT NOT NULL COMMENT 1收入 2支出, sort_order INT DEFAULT 0, KEY idx_user (user_id) ); CREATE TABLE t_bill ( bill_id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, category_id INT NOT NULL, amount DECIMAL(10, 2) NOT NULL, bill_date DATE NOT NULL, remark VARCHAR(200) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_date (user_id, bill_date), KEY idx_category (category_id) ); CREATE TABLE t_budget ( budget_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, category_id INT NOT NULL, budget_amount DECIMAL(10, 2) NOT NULL, period_start DATE NOT NULL, period_end DATE NOT NULL );USER 表用自增主键密码字段预留 255 长度因为 Spring Security 的 BCrypt 加密后是 60 位加上可能的盐前缀255 是安全余量。BILL 表有两个联合索引idx_user_date 是查询“某个用户某段时间所有流水”的必经之路没这个索引数据量到十万级后页面会卡到怀疑人生。idx_category 用于按分类聚合统计报表页的 GROUP BY 语句靠它加速。period_start 和 period_end 直接把预算周期写成闭区间查询时用 bill_date BETWEEN 判断简单直接不用在代码里算“月初月末”再传参。3. Spring Boot 后端分层结构、鉴权与金额计算的核心逻辑后端部分是 Spring Boot 2.x MyBatis-Plus 的组合。为什么不用 JPA个人理财系统的查询以明细列表、分组统计为主MyBatis-Plus 的 LambdaQueryWrapper 写条件查询比 JPA 直观太多而且分页插件 PaginationInnerInterceptor 一行配置搞定不需要自己写 Pageable 的转换逻辑。工程结构按 controller → service → mapper 三层拆DTO 和 VO 分开DTO 是前端传来的参数对象VO 是返回给前端的展示对象。很多人把 Entity 直接返给前端结果金额、时间格式全裸奔懒是懒了后续改一个字段要连带改好几处。启动类加上 MapperScan否则 Mapper 接口扫不到。关键依赖就三个spring-boot-starter-web、mybatis-plus-boot-starter、mysql-connector-java。JWT 鉴权用 jjwt 库 0.9.1拦截器里解析 token 拿到 userId存入 ThreadLocal 供 Service 层使用。这里有个细节拦截器放行登录接口和静态资源其余全部校验 token否则前端页面一刷新就直接 401。流水新增接口是核心逻辑上不能只做 insert。我先查分类是否存在且属于当前用户再校验金额大于 0然后写入 t_bill同时更新对应预算表的已用金额如果预算表有冗余字段。这里为了演示简单预算统计用实时聚合而非冗余字段。但生产级做法是加一个 t_budget_usage 表或者冗余 used_amount 字段否则每次打开首页都 GROUP BY 请求一次数据库压力大。个人系统无所谓但养成好习惯没坏处。Service public class BillServiceImpl implements BillService { Override public PageResultBillVO pageList(BillQueryDTO dto) { LambdaQueryWrapperBill wrapper new LambdaQueryWrapper(); wrapper.eq(Bill::getUserId, dto.getUserId()); wrapper.between(dto.getStartDate() ! null dto.getEndDate() ! null, Bill::getBillDate, dto.getStartDate(), dto.getEndDate()); wrapper.eq(dto.getCategoryId() ! null, Bill::getCategoryId, dto.getCategoryId()); wrapper.orderByDesc(Bill::getBillDate); PageBill page new Page(dto.getPageNum(), dto.getPageSize()); billMapper.selectPage(page, wrapper); return PageResult.of(page, bill - toVO(bill)); } Override public boolean addBill(BillDTO dto) { Bill bill new Bill(); BeanUtils.copyProperties(dto, bill); bill.setUserId(dto.getUserId()); return billMapper.insert(bill) 0; } }这段代码前端传 userId 进来我没从 token 里取。实际项目中这是危险的恶意用户可以传别人的 userId 插入数据。正确做法是从拦截器存入的 ThreadLocal 或 RequestContext 里拿当前登录用户DTO 里的 userId 直接忽略。上面代码是为了演示分页和条件查询真实改造一行就行bill.setUserId(UserContext.getUserId())。关于金额计算Service 层统计收入支出总计时我一般写一个专门的统计方法而不是在 Java 里把查询结果循环相加。SQL 的 SUM 聚合交给数据库代码只处理展示格式化。用 Stream 相加也不是不行但分组多、数据量大时效率差。报表接口返回的是一个 MapString, Object包含总收入、总支出、结余、分类占比列表。日期范围查询用 between 条件这里注意 MyBatis-Plus 的 between 是闭区间等价于 SQL 的 AND 不会漏掉边界日期。4. Vue 前端页面登录、记账、报表三个页面把整个系统串起来Vue 部分我用 Vue 3 Vite Element Plus Axios比 Vue 2 webpack 配置轻很多。现代 Vue 项目脚手架生成的目录里src/views 放页面、src/api 放接口封装、src/router 放路由表。个人理财系统的页面不需要很多登录注册页、首页仪表盘当月收支概览、账单明细页列表筛选新增、报表分析页趋势图分类饼图、预算设置页。五个页面足够覆盖“管理系统”的全部演示场景再多就是凑数了。前端最关键的是 Axios 实例封装不封装好每个页面重复写 token 前缀和错误处理代码直接烂掉。我一般创建 src/utils/request.jsbaseURL 设为 /api开发环境走 Vite 代理到 localhost:8080请求拦截器加 Authorization 头响应拦截器统一处理 401 跳登录页和业务错误码提示。路由守卫里判断 token 是否存在不存在重定向到 /login。Element Plus 的 Message 组件做提示比原生 alert 好看且不阻塞交互。记账表单里“金额”和“分类”两个字段的校验有讲究。金额用 InputNumber 组件precision 设为 2确保用户输入 3.14159 时自动变成 3.14分类用 Cascader 组件因为类型表是两级树形结构。这里有个容易踩的坑Cascader 的 value 和 label 字段默认是 value/label而后端返回的是 categoryId/categoryName不配置 props 映射直接显示成空。必须在模板里写明 :props{ value: categoryId, label: categoryName, children: children }。首页仪表盘用 ECharts 画饼图和折线图。引入方式我用按需注册不搞全量引入否则打包体积直奔 1MB 以上。vue-echarts 封装组件或者直接用原生 echarts 都行我建议用原生 echarts ref 容器避免多一层封装在组件销毁时还要手动 dispose。折线图展示近 30 天每日支出趋势后端返回 [{date: 2024-01-01, amount: 125.30}] 这种结构前端 map 成 ECharts 需要的 xAxis data 和 series data不用在接口层做适配。记账页面保存成功后有个细节不要整页刷新把用户表单清空。我用 ElMessage.success 提示“保存成功”然后调用父组件的 loadList() 重新拉列表表单组件内部 this.form {} 重置但保留分类下拉的选中状态。这个交互细节对演示系统的体验提升非常明显用户连续记多笔账时不用重复选分类。Vue 3 的组合式 API 写记账逻辑如下const form reactive({ categoryId: null, amount: 0, billDate: new Date(), remark: }) function handleSave() { formRef.value.validate((valid) { if (!valid) return addBill(form).then(res { ElMessage.success(账单已记录) resetForm() emit(refresh) }).catch(() { ElMessage.error(保存失败请检查网络) }) }) }这里 reactive 的对象在 resetForm 时不能用 Object.assign 直接清空因为 reactive 的 getter 被代理过重新赋值会丢失响应式绑定。正确做法是循环 Object.keys 遍历赋值或者用 reactive 定义后整体 this.$reset 也行。另一个坑Element Plus 的 DatePicker 组件 v-model 绑定的是 Date 对象或字符串接口提交时如果传 Date 对象axios 序列化会变成 ISO 长字符串后端 LocalDate 解析直接 400。我在 handleSave 里手动格式化 billDate 为 yyyy-MM-dd 再提交后端用 DateTimeFormat(pattern yyyy-MM-dd) 接收。Vue 打包部署时vite.config.js 里配置 proxy 开发代理。打包后 dist 目录可以直接让 Spring Boot 当静态资源托管——把 dist 里的 index.html 和 assets 文件夹复制到 Spring Boot 的 src/main/resources/static 下访问 localhost:8080 就能看到页面不用再开一个 Nginx。这种方式对个人项目来说最省事前后端同端口没有跨域问题。唯一注意的是 history 路由模式刷新会 404需要后端 controller 把非 /api 的路径都转发到 index.html。嫌麻烦的可以直接用 hash 模式代价是 url 里多一个 # 号但刷新永远不出问题。5. Spring Boot 与 Vue 联调注意七个必查点和 3 个高频报错症状前后端联调是个人理财系统最容易拖时间的阶段。表面看是接口调不通实际上百分之八十的问题集中在跨域、日期格式、字段命名、token 传递这四个地方。先说跨域开发时 Vite 默认跑在 5173 端口Spring Boot 跑在 8080直接 axios 请求必定跨域。我在 Vite 配置里开了代理把 /api 前缀转发到后端这样前端代码里写相对路径 /api/bill/list浏览器看到的是同源请求后端不用加 CORS 配置。但很多模板直接在后端加 CrossOrigin 或者 WebMvcConfigurer 配置 CORS 全局放行这也能跑缺陷是任何源都能请求安全上裸奔。个人系统无所谓但正常开发还是用代理方案更标准。第二个必查点是日期格式后端返回 LocalDate 默认序列化成 2024-01-01T00:00:00前端要展示成 2024-01-01 必须加 JsonFormat(pattern yyyy-MM-dd)。或者在后端配置一个 Jackson 的 ObjectMapper 全局格式化一劳永逸。字段命名是老生常谈后端变量名 userId 前端传 user_id两个大写字母转换直接变 undefined。我用 Spring Boot 的 application.yml 里 spring.jackson.property-naming-strategy 保持默认 camelCase前端严格按 userId 传参不用下划线。数据库字段用下划线MyBatis-Plus 自带 map-underscore-to-camel-case 自动映射不用手动写 resultMap只要实体类字段是驼峰就行。第三个必查点是分页数据格式MyBatis-Plus 的 Page 对象序列化后是 { records: [], total: 100, size: 10, current: 1 }前端分页组件要用的正是 records 和 total但如果有人改了全局 Jackson 配置把 Page 的 getters 过滤掉records 会丢。建议后端统一封装 PageResult 返回别直接用 MyBatis-Plus 的 Page 对象露给前端。联调时最常见的报错是 401。现象是登录成功后跳转首页刷新一次页面就跳回登录页。原因是刷新后 token 没了或者 axios 没带 Authorization 头。检查前端 localStorage 里 token 是否在登录接口成功后 setItem检查 request.js 请求拦截器是否从 localStorage 取出 token 并放入 headers。这两个地方任何一个漏了都是同样的 401 表现。第二个高频报错是 500 Internal Server Error现象是新增账单提交后直接红屏控制台显示 “Request failed with status code 500”。打开后端日志大概率是字段映射失败比如前端传了 categoryIdnullMyBatis-Plus 的 insert 语句生成了一堆 NULL 值数据库 category_id 非空约束直接抛异常。解决办法是在新增前做一次分类是否存在校验或者数据库字段设置默认值。第三个高频报错是“时间字段时区偏移”。数据库存的是 UTC 时间页面显示中午 12:00 却变成 20:00差了八个钟。根源是数据库连接串里 serverTimezoneUTC改成 serverTimezoneAsia/Shanghai 即可。这个坑在新手项目里出现率奇高因为默认的 MySQL 连接驱动会把服务器时区带进去而服务器往往配的是 UTC。为了把这几个点一次说透我把联调排查顺序整理成固定流程。先后端启动成功看日志有没有报错然后浏览器 F12 打开 Network 面板看 /api 开头的请求状态码接着看请求头里有没有 Authorization再看响应体的 JSON 结构是否符合前端预期。这条链路每步二十秒能定位八成问题。这是个人系统的联调习惯也是团队协作里百试百爽的排错路径。下面这个表格是联调阶段我固定检查的几个点基本覆盖 90% 的异常来源检查项检查方法失败表现解决方案跨域浏览器 F12 看请求状态 CORS error请求被拦截响应为空Vite 或 Nginx 配置 /api 代理Token 传递查看请求头 Authorization后端返回 401检查请求拦截器和 localStorage日期传参接口入参的 billDate 值后端报 400 或解析失败前端格式化为 yyyy-MM-dd后端加注解JSON 字段大小写响应里字段名是否一致前端渲染 undefined统一 camelCase数据库下划线自动映射分页参数页码从 0 还是 1 开始第一页数据是空Element Plus 默认页码从 1后端对应金额精度结算后合计误差报表汇总对不上一律用 decimal避免 float路由刷新直接访问子路径404 页面history 模式加 fallback 或换 hash金额精度这条单独提一下。前端 JavaScript 的 Number 类型理论上精确到 2^53但浮点乘法照样有误差比如 0.1*3 结果是 0.30000000000000004。做报表合计时我用后端 decimal 求和前端只展示不参与聚合运算。如果你要在前端计算每日结余先把金额转成整数分再计算最后除以 100 展示。这是前端金额计算的最后一道防线这个常识值得刻进肌肉记忆。6. 进阶玩法把预算超支提醒和报表导出加进个人理财系统项目跑通基础流程后真正让它从“课设”变成“能用”的是预算超支提醒和账单导出这两个功能。预算超支的思路不难每次新增账单成功后异步查一下当月该分类的总支出和预算表比对超了就用系统通知的方式在页面右上角弹红色提醒。实现上我习惯在 BillService 的 addBill 方法里最后加一步 triggerBudgetAlert(bill)不搞定时任务——定时任务只能查昨天之前的数据当天记账当天提醒做不到。private void triggerBudgetAlert(Bill bill) { LocalDate now LocalDate.now(); BigDecimal monthTotal billMapper.selectMonthTotal(bill.getUserId(), bill.getCategoryId(), now); Budget budget budgetMapper.selectByCategoryAndPeriod(bill.getUserId(), bill.getCategoryId(), now); if (budget ! null monthTotal.compareTo(budget.getBudgetAmount()) 0) { Alert alert new Alert(); alert.setUserId(bill.getUserId()); alert.setMessage(分类已超预算 monthTotal); alertMapper.insert(alert); } }这个逻辑放在记账事务里如果插入提醒失败会导致整个记账回滚。要把提醒改成异步在 Spring Boot 里加 Async 并开启 EnableAsync或者用事务同步机制保证可靠。个人系统不做消息队列因为系统里没有除记账外的并发写入异步线程在应用内足够。注意异步方法和同类调用是无效的Spring 的 Async 基于代理必须把方法放到另一个 Service 类里调用否则注解不生效这个方法就没被执行。报表导出我用的是 Apache POI生成 xlsx 文件。导出内容包含两个 Sheet账单明细和月度汇总。这个功能看似简单但有一个常被忽略的点导出是 IO 密集操作数据量大时不能一次性全部查进内存。个人系统的数据量撑死几万条一次性查无压力但如果设计目标是给财务软件做原型还是得按批次查询。我的习惯是明细 Sheet 直接查询全部汇总 Sheet 用 SQL 按月份分组控制总行数在 Excel 单 Sheet 上限内。public void exportBills(HttpServletResponse response, ExportDTO dto) throws IOException { ListBillVO list billService.exportList(dto); XSSFWorkbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(记清明细); Row header sheet.createRow(0); String[] cols {日期, 分类, 金额, 备注}; for (int i 0; i cols.length; i) { header.createCell(i).setCellValue(cols[i]); } for (int i 0; i list.size(); i) { Row row sheet.createRow(i 1); row.createCell(0).setCellValue(list.get(i).getBillDate().toString()); row.createCell(1).setCellValue(list.get(i).getCategoryName()); row.createCell(2).setCellValue(list.get(i).getAmount().doubleValue()); row.createCell(3).setCellValue(list.get(i).getRemark()); } response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenamebills.xlsx); workbook.write(response.getOutputStream()); workbook.close(); }导出时 Excel 的金额列容易变成科学计数法尤其是金额是 10 位数以上时。处理办法是 setCellValue 传 BigDecimalPOI 会自动识别为数字类型。还有文件名乱码问题浏览器下载时中文文件名必须 URLEncoder.encode 再拼到 Content-Disposition 里否则 Firefox 直接下载成乱码文件。这段代码里的 XSSFWorkbook 是大文件用小数据量也可以用 HSSFWorkbook区别是 xlsx 和 xls 格式前端导出按钮两个都能识别。验证系统的最终环节我建议做三类回归测试第一跨月数据统计造一笔上月末的账单和一笔本月初的账单看月度报表是否只统计本月第二分类预算超支把预算金额设成 1 元记录一笔 2 元的支出看是否弹出提醒第三导出的 Excel 打开后金额、日期格式是否正确。这三个测试跑完系统的核心逻辑基本立于不败之地。我踩过一个反例月度汇总接口漏了用户过滤条件导致 A 用户的账单出现在 B 用户的报表里。查了两小时发现 SELECT 语句里 WHERE 子句漏了 user_id这种隐性 bug 不通过跨用户测试根本暴露不出来。最后说一点个人习惯。我做个人理财系统的经验是代码写的再花哨不如先保证账目一笔记一笔的准确性。把所有“智能分析”“自动归类”的功能往后放先把保存、查询、统计的边界条件测透。等到日常记账用起来不闹心再回头加 AI 分类或者趋势预测那时候才有底气说这个系统是自己能用的。希望这一篇能帮你把 Java Spring Boot Vue 这条线完整地走一遍少撞几个我撞过的墙。本文还有配套的精品资源点击获取
返回列表