Spring Boot+Vue3家装预算系统开发实战:从数据库设计到部署
做技术这么多年接的项目形形色色“家装预算系统”这个名字乍一听不算起眼但真正把一个装修预算的需求从数据库建模做到接口落地你会发现里面细节非常多。它表面是个管理系统骨子里却带着财务核算和工程项目管理的双重基因难度其实被低估了。这个系统我最终选用 Spring Boot 作为后端基础框架配 MySQL 存储、MyBatis-Plus 做数据访问前端用 Vue3 搭了一套简单后台整体是一个很典型的前后端分离的中型管理项目。写完之后复盘我觉得这个系统最大的价值在于业务闭环做得很完整预算编制、支出登记、余额统计、超支预警是一条完整链路每一步都有真实业务含义不像很多练手项目只有枯燥的增删改查。所以这篇博客我打算把从需求拆解、表结构设计、核心接口实现到部署上线的完整过程都捋一遍顺便把实操中踩过的坑一并记录下来。如果你正在找 Spring Boot 练手项目或者准备做家装预算相关的毕业设计这篇文章应该能帮你省下不少试错时间。1. 项目需求与核心痛点拆解1.1 家装预算到底难在什么地方装修过房子的人都知道预算超支几乎是必然事件。开工前拿着报价单觉得钱够用开工后才发现水电改造要加钱、瓷砖要补货、门套要额外打胶每一笔都不大加起来却像无底洞。市面上的记账软件能解决“记流水账”的问题但解决不了“这笔钱该不该花”“这个科目的预算还剩多少”这类管控问题。所以家装预算系统不是简单的记账本它本质上是一个以预算科目为维度的成本控制系统。核心需求有三条第一能把装修总预算按照硬装、软装、设备几大类拆解到具体科目第二每发生一笔支出能关联到对应科目实时刷新剩余额度第三当某科目花费接近预算上限时系统要提前发出预警而不是等钱花完了才马后炮。这里有个容易踩的坑很多人一开始把系统设计成“记账软件”只想着记录支出流水。但真实业务里预算科目本身是分层的比如“客厅装修”下面有“地面材料”“墙面涂料”“吊顶施工”如果科目表不设计成树形结构后面做统计报表的时候会非常痛苦。我在做需求梳理时把科目层级作为第一优先级这和普通记账软件就拉开了差距。1.2 用户角色与功能边界划分系统面向的用户不是单一角色家里装修时真正会用这个系统的人至少有三类业主本人要录入预算和支出工长或装修公司要申报费用可能还有一个“管家”负责审批和总控。所以在设计权限模型时我采用了相对轻量的三角色方案角色核心权限典型操作业主全部权限编制预算、登记支出、查看报表、审批超支申请工长受限权限提交支出申请、上传票据、查看自己负责的科目进度管理员系统维护用户管理、基础材料库维护、操作日志查看从这个权限划分来看系统至少包含用户认证、预算管理、支出管理、统计报表、基础数据维护这五个模块。实际开发中我并没有一开始就把所有模块的代码写完而是按“先会计科目、再预算编制、再支出流水、最后报表”的顺序迭代推进确保每一步都有可交付的演示版本。功能边界这块我还做了个减法不做装修进度管理不做材料库存管理也不做供应商比价。原因是这些功能会显著扩大项目范围但对“控制预算”这个核心目标贡献不大。产品学里有个说法叫“MVP”放到个人项目里同样适用——先把预算闭环跑通其他都是锦上添花。2. 技术选型与工程结构设计2.1 为什么选 Spring Boot 和这套技术组合选型这事儿我比较务实。Spring Boot 作为后端框架的理由不用多说它的自动配置、内嵌容器、生态成熟度在中小型系统里几乎没有对手。但具体到版本我建议新项目直接用 Spring Boot 2.7.x而不是最新的 3.x。原因很现实很多第三方 starter 和网上流传的配置方案在 2.7 下验证过的数量远多于 3.x而且 JDK 8 用户直接跑 2.7 版本最省心。我之前用一个 3.x 版本号做集成时光一个 数据库驱动 的兼容问题就折腾了半天这在 2.7 下根本不会发生。数据访问层我选了 MyBatis-Plus没有用 Spring Data JPA。做这类管理系统的同学应该都懂MyBatis-Plus 对于“单表复杂查询、多表关联查询、分页、逻辑删除”的支持非常顺手代码生成器还能一键产出实体和 Mapper开发效率明显更高。JPA 虽然写简单 CRUD 很爽但碰到预算统计这种多表聚合查询调试 SQL 的体验不如 MyBatis 直观。说句大实话国内多数公司后端项目里 MyBatis 系的使用率就是碾压级从就业角度也该选它。前端技术栈选择了 Vue3 Element Plus这是我个人比较推荐的后台管理搭配。Vue3 的组合式 API 写起来比 Options API 清爽Element Plus 的表单、表格、树形控件直接覆盖了预算科目管理和报表展示的需求。整个项目我做了前后端分离前端通过 Axios 调用后端 RESTful 接口部署时把前端构建产物放到 Nginx 下后端打包成 Jar 独立运行。2.2 后端工程目录结构与分层约定工程结构我倾向于按业务模块分包而不是按技术分层包。常见的 Controller / Service / Mapper 三层分包方式小项目用起来挺顺手但业务一旦变复杂所有代码堆在几个包里会越来越乱。我采用的目录结构大致是这样com.example.budget ├── common // 通用统一返回体、异常处理、全局配置 ├── security // 认证授权JWT过滤器、登录处理 ├── module │ ├── user // 用户模块 │ ├── category // 预算科目模块 │ ├── plan // 预算计划模块 │ ├── expense // 支出记录模块 │ └── report // 统计报表模块 ├── config // 跨域、MyBatis-Plus分页插件等配置 └── utils // 工具类每个 module 内部再拆 entity、mapper、service、controller 四个子包。有人说这样分包比按层分包“绕”但项目做到后期你会发现按业务模块划分以后每改动一个功能只需要在一个模块里操作不用在十几个包里来回跳维护成本低很多。Controller 层我统一封了一个返回体ResultT里面放 code、message、data 三个字段。接口错误通过业务异常抛出由全局异常处理器统一转换成标准返回体。这样前端拿到响应后只需要判断 code 是否为 200处理逻辑非常规整。前端部分我用的 Vite 构建目录划分是 views、api、components、router、store。所有后端接口地址集中在 api 目录里按后端模块拆文件例如plan.js、expense.js后面排错时能很快定位到具体接口。3. 核心数据模型与数据库设计3.1 预算科目表用树形结构支撑多维统计预算科目是整套系统的主心骨。装修预算天然具备层级结构我拿常见的分类举例一级科目分为硬装、软装、设备设施硬装下面又分拆改工程、水电工程、泥瓦工程、木工工程、油漆工程水电工程下面可能还会细化到强弱电布线、水管铺设、防水处理。数据库表budget_category我用自关联来做树形结构字段如下字段名类型说明idbigint主键parent_idbigint父级科目ID一级科目该值为0category_namevarchar(50)科目名称category_codevarchar(20)科目编码方便排序和统计category_typetinyint1-硬装 2-软装 3-设备sort_orderint同级排序statustinyint正常/停用create_timedatetime创建时间设计科目表时我特意加了一个category_code字段。一开始觉得有个主键就够了代码里直接按 id 关联即可。但后来做报表统计时发现如果科目编码能按层级规则编排比如“YJ01”代表一级硬装“YJ0101”代表硬装下的水电工程那么很多汇总统计可以直接用 SQL 模糊匹配LIKE YJ01%完成不用在代码里递归遍历科目树。这个字段对统计接口的简化效果超出预期。3.2 预算计划表与支出流水表的关联设计预算计划表budget_plan记录每个科目在某段时间内的预算金额。为什么不让科目表直接带上预算金额字段因为预算可能经常调整如果把金额和科目绑死在同一个表里每次调预算都要去 update 科目记录而且调预算的历史记录会丢失。budget_plan表设计如下字段名类型说明idbigint主键category_idbigint科目IDplan_amountdecimal(12,2)计划预算金额start_datedate预算周期开始日end_datedate预算周期结束日versionint乐观锁版本号create_timedatetime创建时间支出流水表expense_record则记录每一笔实际消费字段名类型说明idbigint主键plan_idbigint关联预算计划IDexpense_itemvarchar(100)支出项目名称amountdecimal(12,2)支出金额expense_datedate支出日期payee_namevarchar(50)收款方receipt_urlvarchar(200)票据图片地址expense_typetinyint材料/人工/其他remarkvarchar(255)备注create_bybigint录入人IDcreate_timedatetime录入时间把支出关联到plan_id而不是直接关联category_id是我后来调整的一个重要设计。如果只关联科目预算周期一变或者同一个科目有两个预算计划时支出流水就不知道对应哪份预算。关联plan_id以后一个科目可以有历史多期预算计划支出始终能找到归属的预算期统计口径不会乱。金额字段全部用decimal(12,2)别用 double 或 float这个属于老生常谈了。装修支出虽然单笔金额不大但累计统计时二进制浮点误差会让你对不上账钱这东西一分都不能差老老实实 BigDecimal。3.3 预算余额和超支比例的计算逻辑数据库里不存“剩余预算”或“已花金额”这类冗余字段这叫读时计算统计时临时查询汇总。原因是只要支出流水记录了金额已花金额任何时候都能算出来存冗余字段反而要担心数据同步不及时。核心查询就是按 plan_id 分组汇总SELECT plan_id, SUM(amount) AS used_amount FROM expense_record WHERE create_time BETWEEN #{startTime} AND #{endTime} GROUP BY plan_id;后端拿到plan_id对应的plan_amount以后通过BigDecimal做一次减法算出余额再做一次除法算出使用率。这种方法最直观也没有同步一致性问题。数据量几万条以内性能完全没有压力个人项目没必要上复杂的设计。4. 关键功能模块实现要点4.1 预算编制物料清单与人工费的混合录入预算编制的实际操作流程不是填一个总金额那么简单。装修预算往往要落到“项目—子项—材料/人工”的颗粒度比如“卫生间防水工程”下面会有防水涂料、水泥砂浆、人工费等多个明细项。我的做法是在预算计划下面提供两种录入方式一是直接录总金额适合粗略估算二是按明细录入每条明细相当于一个小的支出预估项。企业版可以直接把明细做成budget_plan_item子表但个人项目为了控制复杂度我把“明细预估”直接复用支出流水表用一个is_estimate字段区分预估和实际简化掉一张表。预算编制接口的入参大致长这样{ categoryId: 15, planAmount: 8500.00, startDate: 2025-03-01, endDate: 2025-04-30, items: [ { name: 防水涂料, estimatedAmount: 3200.00 }, { name: 瓦工人工, estimatedAmount: 5300.00 } ] }接口内部用事务包裹先插入budget_plan主记录再批量插入明细预估。为什么一定要开事务因为这两步操作如果没有原子性会出现预算计划创建成功但明细丢失的脏数据后面统计对不上账很难排查。4.2 支出登记与预算扣减的一致性处理支出登记是整个系统里最容易出 bug 的地方。业务要求是登记一笔支出后立即能看到该科目预算剩余额度变化。这里我采用了事务控制 校验前置的组合方案核心代码骨架如下Transactional(rollbackFor Exception.class) public void addExpense(ExpenseAddDTO dto) { // 1. 校验参数合法性 // 2. 查询预算计划判断是否存在且处于有效期内 BudgetPlan plan planMapper.selectById(dto.getPlanId()); if (plan null) { throw new BizException(预算计划不存在); } // 3. 汇总该计划当前已支出金额 BigDecimal usedAmount expenseMapper.sumAmountByPlanId(dto.getPlanId()); // 4. 计算剩余额度并做超支持校验 BigDecimal remain plan.getPlanAmount().subtract(usedAmount); if (remain.compareTo(dto.getAmount()) 0) { throw new BizException(超出剩余预算无法登记); } // 5. 插入支出记录 expenseMapper.insert(buildEntity(dto)); }代码本身不复杂但有几个容易忽略的细节。第一Transactional一定要加rollbackFor Exception.class否则运行时异常默认不触发回滚会出现支出记录插入成功但接口报错的问题。第二查询已支出金额放在插入之前且在同一事务内执行这样同一时间的并发请求不会互相干扰。第三如果需求允许“超支登记”这种场景需要把第 4 步改成可选校验由参数里的allowOverBudget字段控制。并发控制方面个人项目通常不会有太大并发量但为了防止两个人同时登记支出把预算“花超”两遍我在budget_plan表加了乐观锁字段version。更新预算计划时带上WHERE version #{oldVersion}更新影响行数为 0 时说明有人改过直接提示稍后重试。这是非常简单的乐观锁应用但能让系统的数据可靠性明显提升。4.3 超支预警阈值状态的计算与展示超支预警是系统里最有业务价值的功能。我的设计不是只做一个二元判断“是否超支”而是分三个状态正常、预警、超支。某科目预算使用率达到 80% 时进入预警状态使用率超过 100% 时进入超支状态。状态计算不需要单独存字段每次查询时通过字段计算比较得到public String calcBudgetStatus(BigDecimal planAmount, BigDecimal usedAmount) { if (planAmount null || planAmount.compareTo(BigDecimal.ZERO) 0) { return invalid; } BigDecimal ratio usedAmount .divide(planAmount, 4, RoundingMode.HALF_UP); if (ratio.compareTo(new BigDecimal(1.00)) 0) { return over; } if (ratio.compareTo(new BigDecimal(0.80)) 0) { return warning; } return normal; }阈值用常量类配置后续要改成 85% 预警只需要改常量不用动 SQL。前端拿到状态字段后在科目列表上用不同颜色的标签展示并支持只看“预警”和“超支”的筛选条件这样整个预算看板就非常有实操价值了。4.4 报表统计科目汇总与全屋总览报表模块我做了两个维度的展示。第一个维度是按科目维度汇总展示每个一级科目下的预算金额、已支出金额、余额和使用率用户可以逐层往下钻取到二级、三级科目。第二个维度是全屋总览用卡片展示总预算、总支出、总剩余、超支科目数量配合 ECharts 画一个饼图看各类别支出占比。SQL 层面做了一个按一级科目聚合统计的语句SELECT c.category_name, p.plan_amount, IFNULL(SUM(e.amount), 0) AS used_amount FROM budget_plan p LEFT JOIN budget_category c ON p.category_id c.id LEFT JOIN expense_record e ON e.plan_id p.id GROUP BY p.id, c.category_name;注意这里LEFT JOIN和IFNULL不能省否则没有支出的预算科目会因为 SUM 结果为 NULL 而统计错误。实际的聚合查询比这个示例复杂因为还需要处理科目树的层级汇总我的做法是先查出明细在内存里做树形汇总而不是用一串复杂的 SQL 嵌套。数据量几千条级别时内存计算速度和清晰度的优势非常明显。5. 部署上线与常见问题排查5.1 环境准备与部署方式开发环境下我用 Spring Boot 内置的 Tomcat 跑不需要单独装 Tomcat这也是 Spring Boot 最舒服的地方。但部署到生产环境时我的习惯是用 Maven 打包成可执行 Jar然后直接交给宝塔面板用java -jar启动前端构建产物放到 Nginx 静态目录通过反向代理把/api前缀的请求转发到后端 8080 端口。需要重点检查的配置有四块数据库连接线上环境务必加上useSSLfalseserverTimezoneAsia/Shanghai参数否则时区不对会导致日期字段差 8 小时。端口配置server.port不改就用默认 8080如果被占用就换一个。JVM 参数个人项目-Xms256m -Xmx512m完全够用不用贪大。日志配置至少把 error 级别日志单独输出到文件排查线上问题全靠它。部署时我还遇到一个坑前端页面能打开但所有接口都报 404。排查半天发现是 Nginx 反向代理的转发规则写错location /api/里的斜杠和代理地址之间的路径拼接逻辑没搞对。正确写法是把 /api 前缀替换成后端地址比如location /api/ { proxy_pass http://127.0.0.1:8080/; }proxy_pass末尾带斜杠和不带斜杠是完全不同的语义这个细节很容易翻车。5.2 实操中遇到的高频问题与排查清单做这个项目过程中攒了不少问题我挑几个最具代表性的列出来给后面做类似系统的人做个参考问题现象排查思路解决方案接口正常但前端拿不到数据检查跨域配置是否生效在后端统一配置 CORS允许前端开发服务器地址访问日期查询相差 8 小时数据库连接参数缺少时区设置配置serverTimezoneAsia/Shanghai并统一使用 LocalDateTime金额统计出现精度误差数据库字段或实体字段用了浮点数全部改用decimal BigDecimal严禁 double修改科目后预算汇总不对科目树缓存或递归逻辑错误改用category_code前缀匹配做汇总不依赖递归事务方法内调用不生效this方法自调用导致 AOP 无法代理将事务拆到独立 Service 方法通过注入对象调用有个特别隐蔽的问题要单独提一下用 MyBatis-Plus 分页时如果不配置分页拦截器分页查询不会真正执行LIMIT而是一次性查出全表再在内存里“假分页”。这个问题表面看不出异常数据量小的时候性能没区别数据量一上去接口就明显变慢。记得在配置类里注册PaginationInnerInterceptor这个坑非常经典。5.3 项目优化空间与后续扩展方向系统按预算闭环开发完以后能明显感觉到还有不少扩展方向可以继续做。比较有价值的有三个一是引入预算审批流支出超过某阈值时自动触发业主审批工长不能直接登记这能进一步强化成本管控二是增加票据 OCR 识别拍照自动读出金额和商户省去手动录入三是把预算模板做成公共库用户可以直接套用常见户型的基础预算模板比如三室一厅磨砺包、出租屋简装包录入成本大幅降低。这三个方向各对应一种技术难点审批流涉及状态机设计OCR 涉及图像处理和 NLP模板库涉及数据标准化。当前版本没有急切地全部实现因为任何项目都有一个规律核心闭环完整以后锦上添花的扩展功能优先级并不高先把稳定性和体验打磨好更重要。6. 个人经验与教训总结预算系统这个项目做完我自己最大的体会是——一个系统好不好用根本不在于技术多花哨而在于业务模型是否贴合真实场景。家装预算的核心不是“记多少钱”而是“管住别超支”。只有把科目层级、预算计划、支出流水、余额计算这条链路想清楚代码才不至于返工。其次要特别提醒大家注意 BigDecimal 和日期时区这两个看似琐碎的点。做非财务类项目时可能没有感觉但在和钱、时间打交道的地方一旦出错就是数据对不上账或者时间差八小时的诡异问题排查起来非常耗时间。写代码时坚持“金额一律 BigDecimal、时间一律 LocalDateTime、统一走配置类序列化”能在上游规避掉一大批下游故障。顺便提一下技术版本选择的教训新项目不要盲目追求最新版框架。Spring Boot 2.7.x 在我个人经验里比 3.x 更稳生态兼容性更好。虽然 3.x 已经推出很久但不少第三方库和教程依然停留在 2.x 语境下个人项目求稳比求新重要。真要用 3.x务必提前确认所有依赖版本都支持 Jakarta 命名空间别等代码写到一半才发现某组件不兼容。最后分享一个开发流程上的小技巧这类管理系统优先把核心闭环的接口写通再考虑附加功能。我当时是先做预算编制和支出登记用 Postman 把所有接口调通再开始写前端页面。前后端分离模式下这种方式能让你把业务逻辑彻底搞清楚后面即使界面调整也不会伤筋动骨。很多同学做项目喜欢并行推进前后端结果接口改了三次导致前端也改三次白白消耗大量时间。如果你正在做类似的 Spring Boot 管理系统或者打算把它改造成毕业设计我建议你先把本文提到的 5 张表建好用 Postman 把预算编制、支出登记、预算看板三个接口调通这个系统的骨架就立住七八成了。剩下的报表美化、权限细化、部署加固都是围绕骨架的增量工作。这个项目本身的技术门槛不算高真正拉开差距的是你能否把业务逻辑讲清楚以及踩过坑之后能不能沉淀出属于自己的解决方案。