Spring Boot+Vue 3全栈实战:从零构建高性能家庭记账系统
1. 项目概述与核心价值最近在整理家里的财务发现用Excel记账虽然灵活但数据同步、分类统计和趋势分析实在太麻烦了。每次想看看这个月钱花哪儿了都得手动筛选、求和费时费力。于是我决定自己动手用Spring Boot和Vue 3搭建一个专属的家庭记账系统。这个项目我把它命名为“家庭记账系统”核心目标就是让记账这件事变得简单、直观并且能随时掌握家庭的财务脉搏。这个系统不是一个简单的CRUD应用它涵盖了从后端API设计、数据库建模到前端交互和性能优化的完整链路。对于正在学习全栈开发或者想找一个有实际业务场景的练手项目的朋友来说非常有参考价值。它用到了Spring Boot、MyBatis Plus、Redis、Vue 3、Element Plus等主流技术栈并且设计了清晰的账单、分类、支付方式管理以及多维度的统计分析功能。无论你是想了解如何设计一个前后端分离的应用架构还是想学习如何将缓存、连接池优化落地到具体业务中这个项目都能给你提供一套完整的实现思路和代码参考。2. 技术选型与架构设计思路2.1 后端技术栈深度解析选择Spring Boot 2.7.14作为后端框架主要是看中了它的“约定大于配置”理念和强大的生态。这个版本是一个长期支持版本稳定性和社区支持都很好避免了使用最新版可能遇到的兼容性问题。对于ORM框架我放弃了传统的MyBatis直接选择了MyBatis Plus。原因很简单它提供了强大的单表CRUD封装比如QueryWrapper、Service层封装能减少大量模板代码同时保留了MyBatis编写复杂SQL的灵活性。在账单的多条件分页查询场景下用QueryWrapper动态构建查询条件非常方便。数据库选用MySQL 8.0主要是考虑到它的事务可靠性、成熟的生态以及对于JSON等新特性的支持虽然本项目暂未用到。字符集统一使用utf8mb4这是为了完全兼容Emoji等四字节字符避免未来在备注等信息存储上出现乱码。缓存方面引入了Redis这步棋很关键。像“支付方式列表”、“分类列表”这种几乎不变的基础数据以及“月度统计结果”这类计算成本较高的数据非常适合用缓存。我实测过一个简单的分类查询接口引入Redis缓存后在高并发场景下响应时间能降低一个数量级。连接池选择了阿里开源的Druid而不是HikariCP。这里有个小考量Druid提供了非常强大的监控功能可以清晰地看到SQL执行情况、连接池状态等这对于后期排查慢SQL、优化数据库性能非常有帮助。在家庭记账这种数据量不大但访问可能随机的场景下细致的监控比极致的性能更重要。2.2 前端技术栈与工程化考量前端框架锁定Vue 3.3.4并搭配Composition API。Vue 3的响应式系统重写后性能提升明显Composition API的逻辑复用能力比Options API强太多。特别是在封装账单列表的查询、分页逻辑时可以抽离成独立的useBillList组合式函数在各个页面中复用代码非常清晰。构建工具从Webpack换成了Vite 5.0。Vite基于原生ES模块启动速度和热更新速度是碾压级的优势。开发体验的提升是实实在在的改完代码几乎瞬间就能在浏览器看到效果。UI库选择了Element Plus它对Vue 3的支持完善组件丰富且风格中性能快速搭建出美观可用的后台管理界面。像日期选择器、表格、分页这些记账系统高频使用的组件Element Plus都提供了开箱即用的解决方案。状态管理没有引入Pinia。经过评估本项目前端状态并不复杂主要是账单的查询条件、用户信息等。利用Vue 3的reactive和provide/inject完全可以在组件间共享状态避免引入额外的复杂度。HTTP客户端选用Axios并进行了统一的请求/响应拦截器封装便于处理全局的Loading状态、错误提示和Token注入为后续的多用户认证预留。2.3 整体架构设计图与数据流整个系统采用经典的前后端分离架构。浏览器访问Vue构建的静态页面页面通过Axios调用部署在8090端口的Spring Boot后端RESTful API。后端应用通过Druid连接池与MySQL交互同时将热点数据缓存到Redis中。数据流的核心是“账单”。用户在前端新增一条账单记录前端通过POST请求将JSON数据发送到/api/bills。后端BillController接收请求调用BillService进行业务逻辑处理如校验金额正负。BillService再调用BillMapper由MyBatis Plus增强将数据插入bill表。插入成功后为了保证统计数据的实时性服务会主动清除Redis中相关的统计缓存例如以当前月份为Key的缓存。当用户查询账单列表或统计报表时服务会先尝试从Redis读取命中则直接返回未命中则查询数据库并将结果写入Redis后再返回。这种设计将读压力尽可能导向缓存写操作则保证数据库的强一致性并通过清理缓存来保证数据的最终一致性非常适合记账这种“读多写少”的场景。3. 数据库设计与核心表结构剖析3.1 核心表关系与设计原则数据库一共设计了3张核心表bill账单表、bill_category分类表、payment_method支付方式表。它们之间的关系是典型的星型结构bill表作为事实表通过category_id和payment_method_id两个外键分别关联到bill_category和payment_method这两张维度表。这样设计有几个好处。首先是清晰账单是什么、属于哪类、用什么方式支付关系一目了然。其次是易于扩展如果想增加新的支付方式比如某个新的电子钱包只需在payment_method表插入一条记录所有历史账单都能通过ID关联上无需修改账单表结构。最后是便于统计做“按分类统计支出”这样的分析时一个简单的GROUP BY联表查询就能搞定。所有表都包含了create_time和update_time字段这是必须的。不仅是为了记录数据变更时间在排查问题或者做数据审计时这两个字段能提供巨大的帮助。我习惯用数据库的CURRENT_TIMESTAMP作为默认值update_time则在更新时自动刷新。3.2 账单表bill字段设计详解bill表是系统的核心每个字段都经过仔细推敲。id: 主键使用BIGINT自增足够应对海量数据。payee收款方:VARCHAR(100)。长度100是基于实际场景估算的超市、电商平台、个人名字一般都不会超过这个长度。amount金额:DECIMAL(10,2)。这是最关键的设计之一。我刻意将支出记为负数收入记为正数。比如买菜花100元就存-100.00发工资10000元就存10000.00。这样做有个巨大的好处计算净收入总收入-总支出时一个简单的SUM(amount)就能得到结果逻辑非常直观。DECIMAL(10,2)表示总共10位数字其中小数位占2位整数位最多8位足以应对千万级别的金额。transaction_time交易时间:DATETIME。精确到秒用于记录交易发生的具体时刻而create_time是记录入系统的时间两者有区别。payment_method_idcategory_id: 外键关联到维度表。这里没有在数据库层面设置外键约束而是依靠应用层逻辑来维护。主要是考虑到未来可能的数据库分库分表虽然家庭场景很难用到以及提升写入性能。但代码中必须做好校验防止关联到不存在的ID。transaction_no交易号:VARCHAR(100)。用于记录支付宝、微信支付的订单号方便后续对账。允许为空因为现金交易就没有交易号。remark备注:VARCHAR(500)。给用户一个自由发挥的空间记录一些额外信息比如“请同事吃饭”、“购买儿童节礼物”。user_id: 这是一个预留字段。当前版本是单用户系统所有数据都默认属于一个虚拟用户。当未来需要扩展为多用户系统时这个字段就会派上用场通过它来隔离不同用户的数据。注意关于金额正负的设计。这是一个有争议但经过实践检验的设计。有的系统会用两个字段income和expense分别记录或者用一个type字段加一个正的amount字段。我选择“正负金额”方案是因为它在数据分析和统计时异常简洁。但务必在前端和后端都做好校验确保逻辑一致避免出现“正负颠倒”的Bug。3.3 维度表设计分类与支付方式bill_category分类表和payment_method支付方式表设计类似都包含了name、icon、type、sort_order、status等字段。type字段用于区分大类。对于分类1表示支出分类如餐饮、交通2表示收入分类如工资、理财。对于支付方式1表示仅用于支出如信用卡2表示仅用于收入如银行到账3表示通用如现金、支付宝。parent_id字段仅在分类表中支持了二级分类。比如“餐饮”是一级分类其下可以有“早餐”、“午餐”、“晚餐”等二级分类。这样既保持了灵活性又不会让分类体系过于扁平化。sort_order排序顺序和status启用/禁用是管理类数据的标配。sort_order可以让用户自定义分类或支付方式在下拉列表中的显示顺序status则允许临时隐藏某个选项而不删除数据。icon字段存储图标URL或图标字体类名。前端可以根据这个值渲染出对应的小图标让界面更加生动直观。例如餐饮分类用一个刀叉图标交通分类用一个汽车图标。4. 后端核心实现与业务逻辑拆解4.1 统一响应封装与全局异常处理一个好的API首先要有统一的“语言”。我定义了一个Result类作为所有接口的响应体模板包含code状态码、message提示信息、data数据和timestamp时间戳。这样前端处理响应时逻辑可以统一比如判断code 200才视为成功。全局异常处理通过RestControllerAdvice注解的GlobalExceptionHandler类实现。它会捕获控制器层抛出的各种异常比如SQLException、自定义的BusinessException等然后转换成格式统一的Result对象返回给前端。举个例子当查询一个不存在的账单ID时服务层会抛出BusinessException(账单不存在)异常处理器会捕获它并返回{“code”: 500, “message”: “账单不存在”, “data”: null}。这避免了将晦涩的服务器错误栈直接暴露给用户。// 简化版的统一响应体 Data public class ResultT implements Serializable { private Integer code; private String message; private T data; private Long timestamp System.currentTimeMillis(); public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } // 其他静态工厂方法... }4.2 账单服务的核心CRUD与缓存策略账单的增删改查是业务核心。MyBatis Plus在这里大显身手。我的BillMapper接口直接继承BaseMapperBill立刻就拥有了单表的基础CRUD方法。对于复杂的多条件分页查询我在BillService中是这样实现的Override public PageBillVO queryBillPage(BillQueryDTO queryDTO) { // 1. 构建查询条件 QueryWrapperBill queryWrapper new QueryWrapper(); if (StringUtils.isNotBlank(queryDTO.getPayee())) { queryWrapper.like(payee, queryDTO.getPayee()); } if (queryDTO.getType() ! null) { // 根据类型收入/支出筛选金额正负 if (queryDTO.getType() 1) { // 支出 queryWrapper.lt(amount, 0); } else if (queryDTO.getType() 2) { // 收入 queryWrapper.gt(amount, 0); } } if (queryDTO.getStartTime() ! null) { queryWrapper.ge(transaction_time, queryDTO.getStartTime()); } if (queryDTO.getEndTime() ! null) { queryWrapper.le(transaction_time, queryDTO.getEndTime()); } // 排序 queryWrapper.orderByDesc(transaction_time); // 2. 执行分页查询 PageBill page new Page(queryDTO.getPageNum(), queryDTO.getPageSize()); PageBill billPage billMapper.selectPage(page, queryWrapper); // 3. 转换为VO并返回 return convertToVOPage(billPage); }缓存策略是性能关键。对于支付方式和分类列表我使用Redis进行缓存。键的设计很有讲究比如payment_method:list:type:1。当后台管理页面增删改支付方式时我会直接删除payment_method:list:*这个模式的所有键保证下次查询时能获取到最新数据。这是一种简单的“写时失效”策略。对于统计接口如/api/bills/statistics由于其计算涉及全表或大量数据的聚合我采用了“定时刷新请求触发”的双重缓存。我会以“统计:年月”为Key如statistics:202405设置一个较短的过期时间如5分钟。当用户请求本月统计时先查缓存命中则返回未命中则查询数据库计算后存入缓存。任何新增、修改、删除账单的操作都会清除对应月份的统计缓存确保用户看到的数据在短时间内最多5分钟达到最终一致。4.3 复杂统计查询的SQL与Java实现统计功能是系统的价值所在。以“按分类统计支出”为例其SQL逻辑并不简单需要关联账单表和分类表并按分类进行分组求和。我在BillMapper.xml中是这样写的select idselectCategoryStatistics resultTypecom.family.bill.vo.CategoryStatVO SELECT c.id AS categoryId, c.name AS categoryName, c.type, SUM(b.amount) AS amount FROM bill b INNER JOIN bill_category c ON b.category_id c.id WHERE b.transaction_time BETWEEN #{startTime} AND #{endTime} GROUP BY c.id, c.name, c.type ORDER BY amount ASC -- 支出为负所以ASC让支出最多的负得最少排前面 /select这里有个细节因为支出金额是负数所以SUM(b.amount)对于支出分类来说结果是负值。前端展示时需要取绝对值并标明是支出。这个逻辑我放在BillService中进行处理将负值转换为正数并打上类型标记方便前端渲染。// 在Service层处理统计结果 ListCategoryStatVO list billMapper.selectCategoryStatistics(params); for (CategoryStatVO vo : list) { if (vo.getType() 1) { // 支出分类 // 金额本是负数取绝对值并标记为支出 vo.setDisplayAmount(vo.getAmount().abs()); vo.setDisplayType(支出); } else { // 收入分类金额本就是正数 vo.setDisplayType(收入); } }5. 前端工程化与组件化实践5.1 基于Vite和Vue 3的项目初始化与配置项目初始化使用npm create vuelatest这是一个官方脚手架。在选项里我取消了Pinia和Vitest手动选择了Router和ESLint。初始化完成后第一件事就是配置vite.config.js。我主要做了两处改动一是设置了server.port为3000与后端8090端口区分开二是配置了代理解决开发时的跨域问题。// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue import { resolve } from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : resolve(__dirname, src) // 设置路径别名 } }, server: { port: 3000, proxy: { // 开发环境代理配置 /api: { target: http://localhost:8090, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) // 实际后端接口没有/api前缀这里重写掉 } } } })这样配置后前端在开发时访问/api/billsVite开发服务器会自动将其代理到http://localhost:8090/bills完美解决跨域。生产环境则是通过Nginx反向代理来处理。5.2 请求拦截器封装与全局状态管理我在src/utils/request.js中封装了Axios实例并添加了请求和响应拦截器。import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: import.meta.env.VITE_APP_BASE_API, // 从环境变量读取 timeout: 10000 }) // 请求拦截器 service.interceptors.request.use( (config) { // 可以在这里统一添加token // const token localStorage.getItem(token) // if (token) { // config.headers[Authorization] Bearer ${token} // } return config }, (error) { return Promise.reject(error) } ) // 响应拦截器 service.interceptors.response.use( (response) { const res response.data // 根据后端统一的Result结构判断 if (res.code 200) { return res.data // 直接返回data字段 } else { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message || Error)) } }, (error) { ElMessage.error(error.message || 网络错误) return Promise.reject(error) } ) export default service这个封装让业务组件调用API时异常简洁。在src/api/bill.js中import request from /utils/request export function getBillList(params) { return request({ url: /bills, method: get, params }) }在组件中直接使用const { data } await getBillList(queryParams)即可拦截器会自动处理错误提示和数据结构解构。对于全局状态比如当前用户信息或主题设置我使用了Vue 3的provide/inject配合reactive。在App.vue中创建一个响应式对象并提供给所有子组件比引入Pinia更轻量。5.3 账单管理页面的组件化开发账单管理是核心页面我将其拆分为几个组件BillTable.vue: 展示账单数据的表格组件使用Element Plus的el-table并自定义了“金额”列的渲染负数显示为红色。BillFilter.vue: 查询条件表单组件包含日期范围选择器、分类/支付方式下拉框等。使用el-form进行表单绑定和校验。BillDialog.vue: 新增/编辑账单的弹窗组件。通过一个visible的prop控制显示内部表单根据传入的billData判断是新增还是编辑。它们之间的通信通过Props和Events完成。BillFilter组件在用户点击查询时通过$emit将查询条件对象传递给父页面。父页面接收到后调用getBillListAPI获取数据后再通过Props传递给BillTable组件。一个关键的细节是表单验证。对于账单金额我要求必须是非零数字。在BillDialog组件中我使用了Element Plus的表单验证规则const rules reactive({ payee: [{ required: true, message: 请输入收款方, trigger: blur }], amount: [ { required: true, message: 请输入金额, trigger: blur }, { validator: (rule, value, callback) { if (value 0) { callback(new Error(金额不能为0)) } else if (isNaN(Number(value))) { callback(new Error(金额必须为数字)) } else { callback() } }, trigger: blur } ], transactionTime: [{ required: true, message: 请选择交易时间, trigger: change }] })6. 系统部署与运维实战指南6.1 后端Spring Boot应用的生产部署开发环境用mvn spring-boot:run没问题但生产环境必须打包成可独立运行的JAR文件。命令是mvn clean package -DskipTests-DskipTests是为了跳过测试加快打包速度。打包后会在target目录下生成一个形如family-bill-backend-1.0.0.jar的文件。直接使用java -jar命令运行是最简单的方式但这只适合前台运行。生产环境我们需要让应用在后台运行并且能随系统重启而自启。我推荐使用systemdLinux系统来管理。首先创建一个服务文件/etc/systemd/system/family-bill.service[Unit] DescriptionFamily Bill Backend Service Afternetwork.target [Service] Typesimple Userwww-data # 建议使用非root用户 WorkingDirectory/opt/family-bill/backend ExecStart/usr/bin/java -Xms256m -Xmx512m -jar family-bill-backend-1.0.0.jar SuccessExitStatus143 TimeoutStopSec10 Restarton-failure RestartSec5 [Install] WantedBymulti-user.target这里有几个关键参数-Xms256m -Xmx512m: 设置JVM堆内存初始值和最大值。根据服务器内存情况调整512M对于初期足够。Userwww-data: 使用一个专门的系统用户来运行服务更安全。Restarton-failure: 当服务异常退出时自动重启。配置好后执行以下命令sudo systemctl daemon-reload sudo systemctl start family-bill sudo systemctl enable family-bill # 设置开机自启 sudo systemctl status family-bill # 查看状态6.2 前端静态资源与Nginx配置优化前端使用npm run build打包后会生成一个dist目录里面就是所有的静态资源HTML、JS、CSS、图片。我们需要一个Web服务器来托管它们Nginx是首选。将dist目录上传到服务器例如/var/www/family-bill。然后配置Nginx。一个基础的配置如下server { listen 80; server_name your-domain.com; # 你的域名或IP root /var/www/family-bill; index index.html; # 前端路由Vue Router支持避免404 location / { try_files $uri $uri/ /index.html; } # 反向代理后端API location /api/ { proxy_pass http://127.0.0.1:8090/; # 后端服务地址 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; } # 开启gzip压缩提升加载速度 gzip on; gzip_vary on; gzip_min_length 1024; gzip_types text/plain text/css text/xml text/javascript application/javascript application/xmlrss application/json; }try_files $uri $uri/ /index.html;这行配置是单页应用SPA的关键。它让Nginx在找不到对应文件时都返回index.html由Vue Router来处理前端路由从而避免刷新页面时出现404错误。6.3 数据库与Redis的安装、配置与备份MySQL安装与配置 在Ubuntu上可以使用apt install mysql-server。安装后运行mysql_secure_installation进行安全初始化设置root密码、移除匿名用户等。创建项目专用的数据库和用户CREATE DATABASE family_bill CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER bill_userlocalhost IDENTIFIED BY StrongPassword123!; GRANT ALL PRIVILEGES ON family_bill.* TO bill_userlocalhost; FLUSH PRIVILEGES;然后使用mysql -u bill_user -p family_bill schema.sql导入项目提供的SQL文件完成表结构初始化。Redis安装与配置 同样使用apt install redis-server。安装后编辑/etc/redis/redis.conf有几个建议的修改bind 127.0.0.1确保只监听本地如果后端和Redis在同一台服务器。requirepass your_redis_password设置一个强密码这是必须的安全措施。maxmemory 256mb根据服务器内存设置最大使用内存防止Redis耗尽内存。maxmemory-policy allkeys-lru内存满时的淘汰策略allkeys-lru是一个通用选择。修改后重启Redissudo systemctl restart redis-server。备份策略 对于家庭数据定期备份至关重要。数据库备份使用mysqldump命令定期如每天凌晨备份数据库。mysqldump -u bill_user -p family_bill /backup/family_bill_$(date %Y%m%d).sql可以将此命令加入crontab实现自动化。Redis备份Redis默认会定期将数据快照RDB文件保存到磁盘由save配置项控制。你也可以手动执行SAVE或BGSAVE命令。确保将RDB文件默认在/var/lib/redis/dump.rdb也纳入你的备份计划中。7. 开发中遇到的典型问题与解决方案7.1 前后端联调中的跨域与时间格式问题在开发初期前端localhost:3000调用后端localhost:8090接口时浏览器会报CORS跨域资源共享错误。解决方案有两种后端解决推荐在Spring Boot中通过CrossOrigin注解或全局配置类来允许前端域名的跨域请求。我采用了配置类的方式更灵活。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) // 生产环境应替换为具体前端地址 .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowCredentials(true) .maxAge(3600); } }前端代理开发环境如前所述在Vite配置中设置proxy。这是开发时最方便的方法。另一个常见问题是时间格式。后端Bill实体中transactionTime字段是LocalDateTime类型Jackson默认序列化成数组格式[2024,5,20,14,30,0]前端无法直接解析。解决办法是在后端返回的VO对象中将时间字段定义为String类型并使用JsonFormat注解指定格式。public class BillVO { // ... JsonFormat(pattern yyyy-MM-dd HH:mm:ss) private String transactionTime; // ... }同时前端提交表单时也要确保传给后端的时间字符串格式一致。7.2 MyBatis Plus分页查询与条件构造的坑使用MyBatis Plus的分页插件PaginationInnerInterceptor时必须将其配置到MybatisPlusInterceptor中否则分页不会生效。Configuration public class MyBatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }在使用QueryWrapper进行多条件动态查询时要注意空值判断。如果前端传来的查询条件某个字段为空我们不应该将其加入查询条件。我最初写的代码是queryWrapper.eq(type, queryDTO.getType())当type为null时生成的SQL会是WHERE type null这查不到任何结果因为SQL中null不能用判断。正确的做法是加上判空if (queryDTO.getType() ! null) { queryWrapper.eq(type, queryDTO.getType()); }7.3 Vue 3响应式数据更新与表格渲染的细节在Vue 3的script setup语法中使用reactive定义响应式对象时直接替换整个对象会导致响应性丢失。例如在获取账单列表后// 错误做法直接赋值会失去响应性 state.tableData response.data.records state.total response.data.total // 正确做法赋值给reactive对象的属性 state.tableData response.data.records state.total response.data.total // 或者如果state本身是reactive({ tableData: [], total: 0 })我的state是用reactive定义的所以直接给它的属性赋值是安全的。另一个问题是Element Plus表格在数据更新后有时视图不会立即刷新。这通常是因为Vue的更新是异步的。可以尝试使用nextTick确保DOM更新后再进行某些操作或者检查是否因为对象的引用未改变而导致Vue认为数据未变化。对于深层嵌套的对象可以考虑使用Vue.set在Vue 3中是set函数或者直接创建一个新对象来触发更新。7.4 Redis缓存穿透与雪崩的简易应对策略虽然家庭记账系统并发不高但了解这些概念有备无患。缓存穿透查询一个数据库中一定不存在的数据比如id-1的账单。请求会绕过缓存直接查数据库可能被恶意利用。解决方案对于查不到的数据也将其空值如null缓存起来并设置一个较短的过期时间如30秒。这样下次同样的请求就会命中缓存直接返回空值。缓存雪崩大量缓存数据在同一时间过期导致所有请求瞬间涌向数据库。解决方案给缓存数据的过期时间加上一个随机值。例如原本都设置5分钟过期可以改为5分钟 随机(0-60秒)让缓存失效时间点分散开。在本项目中我对统计数据的缓存就采用了“基础过期时间随机偏移”的策略有效避免了潜在的雪崩风险。8. 项目总结与未来演进思考经过从零到一的开发和部署这个家庭记账系统已经稳定运行了一段时间。回过头看技术选型上Spring Boot Vue 3的组合非常高效MyBatis Plus和Element Plus极大地提升了开发速度。将支出设计为负数这个决定在后续做统计报表时被证明是明智的简化了大量计算逻辑。几个让我印象深刻的点缓存不是银弹引入Redis确实大幅提升了查询性能尤其是统计接口。但缓存的管理失效、更新带来了额外的复杂度。一定要想清楚哪些数据适合缓存以及更新策略。前后端约定大于沟通定义好统一的API响应格式Result、时间格式、错误码能节省大量联调时间。Swagger文档在这过程中起到了关键作用。预留扩展字段在bill表中预留user_id字段为后续的多用户扩展留了后路。在软件设计中适当的前瞻性很有必要。关于后续的优化方向我个人的想法是分步走首要任务是多用户与认证引入Spring Security JWT实现用户注册、登录。这是系统从“玩具”走向“工具”的关键一步。数据库表需要增加用户表并且所有查询都要加上user_id过滤条件。数据可视化集成ECharts将月度支出趋势、分类占比用饼图、折线图展示出来比单纯看数字直观得多。预算管理这是一个很实用的功能。可以设置月度总预算或分类预算当支出接近或超出预算时系统给出提醒。移动端适配目前前端主要是PC端管理后台。可以考虑用Uni-App或Taro重构一套移动端H5方便随时随地记账。这个项目的代码已经开源地址是damon-liu/ai_code_family_bill。它不仅仅是一个记账工具更是一个展示了现代Web全栈开发常用技术和设计模式的完整案例。无论是想学习技术还是需要一套家庭财务管理方案相信它都能给你带来启发。如果在搭建或使用过程中遇到任何问题欢迎在项目仓库中交流讨论。