尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于SpringBoot的家庭财务管理系统:从设计到部署的完整实战

基于SpringBoot的家庭财务管理系统:从设计到部署的完整实战 简介这是一份面向计算机专业本科生的毕业设计级家庭财务管理系统完整交付包基于SpringBoot框架构建解决个人或家庭日常收支记录、统计与可视化管理需求适合作为课程设计、毕设选题及Java全栈开发能力训练项目。压缩包共807个文件涵盖300余张界面GIF动图、108个前端JS交互脚本、89个XML配置与映射文件、85个编译后Class字节码、56个HTML页面模板、28个CSS样式文件及核心SQL建表语句等完整呈现前后端分离架构下的开发成果总大小22.64MB。已有1186人学习下载资源包含可直接运行的IDEA工程含MySQL数据库脚本、Tomcat9部署配置、E-R图与数据流图等设计文档、以及结构清晰的论文.docx——从需求分析、系统设计到测试结论全部覆盖特别适合需要快速上手、理解MVC分层实现与财务类业务逻辑的学生开发者。1. 项目概述为什么我们需要一个家庭财务管理系统最近几年我身边越来越多的朋友开始跟我聊起“钱”的事儿。不是聊怎么赚大钱而是聊钱都花哪儿去了。每个月工资一到手感觉还没怎么花月底一看余额就所剩无几。信用卡账单、花呗、各种会员订阅钱像流水一样悄无声息地溜走。这种对财务状况的“失控感”我相信很多人都有。你可能试过用Excel记账但坚持不了几天就放弃了也用过一些手机App但总觉得功能要么太简单要么太复杂数据还不在自己手里不太放心。这正是我决定动手开发一个“家庭财务管理系统”的初衷。它不是一个简单的记账工具而是一个集收入、支出、预算、资产、负债、报表分析于一体的综合性管理平台。更重要的是它基于SpringBoot开发部署在自己信任的服务器上数据完全私有功能可以按自家需求深度定制。想象一下你能清晰地看到家庭资产的健康度能预测下个月的现金流能为孩子的教育基金或一次长途旅行制定科学的储蓄计划这种掌控感是任何现成软件都给不了的。这个系统适合所有对家庭财务状况有梳理、规划和优化需求的家庭。无论你是技术爱好者想自己部署一套还是Java开发者想学习一个完整的SpringBoot实战项目它都能提供从技术到业务层面的完整参考。接下来我就把这个项目的设计思路、技术实现细节以及我踩过的坑毫无保留地分享给你。2. 系统核心功能与架构设计2.1 功能模块全景图一个完整的家庭财务管理系统远不止“记一笔账”那么简单。它需要围绕“财务全景”来构建。我将核心功能拆解为以下六大模块账户与流水管理这是系统的基石。你需要管理各类资金账户如现金、储蓄卡、信用卡、支付宝、微信钱包、投资账户等。每一笔资金的流入工资、理财收益、报销和流出消费、还款、投资都需要被清晰记录并关联到具体的账户实时计算账户余额。预算与控制预算是财务健康的“方向盘”。系统支持按周期月、年、按类别餐饮、交通、娱乐设置预算。在记录支出时能实时对比预算执行情况当支出接近或超出预算时给出预警。资产与负债总览这是你的“资产负债表”。资产端包括所有账户的现金总额、投资的市值、房产车辆的估值需手动更新负债端包括房贷、车贷、信用卡欠款等。系统自动计算家庭净资产资产-负债让你对家庭财富有整体认知。统计分析与报表数据只有被分析才有价值。系统需要提供多维度报表月度收支趋势图、消费类别占比饼图、账户余额变化曲线、预算与实际对比柱状图等。可视化报表能直观揭示消费习惯和财务问题。多成员与权限家庭财务往往是夫妻共同管理。系统支持创建家庭成员账号并分配权限。例如可以设置只有“管理员”才能修改预算或删除流水而“普通成员”只能查看和记录流水。数据备份与安全财务数据极其敏感。系统必须提供数据导出Excel、CSV功能并鼓励定期备份。同时从登录认证到数据传输存储都需要有基本的安全考量。2.2 技术架构选型为什么是SpringBoot面对这样一个前后端交互复杂、业务逻辑清晰的中小型项目SpringBoot几乎是毋庸置疑的最优解。它解决了传统Spring项目“配置地狱”的问题让我们能专注于业务开发。后端Backend:核心框架SpringBoot 2.7.x选择此版本是因为它长期支持且生态稳定避免了最新版可能存在的未知坑。它整合了Spring MVC、Spring Data JPA、Spring Security等开箱即用。数据持久层Spring Data JPA Hibernate。为什么不用MyBatis对于家庭财务这种领域模型相对固定的系统JPA的ORM能力能极大提升开发效率基础的CRUD几乎不用写SQL。复杂的统计查询我们通过JPA的Query注解写JPQL或原生SQL也能轻松应对。数据库MySQL 8.0。关系型数据库对交易流水、账户关系这类结构化数据的管理得天独厚事务支持也完善。考虑到数据量不会特别大MySQL完全够用且运维简单。安全框架Spring Security。用它来处理用户登录、注销、权限验证如区分管理员和成员再合适不过。结合JWTJSON Web Token实现无状态的API认证便于后续扩展移动端。缓存Spring Boot Cache Redis。将一些频繁访问且变化不大的数据缓存起来比如用户信息、枚举类字典消费分类、首页的聚合统计结果能显著减轻数据库压力提升响应速度。任务调度Spring Scheduler。用于执行一些定时任务比如每月1号自动生成上月的财务报告摘要并邮件推送或者每天凌晨核对账户余额是否与流水总和匹配。前端Frontend:考虑到这是一个个人或家庭内部使用的系统对界面美观度和跨平台性有要求但不需要极致的用户体验。我选择了Vue 3 Element Plus的组合。Vue框架上手快组件化开发效率高Element Plus提供了丰富的桌面端UI组件能快速搭建出清晰、易用的管理后台界面。前后端通过RESTful API进行分离部署灵活。部署与运维:传统部署可以直接打可执行JAR包用java -jar命令运行。更推荐使用Docker容器化部署。将应用、MySQL、Redis都容器化通过docker-compose.yml一键编排启动环境隔离迁移和备份都非常方便。这也是现代应用部署的最佳实践。这个技术栈组合在开发效率、性能、可维护性和学习成本之间取得了很好的平衡。下面我们就深入核心模块看看具体是怎么实现的。3. 核心数据库设计与领域模型数据库设计是系统的骨架设计得好后续开发事半功倍设计得差则举步维艰。我的核心设计围绕以下几个实体展开。3.1 核心表结构解析用户表 (sys_user):CREATE TABLE sys_user ( id bigint NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录用户名, password varchar(100) NOT NULL COMMENT 加密后的密码, nickname varchar(50) DEFAULT NULL COMMENT 用户昵称, family_id bigint DEFAULT NULL COMMENT 所属家庭ID, role varchar(20) DEFAULT MEMBER COMMENT 角色ADMIN-家长/管理员, MEMBER-成员, email varchar(100) DEFAULT NULL, avatar varchar(255) DEFAULT NULL COMMENT 头像, status tinyint DEFAULT 1 COMMENT 状态0-禁用1-正常, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB COMMENT系统用户表;设计心得将family_id作为外键是实现“多家庭”隔离的基础。一个家庭一个family_id所有财务数据都通过这个ID进行过滤实现数据隔离。role字段用于简单的权限控制。账户表 (fin_account):CREATE TABLE fin_account ( id bigint NOT NULL AUTO_INCREMENT, family_id bigint NOT NULL, name varchar(50) NOT NULL COMMENT 账户名称如“招商银行工资卡”, type varchar(20) NOT NULL COMMENT 账户类型CASH-现金, DEBIT_CARD-借记卡, CREDIT_CARD-信用卡, DIGITAL-数字钱包(支付宝/微信), INVESTMENT-投资账户, OTHER-其他, balance decimal(15,2) NOT NULL DEFAULT 0.00 COMMENT 当前余额, initial_balance decimal(15,2) DEFAULT 0.00 COMMENT 初始余额, currency varchar(10) DEFAULT CNY COMMENT 货币, include_in_total tinyint DEFAULT 1 COMMENT 是否计入总资产0-否1-是, remarks varchar(255) DEFAULT NULL COMMENT 备注, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_family (family_id) ) ENGINEInnoDB COMMENT资金账户表;踩坑记录balance余额字段的更新是核心也是易错点。绝对不能在应用层简单地newBalance oldBalance amount然后更新在高并发下虽然家庭系统并发低但好习惯要养成会产生更新丢失。正确做法是在记录流水fin_transaction时使用一条SQL原子性地更新账户余额UPDATE fin_account SET balance balance ? WHERE id ?。include_in_total字段很实用比如你可能有某个不常用的银行卡不想让它影响总资产视图就可以将其排除。交易流水表 (fin_transaction)—— 这是最核心的表:CREATE TABLE fin_transaction ( id bigint NOT NULL AUTO_INCREMENT, family_id bigint NOT NULL, transaction_time datetime NOT NULL COMMENT 交易时间, type varchar(10) NOT NULL COMMENT 交易类型INCOME-收入, EXPENSE-支出, TRANSFER-转账, amount decimal(15,2) NOT NULL COMMENT 交易金额永远为正数, category_id bigint NOT NULL COMMENT 分类ID, from_account_id bigint DEFAULT NULL COMMENT 支出/转出账户ID, to_account_id bigint DEFAULT NULL COMMENT 收入/转入账户ID, description varchar(500) DEFAULT NULL COMMENT 描述/备注, tags varchar(255) DEFAULT NULL COMMENT 标签多个用逗号分隔如“聚餐,朋友”, attachment varchar(255) DEFAULT NULL COMMENT 附件如发票照片路径, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_family_time (family_id,transaction_time), KEY idx_account (from_account_id), KEY idx_category (category_id) ) ENGINEInnoDB COMMENT交易流水表;核心逻辑解析amount字段永远存储正数。这样在统计和逻辑判断时更清晰。类型(type)和账户ID共同决定资金流向。收入typeINCOME,to_account_id不为空钱进入哪个账户from_account_id为空。支出typeEXPENSE,from_account_id不为空钱从哪个账户出to_account_id为空。转账typeTRANSFER,from_account_id和to_account_id都不为空。这里有一个关键点转账本质上是一笔支出和一笔收入的组合但为了不重复计算在统计净收支时转账交易应该被排除。我们只在报表中展示“转账”这个特殊类型或者将其过滤掉。tags字段用于灵活标记比固定分类更细粒度方便后期筛选如“双十一”、“医疗报销”等。分类表 (fin_category)和预算表 (fin_budget): 分类表是树形结构支持多级分类如支出-餐饮-早餐。预算表则关联family_id、category_id、period月度/年度和amount预算金额。3.2 JPA实体映射与关联关系在Spring Data JPA中我们将上述表结构映射为实体类。这里以Transaction交易流水实体为例展示如何设计关联关系。Entity Table(name fin_transaction) Data EqualsAndHashCode(callSuper false) public class Transaction extends BaseEntity { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(nullable false) private Long familyId; // 家庭ID用于数据隔离 Column(nullable false) private LocalDateTime transactionTime; // 交易时间 Enumerated(EnumType.STRING) Column(nullable false, length 10) private TransactionType type; // 枚举INCOME, EXPENSE, TRANSFER Column(nullable false, precision 15, scale 2) private BigDecimal amount; ManyToOne(fetch FetchType.LAZY) // 多对一懒加载 JoinColumn(name category_id, nullable false) private Category category; // 关联分类 ManyToOne(fetch FetchType.LAZY) JoinColumn(name from_account_id) private Account fromAccount; // 关联转出账户 ManyToOne(fetch FetchType.LAZY) JoinColumn(name to_account_id) private Account toAccount; // 关联转入账户 private String description; private String tags; // 业务逻辑方法获取交易对手账户用于前端显示 public Account getCounterAccount() { if (type TransactionType.INCOME) { return toAccount; } else if (type TransactionType.EXPENSE) { return fromAccount; } else { // TRANSFER // 转账时通常显示“从A转到B” return null; // 或者根据需要返回一个包含双方信息的对象 } } }注意事项所有关联关系都使用FetchType.LAZY懒加载。这是JPA性能优化的黄金法则。只有在需要用到关联对象数据时比如在Thymeleaf模板或Jackson序列化时才会触发额外的查询。避免在查询列表时一次性拉取所有关联数据导致“N1查询问题”。对于familyId这种用于过滤的字段我直接使用了基本类型而非关联因为它的查询频率极高且不需要Family实体的其他信息这样效率更高。4. 关键业务逻辑与Service层实现领域模型建立好后核心业务逻辑集中在Service层。这里我挑两个最复杂也最重要的服务来讲交易记录服务和报表统计服务。4.1 交易记录服务确保数据一致性记录一笔交易不仅仅是向fin_transaction表插入一条数据那么简单。它必须是一个原子性操作至少包含以下步骤验证参数账户是否存在、余额是否充足等。在fin_transaction表中插入流水记录。更新相关账户的balance字段。这必须在一个数据库事务中完成否则可能出现流水记了但余额没变或者反之的脏数据。Spring的Transactional注解完美解决了这个问题。Service Slf4j public class TransactionService { Autowired private TransactionRepository transactionRepo; Autowired private AccountRepository accountRepo; Autowired private PlatformTransactionManager transactionManager; /** * 记录一笔支出 * param dto 前端传入的数据传输对象 */ Transactional(rollbackFor Exception.class) // 声明式事务管理 public void recordExpense(TransactionDTO dto) { // 1. 参数校验 Account fromAccount accountRepo.findByIdAndFamilyId(dto.getFromAccountId(), getCurrentFamilyId()) .orElseThrow(() - new BusinessException(支出账户不存在或无权访问)); if (fromAccount.getBalance().compareTo(dto.getAmount()) 0) { throw new BusinessException(账户余额不足); } // 2. 创建流水实体 Transaction transaction new Transaction(); // ... 属性填充 (使用MapStruct或BeanUtils进行DTO-Entity转换更好) transaction.setType(TransactionType.EXPENSE); transaction.setAmount(dto.getAmount()); transaction.setFromAccount(fromAccount); // ... 设置其他字段 // 3. 保存流水此时还未提交事务 transactionRepo.save(transaction); // 4. 原子性更新账户余额使用乐观锁或直接计算 int updatedRows accountRepo.decreaseBalance(fromAccount.getId(), dto.getAmount()); if (updatedRows ! 1) { // 更新失败抛出异常触发回滚 throw new RuntimeException(更新账户余额失败可能并发修改); } // 5. 事务提交所有操作生效 log.info(记录支出成功流水ID: {}, transaction.getId()); } /** * 记录一笔转账涉及两个账户 */ Transactional(rollbackFor Exception.class) public void recordTransfer(TransactionDTO dto) { // 校验转出账户和转入账户 Account fromAcc accountRepo.findByIdAndFamilyId(dto.getFromAccountId(), getCurrentFamilyId())...; Account toAcc accountRepo.findByIdAndFamilyId(dto.getToAccountId(), getCurrentFamilyId())...; if (fromAcc.getId().equals(toAcc.getId())) { throw new BusinessException(转出账户和转入账户不能相同); } if (fromAcc.getBalance().compareTo(dto.getAmount()) 0) { throw new BusinessException(转出账户余额不足); } // 创建转账流水 Transaction transaction new Transaction(); transaction.setType(TransactionType.TRANSFER); transaction.setAmount(dto.getAmount()); transaction.setFromAccount(fromAcc); transaction.setToAccount(toAcc); // ... transactionRepo.save(transaction); // 关键同时更新两个账户的余额 int outUpdated accountRepo.decreaseBalance(fromAcc.getId(), dto.getAmount()); int inUpdated accountRepo.increaseBalance(toAcc.getId(), dto.getAmount()); if (outUpdated ! 1 || inUpdated ! 1) { throw new RuntimeException(转账过程中更新账户余额失败); } log.info(转账成功从[{}]到[{}]金额: {}, fromAcc.getName(), toAcc.getName(), dto.getAmount()); } }在AccountRepository中我们定义原子更新的方法Repository public interface AccountRepository extends JpaRepositoryAccount, Long { Modifying Query(UPDATE Account a SET a.balance a.balance - :amount WHERE a.id :id AND a.familyId :familyId) int decreaseBalance(Param(id) Long id, Param(amount) BigDecimal amount, Param(familyId) Long familyId); Modifying Query(UPDATE Account a SET a.balance a.balance :amount WHERE a.id :id AND a.familyId :familyId) int increaseBalance(Param(id) Long id, Param(amount) BigDecimal amount, Param(familyId) Long familyId); }实操心得这里我使用了Query配合Modifying来执行更新操作并返回受影响的行数。在Service层判断返回值是否为1是一种简单的乐观锁机制可以防止在极少数并发情况下的数据不一致。虽然家庭系统并发极低但养成这样的编程习惯对开发大型系统至关重要。4.2 报表统计服务复杂查询与性能优化报表统计是系统的价值所在但也是最容易产生性能瓶颈的地方。例如“统计本年度每个月的收支情况”这个需求如果处理不当会对数据库造成巨大压力。错误做法在Java代码中循环查询每个月的数据。正确做法尽量利用数据库的聚合计算能力一条SQL搞定。Service public class ReportService { Autowired private TransactionRepository transactionRepo; /** * 获取月度收支趋势数据用于折线图 * param year 年份 * return 列表包含每个月收入和支出的总和 */ public ListMonthlyTrendVO getMonthlyTrend(int year, Long familyId) { // 使用原生SQL进行高效聚合查询 String sql SELECT DATE_FORMAT(t.transaction_time, %Y-%m) as month, SUM(CASE WHEN t.type INCOME THEN t.amount ELSE 0 END) as totalIncome, SUM(CASE WHEN t.type EXPENSE THEN t.amount ELSE 0 END) as totalExpense FROM fin_transaction t WHERE t.family_id :familyId AND YEAR(t.transaction_time) :year AND t.type IN (INCOME, EXPENSE) -- 排除转账 GROUP BY DATE_FORMAT(t.transaction_time, %Y-%m) ORDER BY month ; Query query entityManager.createNativeQuery(sql); query.setParameter(familyId, familyId); query.setParameter(year, year); ListObject[] resultList query.getResultList(); ListMonthlyTrendVO list new ArrayList(); for (Object[] row : resultList) { MonthlyTrendVO vo new MonthlyTrendVO(); vo.setMonth((String) row[0]); vo.setTotalIncome((BigDecimal) row[1]); vo.setTotalExpense((BigDecimal) row[2]); vo.setSurplus(vo.getTotalIncome().subtract(vo.getTotalExpense())); // 计算结余 list.add(vo); } // 补全没有数据的月份数据为0 return fillMissingMonths(list, year); } /** * 获取消费类别占比用于饼图 * 注意这里只统计支出(EXPENSE) */ public ListCategoryStatVO getCategoryStat(LocalDate startDate, LocalDate endDate, Long familyId) { // 使用JPQL关联Category实体更面向对象 String jpql SELECT new com.yourproject.vo.CategoryStatVO( c.name, SUM(t.amount) ) FROM Transaction t JOIN t.category c WHERE t.familyId :familyId AND t.type EXPENSE AND t.transactionTime BETWEEN :start AND :end GROUP BY c.id, c.name ORDER BY SUM(t.amount) DESC ; // ... 执行查询并返回 } }性能优化技巧为查询字段建立索引fin_transaction表上的(family_id, transaction_time)联合索引对按时间范围筛选的查询至关重要。(family_id, category_id)索引则对按分类统计有帮助。缓存聚合结果首页的月度趋势、年度汇总等数据变化频率不高每天最多变一次。我们可以使用Spring Cache将getMonthlyTrend方法的结果缓存到Redis中设置TTL为12小时或24小时。这样能极大提升首页加载速度。分页查询流水列表查询一定要支持分页避免一次性拉取成千上万条记录。使用DTO/VO投影在JPA查询中直接返回自定义的DTO或VO通过new com.example.XXXVO(...)而不是返回完整的Transaction实体及其懒加载关联能减少不必要的SQL查询和数据传输量。5. 前端交互与API设计要点前后端分离架构下清晰、安全的API设计是协作的桥梁。我采用RESTful风格设计API并使用JWT进行认证。5.1 RESTful API设计示例RestController RequestMapping(/api/transactions) public class TransactionController { Autowired private TransactionService transactionService; // 获取流水列表分页、筛选 GetMapping public PageResultTransactionVO getTransactions( RequestParam(required false) LocalDate startDate, RequestParam(required false) LocalDate endDate, RequestParam(required false) TransactionType type, RequestParam(required false) Long accountId, RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 20) Integer size) { // 构建查询条件调用Service Pageable pageable PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, transactionTime)); PageTransaction transactionPage transactionService.findByCriteria(..., pageable); // 转换为VO并返回 return PageResult.success(transactionPage.map(this::convertToVO)); } // 记录一笔支出 PostMapping(/expense) public ResultVoid recordExpense(Valid RequestBody ExpenseDTO dto) { // Valid 注解会自动进行参数校验依靠DTO上的NotNull, Min等注解 transactionService.recordExpense(dto); return Result.success(); } // 记录一笔收入 PostMapping(/income) public ResultVoid recordIncome(Valid RequestBody IncomeDTO dto) { ... } // 记录一笔转账 PostMapping(/transfer) public ResultVoid recordTransfer(Valid RequestBody TransferDTO dto) { ... } // 删除一笔流水需要谨慎通常建议做逻辑删除 DeleteMapping(/{id}) public ResultVoid deleteTransaction(PathVariable Long id) { transactionService.deleteById(id); return Result.success(); } }设计规范统一的返回结构所有API返回ResultT对象包含code、message、data字段。成功时code200失败时有明确的错误码。使用DTO进行数据传输ExpenseDTO、IncomeDTO等与前端表单对应只包含必要的字段与数据库实体Transaction解耦。参数校验在DTO字段上使用JSR-303注解如NotNull、DecimalMin(0.01)进行声明式校验结合Valid注解在Controller层自动触发。分页标准化列表查询统一使用page和size参数返回统一的分页结果对象PageResult。5.2 前端Vue组件关键实现前端使用Vue 3的Composition API和Element Plus。以“记录支出”的组件为例template el-dialog title记录支出 v-modeldialogVisible el-form :modelform :rulesrules refformRef label-width80px el-form-item label账户 propfromAccountId el-select v-modelform.fromAccountId placeholder请选择支出账户 el-option v-foracc in accountList :keyacc.id :label${acc.name} (余额: ${acc.balance}) :valueacc.id / /el-select /el-form-item el-form-item label分类 propcategoryId el-cascader v-modelform.categoryId :optionscategoryTree :props{ label: name, value: id, checkStrictly: true } placeholder请选择消费分类 / /el-form-item el-form-item label金额 propamount el-input-number v-modelform.amount :min0.01 :precision2 controls-positionright/ /el-form-item el-form-item label时间 proptransactionTime el-date-picker v-modelform.transactionTime typedatetime placeholder选择日期时间 value-formatYYYY-MM-DD HH:mm:ss / /el-form-item el-form-item label描述 propdescription el-input v-modelform.description typetextarea :rows2/ /el-form-item /el-form template #footer el-button clickdialogVisible false取消/el-button el-button typeprimary clicksubmitForm :loadingsubmitting确认/el-button /template /el-dialog /template script setup import { ref, reactive, computed } from vue; import { ElMessage } from element-plus; import { recordExpenseApi } from /api/transaction; const props defineProps({ // 传入账户列表和分类树数据 accountList: Array, categoryTree: Array }); const dialogVisible ref(false); const submitting ref(false); const formRef ref(); const form reactive({ fromAccountId: null, categoryId: null, amount: null, transactionTime: new Date(), // 默认当前时间 description: }); const rules { fromAccountId: [{ required: true, message: 请选择支出账户, trigger: blur }], categoryId: [{ required: true, message: 请选择分类, trigger: blur }], amount: [ { required: true, message: 请输入金额, trigger: blur }, { type: number, min: 0.01, message: 金额必须大于0, trigger: blur } ] }; const submitForm async () { if (!formRef.value) return; const valid await formRef.value.validate(); if (!valid) return; submitting.value true; try { await recordExpenseApi(form); ElMessage.success(记录成功); dialogVisible.value false; emit(success); // 通知父组件刷新列表 resetForm(); } catch (error) { ElMessage.error(error.message || 记录失败); } finally { submitting.value false; } }; // 打开对话框的方法 const open () { dialogVisible.value true; }; // 暴露方法给父组件 defineExpose({ open }); /script前端开发心得表单校验是用户体验的关键前端校验Element Plus表单规则可以快速给出反馈但后端校验SpringValid是数据安全的最后防线两者缺一不可。状态管理对于账户列表、分类树这种全局通用的数据可以使用PiniaVue的状态管理库进行集中管理和缓存避免在不同组件中重复请求。日期时间处理前后端传递时间时统一使用ISO 8601格式字符串如2023-10-27T14:30:00或时间戳。后端实体使用LocalDateTime前端使用dayjs或moment库处理能省去很多时区转换的麻烦。文件上传如果支持上传发票附件建议使用分片上传并提供进度提示。后端接口接收MultipartFile保存到本地目录或对象存储如MinIO在数据库中只保存文件路径。6. 部署、安全与后期维护实战6.1 使用Docker Compose一键部署将整个系统容器化部署是保证环境一致性和简化运维的最佳方式。下面是一个简化的docker-compose.yml文件version: 3.8 services: mysql: image: mysql:8.0 container_name: family-finance-mysql environment: MYSQL_ROOT_PASSWORD: your_strong_root_password MYSQL_DATABASE: family_finance MYSQL_USER: ff_user MYSQL_PASSWORD: your_strong_user_password volumes: - ./mysql-data:/var/lib/mysql # 数据持久化 - ./init.sql:/docker-entrypoint-initdb.d/init.sql # 初始化脚本可选 ports: - 3306:3306 networks: - ff-network restart: unless-stopped redis: image: redis:7-alpine container_name: family-finance-redis ports: - 6379:6379 volumes: - ./redis-data:/data networks: - ff-network restart: unless-stopped backend: build: ./backend # Dockerfile所在目录 container_name: family-finance-backend depends_on: - mysql - redis environment: - SPRING_PROFILES_ACTIVEdocker # 使用docker profile的配置 - DB_HOSTmysql - DB_PORT3306 - REDIS_HOSTredis ports: - 8080:8080 networks: - ff-network restart: unless-stopped frontend: build: ./frontend # Dockerfile所在目录 container_name: family-finance-frontend depends_on: - backend ports: - 80:80 networks: - ff-network restart: unless-stopped networks: ff-network: driver: bridge在backend目录的application-docker.yml配置文件中数据库和Redis的连接地址就使用服务名mysql,redisspring: datasource: url: jdbc:mysql://mysql:3306/family_finance?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: ff_user password: your_strong_user_password redis: host: redis port: 6379部署步骤在服务器上安装Docker和Docker Compose。将项目代码包含前后端Dockerfile和docker-compose.yml上传至服务器。执行docker-compose up -d所有服务就会在后台启动。通过docker-compose logs -f backend查看后端日志排查启动问题。6.2 安全加固措施清单家庭财务数据无小事即使系统不大安全底线也必须守住。密码安全用户密码在数据库里绝对不能明文存储使用Spring Security的BCryptPasswordEncoder进行强哈希加密。强制要求密码复杂度至少8位包含字母和数字。API安全所有API登录注册除外都必须通过JWT认证。在Spring Security配置中拦截请求。使用HTTPS协议部署防止数据在传输中被窃听。对敏感操作如删除流水、修改账户可考虑增加二次确认或操作密码。SQL注入与XSS防护使用JPA或MyBatis的参数化查询绝对不要用字符串拼接SQL。前端对用户输入进行转义后端在输出到HTML前也进行转义Thymeleaf默认已做。如果前端是Vue/React它们的数据绑定也默认有XSS防护。数据备份定期如每周使用mysqldump命令备份数据库。将备份文件同步到其他存储如另一台服务器、云存储。可以在docker-compose.yml中增加一个cron服务容器定时执行备份脚本。6.3 常见问题排查与优化记录在开发和维护过程中我遇到并解决了一些典型问题这里分享给你问题首页加载缓慢特别是打开年度报表时。排查打开数据库慢查询日志发现统计年度收支的SQL虽然用了索引但因为数据量积累几年流水GROUP BY和SUM操作依然耗时。解决缓存为ReportService.getMonthlyTrend等方法添加Cacheable注解将结果缓存到Redis有效期设为1天。首页加载速度从2秒提升到200毫秒内。预聚合对于更复杂的统计可以考虑在夜间通过定时任务将聚合结果计算好存入一张statistics_summary表前端直接查汇总表。问题偶尔出现“账户余额不准”的情况。排查检查日志发现是在记录一笔交易时系统异常导致事务回滚但前端已经提示用户“记录成功”因为捕获异常不彻底。解决事务边界清晰确保Transactional注解覆盖所有数据库写操作并且rollbackFor Exception.class。前端状态同步后端操作成功后向前端返回明确的成功信号。前端在收到信号后再更新本地状态如刷新账户列表。对于关键操作可以提供一个“刷新数据”的按钮。定期对账编写一个定时任务每天凌晨计算每个账户的“期初余额 所有流入 - 所有流出”并与当前balance字段对比。如果不一致记录错误日志并报警发邮件给自己。这是一个最终一致性保障。问题分类管理混乱用户自己创建了很多重复或无效的分类。解决提供一套系统默认分类模板如“食品酒水”、“居家物业”、“交通出行”等。用户首次使用时可以选择导入默认模板。同时在用户删除某个已有流水的分类时给予提示并提供“合并到其他分类”的功能避免数据断裂。这个项目从构思到上线我断断续续花了近两个月的时间。最大的体会是开发一个给自己用的系统驱动力和细心程度会完全不同。你会不自觉地思考更多细节怎么录入最快报表怎么看最直观数据错了怎么方便地修改这些思考最终都沉淀成了上面的设计和代码。技术本身SpringBoot, Vue, Docker只是工具真正有价值的是你通过它们解决实际问题的过程。如果你正打算管理自己的家庭财务或者想找一个有完整业务逻辑的SpringBoot项目来练手希望这篇长文能给你提供一个扎实的起点和清晰的路线图。本文还有配套的精品资源点击获取
返回列表