
简介本资源是一套完整的个人财务管理系统实战项目源码面向Java与Vue全栈初学者及课程设计开发者解决个人收支记录、理财目标跟踪与财务数据可视化等实际管理需求。压缩包共377个文件8.18MB涵盖98个后端Java核心业务类含Spring Boot控制器、Service与Mapper层、40个前端Vue组件如账单录入、统计图表、用户设置等模块、16个JS工具脚本、2个SQL建表与初始化脚本、2个YML配置文件以及SVG图标、PNG界面素材等辅助资源结构清晰、模块职责分明。目前已有196人学习下载适合用于毕业设计、实训项目或Spring BootVue技术栈的工程化实践。读者可直接导入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 的核心原因生态与稳定性Java 生态尤其是 Spring 家族在企业级应用开发中经过了无数验证。个人项目虽然规模小但我希望它的代码结构、异常处理、事务管理是“工业级”的这能为后续功能扩展比如接入第三方支付 API、复杂报表计算打下坚实基础。SpringBoot 的“约定大于配置”理念让我能快速搭建起一个具备 RESTful API、数据库连接池、统一日志和监控的健壮后端而不用从零开始折腾 XML 配置。数据处理的强项财务系统涉及大量的金额计算、统计汇总和事务操作。Java 在数值计算通过BigDecimal处理金融精度、并发控制Transactional方面有天然优势社区里关于复杂 SQL 优化、分库分表虽然个人系统暂时用不到的解决方案也极其丰富。个人技术栈的延续作为一名主要使用 Java 的开发者用 SpringBoot 能最大化我的开发效率遇到问题也能快速在熟悉的社区找到答案。前端选择 Vue 的核心原因渐进式与上手速度Vue 的学习曲线相对平缓其模板语法对从 jQuery 时代过来的开发者或者后端转前端的同学非常友好。对于个人财务管理系统这种以表单、表格、图表操作为主的应用Vue 的响应式数据绑定和组件化开发能极大地提升开发体验和代码可维护性。我不需要像 React 那样深入理解函数式编程也能快速构建出交互良好的界面。丰富的 UI 库支持项目前端主要使用了 Element UI。它提供了现成且美观的表格、表单、日期选择器、图表容器等组件让我能专注于业务逻辑开发而不是从零开始写 CSS 调整样式。这对于追求开发效率的个人项目至关重要。前后端分离的清晰边界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 状态管理。用于存储全局状态如当前登录用户信息、全局的分类列表字典避免在不同页面重复请求。核心业务模块用户认证与授权模块采用 JWTJSON Web Token实现无状态登录。用户登录成功后后端生成一个包含用户ID等信息的 Token 返回给前端。前端后续请求在 HTTP Header 中携带此 Token。后端通过一个拦截器HandlerInterceptor来验证 Token 的有效性。这样做的好处是后端无需维护 Session易于扩展。账户管理模块定义你的“钱包”比如“支付宝”、“微信零钱”、“建设银行储蓄卡”。每个账户有独立的余额。任何收支记录都必须关联一个账户。收支记录模块系统的核心。每笔记录包含金额、类型收入/支出、分类餐饮、交通、工资等、账户、时间、备注。这里是 CRUD 操作最频繁的地方。分类管理模块支持用户自定义收支分类树。例如“支出”下可以有“衣食住行”“食”下又可以分“餐饮”、“零食”。这为后续的多维度统计提供了基础。数据统计与仪表盘模块基于用户的记录数据生成图表。例如月度支出趋势图、分类占比饼图、账户余额总览。这里前端使用了 ECharts 库来绘制丰富的图表后端则提供聚合查询的 API。3. 核心功能实现与关键技术点剖析3.1 数据库表设计与财务数据建模数据库设计是财务系统的基石设计不当会导致后期查询复杂、数据冗余或难以扩展。我的核心表只有几张但关系清晰核心表结构user用户表。存储登录名、加密后的密码、昵称等。account账户表。user_id关联用户name账户名balance当前余额关键字段type账户类型如现金、银行卡、信用卡。category分类表。user_idnametype收入/支出parent_id支持树形结构顶级分类 parent_id 为 0icon前端展示用图标。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) -- 复合索引加速按用户和时间查询 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT收支记录表;设计要点金额精度amount字段使用DECIMAL(10,2)表示总共10位小数位2位。绝对不要用FLOAT或DOUBLE它们在计算时会产生精度丢失这是财务系统的大忌。在 Java 实体类中对应使用BigDecimal类型。两个时间字段record_time是业务时间即这笔钱实际发生的时间用于统计。create_time是系统时间记录数据插入数据库的时刻。两者区分开避免用户修改记录时间时影响创建顺序。索引策略最常用的查询场景是“查询某个用户某段时间内的记录”。因此在(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 UPDATE。FOR UPDATE会给这条账户记录加上行级排他锁。这意味着在当前事务提交或回滚前其他任何事务都无法修改或FOR UPDATE查询这条记录。这是防止“超支”等并发问题的关键。没有这个锁两个并发请求可能同时读到相同的余额都判断为足够支付然后分别扣款导致最终余额为负。BigDecimal运算金额计算必须使用BigDecimal的add(),subtract(),compareTo()方法严禁使用,-,等运算符否则会丢失精度。3.3 前端Vue组件与ECharts图表集成前端页面的灵魂在于交互和数据可视化。以仪表盘页面为例它需要展示多个图表。1. 组件化开发Dashboard.vue页面由多个子组件构成template div classdashboard-container el-row :gutter20 el-col :span8 balance-overview :databalanceData / !-- 账户余额概览卡片组件 -- /el-col el-col :span16 expense-trend-chart :chart-datatrendData / !-- 月度支出趋势图组件 -- /el-col /el-row el-row :gutter20 stylemargin-top:20px; el-col :span12 category-pie-chart :chart-datacategoryData / !-- 分类占比饼图组件 -- /el-col el-col :span12 recent-records :listrecentList / !-- 最近记录列表组件 -- /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为例展示如何集成 EChartstemplate div refchartRef stylewidth: 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?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/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.activeprod your-project-0.0.1-SNAPSHOT.jar app.log 21 但这不够优雅进程挂了不会自动重启。推荐使用systemd来管理服务# /etc/systemd/system/finance-app.service [Unit] DescriptionPersonal Finance Management Application Afternetwork.target [Service] Typesimple Userappuser # 建议使用非root用户运行 WorkingDirectory/opt/finance-app ExecStart/usr/bin/java -jar -Dspring.profiles.activeprod /opt/finance-app/your-project-0.0.1-SNAPSHOT.jar SuccessExitStatus143 Restartalways # 总是重启 RestartSec10 StandardOutputjournal StandardErrorjournal [Install] WantedBymulti-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。然后配置 Nginxserver { 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 中使用了float、double或数据库使用了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://...serverTimezoneUTC。SpringBoot 配置在application.yml中设置spring.jackson.time-zone: UTC。前端传递前端传递时间字符串时最好传递带时区的 ISO 8601 格式如2024-05-01T00:00:00.000Z或者传递时间戳。后端接收到后统一转换为 UTC 时间再存入数据库。前端显示从后端拿到 UTC 时间戳后前端使用moment.js或day.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 设计时就要支持pageNum和pageSize参数后端只返回当前页的数据。前端虚拟滚动如果确实需要展示大量数据如导出前预览可以使用支持虚拟滚动的表格组件如 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太短。排查步骤网络连通性在应用服务器上用telnet 数据库IP 3306测试端口是否通。数据库权限确认配置的用户名密码正确且该用户有从应用服务器 IP 连接的权限GRANT ALL PRIVILEGES ON finance_db.* TO user应用服务器IP IDENTIFIED BY password;。连接池配置适当调大spring.datasource.hikari.connection-timeout默认30秒和spring.datasource.hikari.maximum-pool-size默认10。DNS 解析如果数据库连接 URL 用的是域名检查 DNS 解析是否正常。可以尝试换成 IP 地址测试。3. 如何查看和分析应用日志问题线上出了问题不知道去哪里看日志。解决方案日志文件SpringBoot 默认日志会输出到控制台。通过nohup ... app.log 21 或 systemd 的journalctl可以查看。更规范的做法是在application-prod.yml中配置日志输出到文件logging: file: name: /var/log/finance-app/app.log logback: rollingpolicy: max-file-size: 10MB max-history: 30日志级别生产环境通常设置root为INFO自己项目的包可以设为DEBUG以便排查问题。通过Slf4j注解Lombok在代码中打日志。日志内容关键业务节点如记录创建、用户登录、异常捕获处必须打印日志。打印时带上可追踪的请求ID如MDC中放入一个 UUID方便串联一次请求的所有日志。这个项目从构思到实现再到不断优化几乎涵盖了个人全栈开发中会遇到的大部分典型场景用户系统、复杂业务逻辑带事务的金额处理、前后端交互、数据可视化、部署运维。代码虽然不算复杂但每一个设计选择和问题解决方案背后都是对“如何构建一个健壮、可维护的应用”的思考。如果你能跟着这个思路亲手把它跑起来甚至在此基础上增加预算管理、多账本、账单导入导出、数据备份等功能那么你对全栈开发的理解一定会深入一个层次。本文还有配套的精品资源点击获取