news 2026/9/4 14:13:31

SpringBoot+Vue全栈实战:个人财务管理系统设计与实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue全栈实战:个人财务管理系统设计与实现

简介:本资源是一套完整的个人财务管理系统实战项目源码,面向Java与Vue全栈初学者及课程设计开发者,解决个人收支记录、理财目标跟踪与财务数据可视化等实际管理需求。压缩包共377个文件,8.18MB,涵盖98个后端Java核心业务类(含Spring Boot控制器、Service与Mapper层)、40个前端Vue组件(如账单录入、统计图表、用户设置等模块)、16个JS工具脚本、2个SQL建表与初始化脚本、2个YML配置文件,以及SVG图标、PNG界面素材等辅助资源,结构清晰、模块职责分明。目前已有196人学习下载,适合用于毕业设计、实训项目或Spring Boot+Vue技术栈的工程化实践。读者可直接导入IDE运行,完整掌握用户认证、账单CRUD、多维度收支统计、理财计划生命周期管理及前后端分离部署全流程。

1. 项目概述:从零到一构建你的个人财务中枢

最近在整理自己的开源项目仓库,翻到了一个几年前做的个人财务管理系统,当时是为了解决自己“钱花哪儿了”的痛点,用 SpringBoot 和 Vue 前后端分离搭了一套。没想到这个项目在 GitHub 上陆陆续续收到了一些 star,也有不少朋友私信问实现细节和部署问题。今天我就把这个项目的里里外外彻底拆解一遍,从技术选型、核心设计到每一行关键代码的逻辑,都拿出来聊聊。如果你是一名 Java 后端或前端开发者,想找一个完整的、有业务深度的全栈项目来练手,或者你正头疼于自己的收支混乱,想亲手打造一个专属的财务工具,那这篇内容应该能给你提供一条清晰的路径。

这个系统本质上是一个轻量级的“个人版”财务 SaaS,核心目标就三个:记录(每一笔收入支出)、分析(钱花在哪儿了,趋势如何)、规划(预算控制)。技术上采用了经典的 SpringBoot 后端 + Vue 前端架构,数据库用了 MySQL,整体技术栈成熟、学习资源丰富,非常适合个人学习和二次开发。接下来,我会带你深入这个项目的每一个模块,不仅告诉你怎么做,更会解释为什么这么做,以及我在开发过程中踩过的那些“坑”。

2. 技术选型与架构设计背后的思考

2.1 为什么是 SpringBoot + Vue?

当初选型时,市面上可选的技术组合很多。PHP 的 Laravel、Python 的 Django 都能快速出活,Node.js 的全栈方案也很流行。最终选择 SpringBoot + Vue,是基于以下几个很实际的考量:

后端选择 SpringBoot 的核心原因:

  1. 生态与稳定性:Java 生态,尤其是 Spring 家族,在企业级应用开发中经过了无数验证。个人项目虽然规模小,但我希望它的代码结构、异常处理、事务管理是“工业级”的,这能为后续功能扩展(比如接入第三方支付 API、复杂报表计算)打下坚实基础。SpringBoot 的“约定大于配置”理念,让我能快速搭建起一个具备 RESTful API、数据库连接池、统一日志和监控的健壮后端,而不用从零开始折腾 XML 配置。
  2. 数据处理的强项:财务系统涉及大量的金额计算、统计汇总和事务操作。Java 在数值计算(通过BigDecimal处理金融精度)、并发控制(@Transactional)方面有天然优势,社区里关于复杂 SQL 优化、分库分表(虽然个人系统暂时用不到)的解决方案也极其丰富。
  3. 个人技术栈的延续:作为一名主要使用 Java 的开发者,用 SpringBoot 能最大化我的开发效率,遇到问题也能快速在熟悉的社区找到答案。

前端选择 Vue 的核心原因:

  1. 渐进式与上手速度:Vue 的学习曲线相对平缓,其模板语法对从 jQuery 时代过来的开发者,或者后端转前端的同学非常友好。对于个人财务管理系统这种以表单、表格、图表操作为主的应用,Vue 的响应式数据绑定和组件化开发能极大地提升开发体验和代码可维护性。我不需要像 React 那样深入理解函数式编程,也能快速构建出交互良好的界面。
  2. 丰富的 UI 库支持:项目前端主要使用了 Element UI。它提供了现成且美观的表格、表单、日期选择器、图表容器等组件,让我能专注于业务逻辑开发,而不是从零开始写 CSS 调整样式。这对于追求开发效率的个人项目至关重要。
  3. 前后端分离的清晰边界:Vue 通过 axios 与后端 SpringBoot API 通信,使得前后端职责清晰。后端只负责数据和业务逻辑,前端负责展示和交互。这种架构方便未来独立升级前端框架(比如迁移到 Vue 3)或后端服务。

数据库选择 MySQL:没太多悬念。关系型数据库在处理财务流水这种结构化数据、需要复杂查询(如按时间、类别、金额范围多维度筛选统计)和事务保证(确保转账记录同时更新双方账户)时,依然是首选。它的安装、运维简单,可视化工具(如 Navicat、DBeaver)成熟,社区活跃。

2.2 整体架构与核心模块划分

系统采用典型的分层架构,但我在设计时特别强调了“领域清晰”和“模块自治”。

后端(SpringBoot)分层:

  • Controller 层:接收前端 Vue 发来的 HTTP 请求,进行参数校验(使用@Validated注解),并调用对应的 Service 方法。返回统一格式的 JSON 数据(包含 code, message, data)。这里有个关键点:我设计了一个全局的异常处理器(@ControllerAdvice),将业务异常、参数校验异常、系统异常统一捕获并转换为前端能友好展示的错误信息,避免了 API 直接抛出堆栈信息。
  • Service 层:核心业务逻辑所在地。例如,RecordService中包含了新增一笔记录的完整逻辑:校验金额、分类是否存在、更新账户余额、记录流水。这里使用了 Spring 的@Transactional注解来保证“记录插入”和“账户更新”在一个数据库事务中,要么全成功,要么全回滚,防止数据不一致。
  • Mapper 层(DAO层):使用 MyBatis-Plus 作为 ORM 框架。它强大的 CRUD 封装和条件构造器(QueryWrapper)让基础的数据操作变得极其简单。但对于复杂的多表关联查询(如查询某个月所有分类的支出总额),我仍然会手写 XML 映射文件中的 SQL,以保持对性能的绝对控制。
  • Entity 层:对应数据库表的实体类。我使用了 Lombok 插件自动生成 getter/setter 等方法,让实体类代码非常简洁。
  • Common 层:放置工具类、常量定义、统一返回对象(Result)、全局配置等。

前端(Vue)目录结构:

  • src/api/:集中管理所有对后端 API 的调用。每个业务模块一个 JS 文件(如record.js),使用 axios 实例(配置了基础 URL、请求拦截器添加 Token、响应拦截器处理错误)来发起请求。这样前后端接口对接清晰,也便于 Mock 数据。
  • src/views/:页面级组件。如RecordList.vue(流水列表页)、Dashboard.vue(数据概览页)。
  • src/components/:可复用的展示组件。如CategorySelector.vue(分类选择器)、DateRangePicker.vue(日期范围选择器)。
  • src/router/:Vue Router 配置,管理前端路由和页面权限(通过路由守卫实现,例如未登录用户访问受限页面会被重定向到登录页)。
  • src/store/:Vuex 状态管理。用于存储全局状态,如当前登录用户信息、全局的分类列表字典(避免在不同页面重复请求)。

核心业务模块:

  1. 用户认证与授权模块:采用 JWT(JSON Web Token)实现无状态登录。用户登录成功后,后端生成一个包含用户ID等信息的 Token 返回给前端。前端后续请求在 HTTP Header 中携带此 Token。后端通过一个拦截器(HandlerInterceptor)来验证 Token 的有效性。这样做的好处是后端无需维护 Session,易于扩展。
  2. 账户管理模块:定义你的“钱包”,比如“支付宝”、“微信零钱”、“建设银行储蓄卡”。每个账户有独立的余额。任何收支记录都必须关联一个账户。
  3. 收支记录模块:系统的核心。每笔记录包含金额、类型(收入/支出)、分类(餐饮、交通、工资等)、账户、时间、备注。这里是 CRUD 操作最频繁的地方。
  4. 分类管理模块:支持用户自定义收支分类树。例如,“支出”下可以有“衣食住行”,“食”下又可以分“餐饮”、“零食”。这为后续的多维度统计提供了基础。
  5. 数据统计与仪表盘模块:基于用户的记录数据,生成图表。例如,月度支出趋势图、分类占比饼图、账户余额总览。这里前端使用了 ECharts 库来绘制丰富的图表,后端则提供聚合查询的 API。

3. 核心功能实现与关键技术点剖析

3.1 数据库表设计与财务数据建模

数据库设计是财务系统的基石,设计不当会导致后期查询复杂、数据冗余或难以扩展。我的核心表只有几张,但关系清晰:

核心表结构:

  • user:用户表。存储登录名、加密后的密码、昵称等。
  • account:账户表。user_id(关联用户),name(账户名),balance(当前余额,关键字段),type(账户类型,如现金、银行卡、信用卡)。
  • category:分类表。user_idnametype(收入/支出),parent_id(支持树形结构,顶级分类 parent_id 为 0),icon(前端展示用图标)。
  • record:收支记录表。这是最核心的表。
    -- 简化版DDL示例 CREATE TABLE `record` ( `id` bigint NOT NULL AUTO_INCREMENT, `user_id` bigint NOT NULL COMMENT '用户ID', `account_id` bigint NOT NULL COMMENT '账户ID', `category_id` bigint NOT NULL COMMENT '分类ID', `type` tinyint NOT NULL COMMENT '类型:1-收入,2-支出', `amount` decimal(10,2) NOT NULL COMMENT '金额,精确到分', `remark` varchar(500) DEFAULT '' COMMENT '备注', `record_time` datetime NOT NULL COMMENT '记录时间(业务时间)', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_user_time` (`user_id`,`record_time`) -- 复合索引,加速按用户和时间查询 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='收支记录表';
    设计要点
    1. 金额精度amount字段使用DECIMAL(10,2),表示总共10位,小数位2位。绝对不要用FLOATDOUBLE,它们在计算时会产生精度丢失,这是财务系统的大忌。在 Java 实体类中,对应使用BigDecimal类型。
    2. 两个时间字段record_time是业务时间,即这笔钱实际发生的时间,用于统计。create_time是系统时间,记录数据插入数据库的时刻。两者区分开,避免用户修改记录时间时影响创建顺序。
    3. 索引策略:最常用的查询场景是“查询某个用户某段时间内的记录”。因此在(user_id, record_time)上建立了复合索引,能极大提升列表查询和统计查询的速度。

账户余额的维护:这是一个关键且容易出错的点。账户的balance字段不能简单地通过SUM(record.amount)实时计算,因为当记录量巨大时,每次查询余额都会全表扫描,性能无法接受。因此,我采用了“余额作为状态字段,由事务保证一致性”的策略:

  • 当新增一笔支出记录时,在同一个数据库事务中:1) 插入record表;2) 执行UPDATE account SET balance = balance - #{amount} WHERE id = #{accountId}
  • 当新增一笔收入记录时,则是balance = balance + #{amount}
  • 修改删除一条已有记录时,逻辑更复杂:需要先根据旧记录回滚原账户的余额变动,再根据新记录(或删除操作)应用新的变动。这个过程必须在同一个事务中完成,并且加锁(如SELECT ... FOR UPDATE)防止并发操作导致余额错误。

3.2 后端核心业务逻辑与事务控制

以“新增一笔支出记录”这个最核心的业务为例,我们看看 Service 层如何实现:

@Service @Transactional(rollbackFor = Exception.class) // 声明式事务,任何异常都回滚 public class RecordServiceImpl implements RecordService { @Autowired private AccountMapper accountMapper; @Autowired private RecordMapper recordMapper; @Override public Result addRecord(RecordDTO recordDTO) { // 1. 参数校验 (使用Spring Validation或手动校验) if (recordDTO.getAmount() == null || recordDTO.getAmount().compareTo(BigDecimal.ZERO) <= 0) { return Result.error("金额必须大于0"); } // ... 其他校验 // 2. 查询账户并加锁(悲观锁,防止并发更新余额) Account account = accountMapper.selectAccountForUpdate(recordDTO.getAccountId()); if (account == null) { return Result.error("账户不存在"); } // 3. 计算新余额 BigDecimal newBalance; if (RecordType.EXPENSE.getCode().equals(recordDTO.getType())) { // 支出 newBalance = account.getBalance().subtract(recordDTO.getAmount()); if (newBalance.compareTo(BigDecimal.ZERO) < 0) { // 这里可以做成可配置:是否允许透支 return Result.error("账户余额不足"); } } else { // 收入 newBalance = account.getBalance().add(recordDTO.getAmount()); } // 4. 更新账户余额 account.setBalance(newBalance); accountMapper.updateById(account); // MyBatis-Plus 方法 // 5. 创建记录实体并保存 Record record = new Record(); BeanUtils.copyProperties(recordDTO, record); // 属性拷贝 record.setCreateTime(new Date()); recordMapper.insert(record); // 6. 返回成功(事务在此方法结束后,若无异常则提交) return Result.success("记录添加成功"); } }

关键点解析:

  • @Transactional:这是 Spring 事务管理的核心注解。它保证了从“查询账户”到“更新余额”再到“插入记录”这三个数据库操作,要么全部成功,要么全部失败回滚。例如,如果在insert(record)时数据库异常,那么之前updateById(account)对余额的修改也会被撤销,账户余额保持不变。
  • selectAccountForUpdate:这是一个自定义的 Mapper 方法,对应的 SQL 是SELECT * FROM account WHERE id = #{id} FOR UPDATEFOR UPDATE会给这条账户记录加上行级排他锁。这意味着在当前事务提交或回滚前,其他任何事务都无法修改或FOR UPDATE查询这条记录。这是防止“超支”等并发问题的关键。没有这个锁,两个并发请求可能同时读到相同的余额,都判断为足够支付,然后分别扣款,导致最终余额为负。
  • BigDecimal运算:金额计算必须使用BigDecimaladd(),subtract(),compareTo()方法,严禁使用+,-,>等运算符,否则会丢失精度。

3.3 前端Vue组件与ECharts图表集成

前端页面的灵魂在于交互和数据可视化。以仪表盘页面为例,它需要展示多个图表。

1. 组件化开发:Dashboard.vue页面由多个子组件构成:

<template> <div class="dashboard-container"> <el-row :gutter="20"> <el-col :span="8"> <balance-overview :data="balanceData" /> <!-- 账户余额概览卡片组件 --> </el-col> <el-col :span="16"> <expense-trend-chart :chart-data="trendData" /> <!-- 月度支出趋势图组件 --> </el-col> </el-row> <el-row :gutter="20" style="margin-top:20px;"> <el-col :span="12"> <category-pie-chart :chart-data="categoryData" /> <!-- 分类占比饼图组件 --> </el-col> <el-col :span="12"> <recent-records :list="recentList" /> <!-- 最近记录列表组件 --> </el-col> </el-row> </div> </template> <script> import BalanceOverview from './components/BalanceOverview.vue'; import ExpenseTrendChart from './components/ExpenseTrendChart.vue'; import CategoryPieChart from './components/CategoryPieChart.vue'; import RecentRecords from './components/RecentRecords.vue'; import { getDashboardData } from '@/api/dashboard'; export default { name: 'Dashboard', components: { BalanceOverview, ExpenseTrendChart, CategoryPieChart, RecentRecords }, data() { return { balanceData: {}, trendData: {}, categoryData: {}, recentList: [] }; }, mounted() { this.loadData(); }, methods: { async loadData() { try { const response = await getDashboardData(); const data = response.data; this.balanceData = data.balanceOverview; this.trendData = data.expenseTrend; this.categoryData = data.categoryDistribution; this.recentList = data.recentRecords; } catch (error) { this.$message.error('加载仪表盘数据失败'); } } } }; </script>

通过组件化,每个图表或功能块独立开发、维护和复用,代码结构非常清晰。

2. ECharts 图表集成:ExpenseTrendChart.vue为例,展示如何集成 ECharts:

<template> <div ref="chartRef" style="width: 100%; height: 400px;"></div> </template> <script> import * as echarts from 'echarts'; export default { name: 'ExpenseTrendChart', props: { chartData: { type: Object, required: true } }, data() { return { chartInstance: null }; }, watch: { // 监听数据变化,重新渲染图表 chartData: { deep: true, handler(newVal) { if (this.chartInstance) { this.updateChart(newVal); } } } }, mounted() { this.initChart(); window.addEventListener('resize', this.handleResize); }, beforeDestroy() { // 组件销毁时,销毁图表实例并移除监听,防止内存泄漏 if (this.chartInstance) { echarts.dispose(this.chartInstance); } window.removeEventListener('resize', this.handleResize); }, methods: { initChart() { this.chartInstance = echarts.init(this.$refs.chartRef); this.updateChart(this.chartData); }, updateChart(data) { const option = { title: { text: '月度支出趋势', left: 'center' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: data.months // 例如 ['2024-01', '2024-02', ...] }, yAxis: { type: 'value', name: '金额(元)' }, series: [{ name: '支出', type: 'line', smooth: true, data: data.amounts, // 对应月份的金额数组 itemStyle: { color: '#ff6b6b' }, areaStyle: { color: 'rgba(255, 107, 107, 0.1)' } }], grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true } }; this.chartInstance.setOption(option); }, handleResize() { if (this.chartInstance) { this.chartInstance.resize(); // 窗口大小变化时,图表自适应 } } } }; </script>

注意事项

  • 图表实例管理:一定要在beforeDestroy生命周期钩子中调用echarts.dispose()来销毁图表实例,否则会导致内存泄漏。
  • 响应式:监听窗口resize事件并调用图表的resize()方法,是保证图表在页面布局变化时能自适应大小的标准做法。
  • 性能:如果数据量很大(比如展示全年365天的每日支出),需要考虑对数据进行聚合(如按周或月),或者使用 ECharts 的数据采样功能,避免渲染卡顿。

4. 项目部署与运维实践

开发完成只是第一步,让项目稳定跑起来才是终点。我提供了两种主流的部署方式。

4.1 后端 SpringBoot 应用部署

SpringBoot 应用部署非常灵活,我通常推荐打成可执行 JAR 包,通过系统服务来管理。

1. 打包:在项目根目录下,使用 Maven 命令打包:

mvn clean package -DskipTests

打包后会在target/目录下生成一个your-project-0.0.1-SNAPSHOT.jar文件。这个 JAR 包是“可执行”的,因为它内嵌了 Tomcat 服务器。

2. 生产环境配置:千万不要把开发环境的配置(如application-dev.yml)直接用到生产环境!你需要一个独立的application-prod.yml文件,并通过启动参数激活它。

# application-prod.yml spring: datasource: url: jdbc:mysql://你的生产数据库IP:3306/finance_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: prod_user # 使用权限受限的数据库用户 password: 强密码 # 密码应从环境变量或配置中心读取,不应硬编码 hikari: maximum-pool-size: 10 # 根据服务器配置调整连接池大小 # 关闭开发工具,如热部署 devtools: restart: enabled: false # 生产环境日志级别通常设为 INFO 或 WARN logging: level: root: INFO com.yourcompany: DEBUG # 可以保留自己项目包的DEBUG日志便于排查 # JWT 密钥,生产环境务必使用复杂且保密的字符串 jwt: secret: 你的超级复杂生产环境密钥 expire: 7200 # token过期时间(秒)

3. 启动与守护:在 Linux 服务器上,最简单的启动方式是:

nohup java -jar -Dspring.profiles.active=prod your-project-0.0.1-SNAPSHOT.jar > app.log 2>&1 &

但这不够优雅,进程挂了不会自动重启。推荐使用systemd来管理服务:

# /etc/systemd/system/finance-app.service [Unit] Description=Personal Finance Management Application After=network.target [Service] Type=simple User=appuser # 建议使用非root用户运行 WorkingDirectory=/opt/finance-app ExecStart=/usr/bin/java -jar -Dspring.profiles.active=prod /opt/finance-app/your-project-0.0.1-SNAPSHOT.jar SuccessExitStatus=143 Restart=always # 总是重启 RestartSec=10 StandardOutput=journal StandardError=journal [Install] WantedBy=multi-user.target

然后使用sudo systemctl start finance-app启动,sudo systemctl enable finance-app设置开机自启。通过journalctl -u finance-app -f可以查看实时日志。

4.2 前端 Vue 应用部署

Vue 项目需要先构建(Build)成静态文件,然后由 Web 服务器(如 Nginx)托管。

1. 构建:在 Vue 项目根目录下运行:

npm run build

这会在dist/目录下生成优化和压缩后的静态文件(HTML, CSS, JS)。

2. Nginx 配置:dist/目录下的所有文件上传到服务器(例如/usr/share/nginx/html/finance)。然后配置 Nginx:

server { listen 80; server_name your-domain.com; # 你的域名或IP root /usr/share/nginx/html/finance; index index.html; # 处理前端路由(Vue Router的history模式) location / { try_files $uri $uri/ /index.html; } # 反向代理API请求到后端SpringBoot应用 location /api/ { proxy_pass http://localhost:8080/; # 假设后端运行在8080端口 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 可选:静态资源缓存 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ { expires 1y; add_header Cache-Control "public, immutable"; } }

这个配置做了两件关键事:

  • try_files $uri $uri/ /index.html;:这是支持 Vue Routerhistory模式的关键。当用户直接访问一个前端路由(如/dashboard)时,Nginx 会尝试查找对应的文件,找不到则返回index.html,由 Vue 应用自己来处理路由。
  • location /api/:将所有以/api开头的请求转发到后端的 SpringBoot 应用。这样前后端就通过同一个域名(或IP)对外提供服务,避免了跨域(CORS)问题。

3. 启动与检查:

sudo nginx -t # 检查配置文件语法 sudo systemctl reload nginx # 重新加载配置

之后,访问你的服务器 IP 或域名,应该就能看到登录页面了。

5. 开发与部署中的常见问题与解决方案

在实际开发和部署过程中,我遇到了不少典型问题,这里总结一下,希望能帮你绕过这些坑。

5.1 后端常见问题

1. 金额计算精度丢失

  • 现象:统计报表里的合计金额,小数点后出现很多位,或者加减结果有细微误差。
  • 原因:在 Java 中使用了floatdouble或数据库使用了FLOAT/DOUBLE类型进行金额存储和计算。
  • 解决方案
    • 数据库:金额字段一律使用DECIMAL(p, s)类型,例如DECIMAL(10,2)
    • Java 实体类:对应字段使用BigDecimal类型。
    • 计算:所有加减乘除都必须使用BigDecimal的方法:add(),subtract(),multiply(),divide()。特别注意divide()方法必须指定精度和舍入模式,否则可能抛出ArithmeticException
    // 错误 double total = a + b; // 正确 BigDecimal total = amountA.add(amountB); // 除法必须指定精度 BigDecimal average = total.divide(new BigDecimal(count), 2, RoundingMode.HALF_UP);

2. 并发操作导致余额错误

  • 现象:夫妻两人几乎同时用系统记录从同一张银行卡的消费,可能导致余额计算错误,甚至出现负数。
  • 原因:经典的“超卖”问题。两个线程同时读取了相同的余额,都判断足够支付,然后分别进行扣减。
  • 解决方案:使用数据库悲观锁。
    • 在更新账户余额的 SQL 前,先使用SELECT ... FOR UPDATE锁定该行记录。这确保了在事务提交前,其他事务无法修改这行数据。
    • 在 MyBatis-Plus 中,可以自定义 Mapper 方法:
    // AccountMapper.java @Select("SELECT * FROM account WHERE id = #{id} FOR UPDATE") Account selectAccountForUpdate(Long id);
    • 在 Service 方法中,先调用此方法锁定账户,再进行余额计算和更新。务必确保整个操作在一个@Transactional事务内。

3. 时间字段的时区问题

  • 现象:前端选择“2024-05-01”记录,存入数据库后查询出来变成了“2024-04-30”。
  • 原因:服务器、数据库、应用三者的时区设置不一致。
  • 解决方案
    • 统一标准:在系统内部,全部使用UTC 时间服务器本地时间(如 CST)进行存储和计算。我推荐使用 UTC。
    • 数据库连接:在 JDBC URL 中指定时区:jdbc:mysql://...&serverTimezone=UTC
    • SpringBoot 配置:在application.yml中设置:spring.jackson.time-zone: UTC
    • 前端传递:前端传递时间字符串时,最好传递带时区的 ISO 8601 格式(如2024-05-01T00:00:00.000Z),或者传递时间戳。后端接收到后,统一转换为 UTC 时间再存入数据库。
    • 前端显示:从后端拿到 UTC 时间戳后,前端使用moment.jsday.js库,根据用户所在时区转换为本地时间进行展示。

5.2 前端常见问题

1. 页面刷新后 Vuex 状态丢失

  • 现象:用户登录后,刷新浏览器页面,登录状态没了,被踢回登录页。
  • 原因:Vuex 的状态存储在内存中,页面刷新会重新初始化 JavaScript 环境,导致状态清空。
  • 解决方案:结合本地存储(LocalStorage/SessionStorage)持久化关键状态。
    • 登录成功后,不仅将 Token 和用户信息存入 Vuex,也存入localStorage
    • 在 Vue 应用的入口文件(如main.js)或根组件(如App.vue)的created钩子中,从localStorage读取 Token 和用户信息,并提交到 Vuex 进行初始化。
    • 注意安全localStorage对 XSS 攻击没有抵抗力。确保你的网站没有 XSS 漏洞,或者考虑使用vuex-persistedstate这类库,它支持配置存储引擎和加密。

2. 图表组件在弹窗或标签页切换后显示异常

  • 现象:ECharts 图表在el-dialog弹窗中首次打开显示正常,关闭后再打开就变小或布局错乱;或者在el-tab中,切换到其他标签再切回来,图表变形。
  • 原因:ECharts 实例在初始化时,需要获取其容器 DOM 的实际宽度和高度。当容器初始处于隐藏状态(display: none,如未打开的弹窗或非激活的标签页)时,获取到的宽高为 0,导致图表渲染错误。
  • 解决方案
    • 方案一(推荐):在容器变为可见后,手动调用图表的resize()方法。
    // 在弹窗组件或标签页组件中 watch: { // 假设 visible 是控制弹窗显示的 prop visible(newVal) { if (newVal) { // 使用 $nextTick 确保 DOM 更新完成 this.$nextTick(() => { if (this.chartInstance) { this.chartInstance.resize(); } }); } } }
    • 方案二:使用v-if而非v-show控制图表容器的显示。v-if是惰性的,在条件为真时才会创建和挂载图表实例,此时容器是可见的。但要注意频繁切换可能影响性能。

3. 列表数据量大时页面卡顿

  • 现象:收支记录有几千上万条时,一次性渲染到表格,页面滚动卡顿,甚至浏览器崩溃。
  • 原因:DOM 节点过多,浏览器渲染压力巨大。
  • 解决方案
    • 后端分页:这是必须的。API 设计时就要支持pageNumpageSize参数,后端只返回当前页的数据。
    • 前端虚拟滚动:如果确实需要展示大量数据(如导出前预览),可以使用支持虚拟滚动的表格组件,如 Element UI 的el-table在设置height属性后,会对固定高度的区域进行虚拟渲染。或者使用专门的虚拟滚动库,如vue-virtual-scroller
    • 优化表格列:减少不必要的复杂自定义模板,尤其是嵌套过深的 DOM 结构。

5.3 部署与运维问题

1. 前端访问后端 API 跨域(CORS)问题

  • 现象:前端运行在localhost:8081,后端运行在localhost:8080,浏览器控制台报错:Access-Control-Allow-Origin
  • 原因:浏览器的同源策略禁止跨域请求。
  • 解决方案
    • 开发环境:在 Vue 的vue.config.js中配置代理。
    module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', // 后端地址 changeOrigin: true, pathRewrite: { '^/api': '' // 可选,重写路径 } } } } };
    • 生产环境:如前文所述,使用 Nginx 反向代理,让前后端处于同一个域名下,从根本上避免跨域。

2. 应用启动后,数据库连接缓慢或失败

  • 现象:SpringBoot 应用启动日志卡在 “HikariPool-1 - Starting...”,很久才成功或直接报连接超时。
  • 原因:数据库服务器网络不通、防火墙未开放端口、数据库用户权限不足、连接池配置不当(如connectionTimeout太短)。
  • 排查步骤
    1. 网络连通性:在应用服务器上用telnet <数据库IP> 3306测试端口是否通。
    2. 数据库权限:确认配置的用户名密码正确,且该用户有从应用服务器 IP 连接的权限(GRANT ALL PRIVILEGES ON finance_db.* TO 'user'@'应用服务器IP' IDENTIFIED BY 'password';)。
    3. 连接池配置:适当调大spring.datasource.hikari.connection-timeout(默认30秒)和spring.datasource.hikari.maximum-pool-size(默认10)。
    4. DNS 解析:如果数据库连接 URL 用的是域名,检查 DNS 解析是否正常。可以尝试换成 IP 地址测试。

3. 如何查看和分析应用日志

  • 问题:线上出了问题,不知道去哪里看日志。
  • 解决方案
    • 日志文件:SpringBoot 默认日志会输出到控制台。通过nohup ... > app.log 2>&1 &或 systemd 的journalctl可以查看。更规范的做法是在application-prod.yml中配置日志输出到文件:
    logging: file: name: /var/log/finance-app/app.log logback: rollingpolicy: max-file-size: 10MB max-history: 30
    • 日志级别:生产环境通常设置rootINFO,自己项目的包可以设为DEBUG以便排查问题。通过@Slf4j注解(Lombok)在代码中打日志。
    • 日志内容:关键业务节点(如记录创建、用户登录)、异常捕获处必须打印日志。打印时带上可追踪的请求ID(如MDC中放入一个 UUID),方便串联一次请求的所有日志。

这个项目从构思到实现,再到不断优化,几乎涵盖了个人全栈开发中会遇到的大部分典型场景:用户系统、复杂业务逻辑(带事务的金额处理)、前后端交互、数据可视化、部署运维。代码虽然不算复杂,但每一个设计选择和问题解决方案背后,都是对“如何构建一个健壮、可维护的应用”的思考。如果你能跟着这个思路,亲手把它跑起来,甚至在此基础上增加预算管理、多账本、账单导入导出、数据备份等功能,那么你对全栈开发的理解一定会深入一个层次。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/4 14:12:50

【单片机毕业设计】多档位可调语音控制智能台灯硬件系统设计 基于单片机的自动调光人体感应智能台灯实现(021406)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于嵌入式单片机&#xff0c;Java、小程序技术领域和毕业项目实战 ✌️…

作者头像 李华
网站建设 2026/9/4 14:11:17

OpenScreen 导出失败快速修复:MP4 与 GIF 导不出的 3 个主因

OpenScreen 导出失败快速修复&#xff1a;MP4 与 GIF 导不出的 3 个主因 【免费下载链接】openscreen Create stunning demos for free. Open-source, no subscriptions, no watermarks, and free for commercial use. An alternative to Screen Studio. 项目地址: https://g…

作者头像 李华
网站建设 2026/9/4 14:09:55

一份配色,适配 20+ 终端:iTerm2-Color-Schemes 完整使用指南

一份配色&#xff0c;适配 20 终端&#xff1a;iTerm2-Color-Schemes 完整使用指南 【免费下载链接】iTerm2-Color-Schemes Over 450 terminal color schemes/themes for iTerm/iTerm2. Includes ports to Terminal, Konsole, PuTTY, Xresources, XRDB, Remmina, Termite, XFCE…

作者头像 李华
网站建设 2026/9/4 14:09:46

V 语言安装完整指南:5 分钟从零开始跑通 Hello World

V 语言安装完整指南&#xff1a;5 分钟从零开始跑通 Hello World 【免费下载链接】v Simple, fast, safe, compiled language for developing maintainable software. Compiles itself in <1s with zero library dependencies. Supports automatic C > V translation. ht…

作者头像 李华
网站建设 2026/9/4 14:09:21

微信聊天记录导出完全指南:4 种格式加年度报告,数据不出本机

微信聊天记录导出完全指南&#xff1a;4 种格式加年度报告&#xff0c;数据不出本机 【免费下载链接】WeChatMsg 提取微信聊天记录&#xff0c;将其导出成HTML、Word、CSV文档永久保存&#xff0c;对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_…

作者头像 李华
网站建设 2026/9/4 14:05:54

PPT Master:一句话把文档变成原生 PowerPoint 的开源工具

PPT Master&#xff1a;一句话把文档变成原生 PowerPoint 的开源工具 【免费下载链接】ppt-master AI turns documents or topics into real, native PowerPoint decks—with native shapes, transitions and animations, data-backed charts and tables on demand, audio narr…

作者头像 李华