
Procura – Finance Manager 开发实战从零打造一个可用的个人财务管理系统如果你有过记账的经历大概率会遇到这样一个问题手机记账 App 用了很多个从“鲨鱼记账”到“MoneyWiz”每个 App 都有几个让人抓狂的短板——数据没法导出、分类太死板、多账户之间的转账逻辑混乱、预算超支提醒形同虚设。最后很多人会回到那个最原始的工具Excel。但 Excel 记账的痛点是统计报表全靠手写公式每个月对账要花掉一个晚上。这篇文章要讲的 Procura – Finance Manager不是某个商业产品而是一个值得参考的个人财务管理系统实践项目。核心思路很简单自己掌控数据结构、自己定义分类规则、自己决定报表维度甚至可以把交易数据直接导出成 CSV 再交给专业工具做进一步分析。我会从系统设计、技术选型、数据库建模、后端接口、前端展示、部署验证这几个层面把这个项目的完整开发路径拆解给大家。无论你是正准备做自己的财务管理工具还是想在项目里加入预算、账单、报表这类模块这篇文章都能给你一套可落地的方案。文章的重点不在“这个系统有多牛”而在于一个真实的财务管理系统应该怎么设计数据模型、怎么处理交易流水、怎么做预算控制以及整个项目从开发到部署会遇到哪些坑。这是大多数教程不会细讲的部分。1. 为什么需要自建财务管理系统先说结论自建财务管理系统核心价值不是省钱而是数据自主权和规则可控性。商业记账软件的第一目标是让你持续使用所以它会把注册、登录、同步、会员体系做得非常重。但对一个开发者来说个人记账最重要的根本不是社交功能而是三件事账目记录要快要能批量导入历史数据收支统计要准要能看到任意时间段的分类汇总数据要能随时完整导出永远不会被厂商锁死。Procura – Finance Manager 的设计目标就是围绕这三点。它不是一个“记账 App”而是一个Finance Manager它更偏向“管理”管理账户余额、管理交易分类、管理预算计划、管理债务与应收应付。换句话说它更像是给个人使用的“轻量级 ERP”。从技术角度看做这样的系统还有一个好处它几乎覆盖了后端开发的全部基本功。你可以在这一个项目里练习 CRUD 设计、事务处理、聚合查询、定时任务、权限控制、报表统计甚至把它扩展到多用户和家庭共享场景。它是一个非常适合写入简历、也适合用来系统学习全栈开发的练手项目。2. Procura 的核心功能与整体架构一个完整的 Finance Manager核心功能模块应该包括五个部分功能模块核心职责典型操作账户管理维护现金、银行卡、信用卡、数字货币等多种账户新增账户、调整余额、账户归档交易管理记录收入和支出流水支持转账记一笔支出、导入银行账单、修改分类分类与标签自定义收支分类支持二级分类和标签新建“餐饮-午餐”分类给交易打标预算管理按月设置分类预算实时统计超支情况设置“餐饮预算 2000 元/月”查看剩余额度统计报表按日/月/年汇总收支生成分类占比和趋势查看本月支出分类饼图导出月度报表系统整体架构采用前后端分离。后端提供 REST API前端通过 HTTP 调用接口完成数据展示和操作。数据库使用 MySQL 这类关系型数据库因为财务流水天然适合用事务和关系表来管理。前端Vue3 Element Plus | | HTTP / JSON | 后端Spring Boot | | JPA / MyBatis | MySQL账户表、交易表、分类表、预算表这套架构的核心优势是后端接口独立未来可以接 iOS/Android 客户端也可以暴露只读统计接口给家庭共享前端独立部署可以随时换主题或改成服务端渲染。更重要的是所有财务数据都落在自己的数据库里备份和迁移都非常简单。3. 技术选型为什么选择 Spring Boot Vue3技术选型没有绝对最优只有适合场景的选型。Procura – Finance Manager 的后端选择 Spring Boot主要考虑三点第一生态成熟。Spring Boot 对事务、数据库访问、定时任务、参数校验的支持非常完善开发一个财务系统不需要重复造轮子。第二招聘市场需求量大国内 Java 技术栈的后端岗位仍然占主流用这个项目练手对求职有直接帮助。第三部署方便。Spring Boot 可以打成一个可执行 Jar配合 Docker 部署非常简单。前端选择 Vue3 Element Plus是因为 Vue3 的组合式 API 让组件逻辑更清晰Element Plus 的表单、表格、日期选择器非常适合做管理类界面。财务系统里大量的“列表 筛选 统计卡片”交互用 Element Plus 几乎不需要额外封装。数据库方面采用 MySQL原因很简单财务数据是强关系型数据。流水、账户、分类、预算之间都有关联查询需求使用关系型数据库能保证数据一致性和事务能力。预算扣减、转账这类操作必须保证原子性这是 NoSQL 数据库不擅长的场景。4. 环境准备与项目初始化开始写代码之前先把环境准备好。以下版本信息仅供参考实际操作时请以你本机安装的版本为准。4.1 后端环境依赖说明JDK 17Spring Boot 3.x 要求 JDK 17 及以上Maven 3.8依赖管理和构建工具MySQL 8.0主数据库IDEA 或 VS Code开发工具推荐 IDEA Community 版即可确认版本java -version mvn -version mysql --version4.2 前端环境依赖说明Node.js 18前端运行环境npm 或 pnpm包管理器Vue CLI 或 Vite项目脚手架创建前端项目npm create vitelatest procura-web -- --template vue cd procura-web npm install4.3 初始化数据库创建一个名为procura的数据库。CREATE DATABASE IF NOT EXISTS procura DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;这里使用utf8mb4字符集是为了保证中文分类名、备注等内容在存储和查询时不会出现乱码也能支持存储 Emoji 符号。5. 数据库建模财务系统的地基数据库设计是财务系统最关键的部分。设计不好后续写 SQL 聚合统计会非常痛苦。Procura 的核心表设计如下。5.1 账户表account账户代表资金存放的位置比如招商银行储蓄卡、支付宝余额、微信零钱、现金钱包。CREATE TABLE account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 账户名称如招行储蓄卡, type VARCHAR(32) NOT NULL COMMENT 账户类型CASH/BANK/CREDIT_CARD/DIGITAL, balance DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT 当前余额, currency VARCHAR(8) NOT NULL DEFAULT CNY, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-启用 0-停用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT账户表;balance字段使用DECIMAL(12,2)而不是FLOAT或DOUBLE。这是财务系统的基本常识浮点数在二进制环境下存在精度误差计算金额时必须使用定点数类型。5.2 交易表transaction交易表是整个系统最核心的表。它需要同时支持收入、支出、转账三种业务所以设计上要保证每条交易都关联账户并且转账会生成两条关联记录。CREATE TABLE transaction ( id BIGINT PRIMARY KEY AUTO_INCREMENT, account_id BIGINT NOT NULL COMMENT 账户ID, category_id BIGINT COMMENT 分类ID转账时可以为空, type VARCHAR(16) NOT NULL COMMENT INCOME/EXPENSE/TRANSFER, amount DECIMAL(12,2) NOT NULL COMMENT 金额正数, balance_after DECIMAL(12,2) COMMENT 交易后账户余额, remark VARCHAR(256) COMMENT 备注, transact_time DATETIME NOT NULL COMMENT 实际交易时间, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_account_time (account_id, transact_time), KEY idx_category_time (category_id, transact_time) ) ENGINEInnoDB COMMENT交易流水表;5.3 转账关联表transfer_pair两条交易流水之间的关联关系需要单独记录否则无法知道这笔转出对应哪笔转入。CREATE TABLE transfer_pair ( id BIGINT PRIMARY KEY AUTO_INCREMENT, from_transaction_id BIGINT NOT NULL COMMENT 转出流水ID, to_transaction_id BIGINT NOT NULL COMMENT 转入流水ID, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT转账关联表;5.4 预算表budget预算按“分类 月份”维度设置保证一个月内一个分类只能有一个预算金额。CREATE TABLE budget ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL, budget_month VARCHAR(7) NOT NULL COMMENT 预算月份格式 2025-01, amount DECIMAL(12,2) NOT NULL COMMENT 预算金额, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_category_month (category_id, budget_month) ) ENGINEInnoDB COMMENT预算表;5.5 收支分类表category分类表支持两级结构。一级分类表示大类二级分类表示子类。CREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT DEFAULT 0 COMMENT 0-一级分类否则父分类ID, name VARCHAR(32) NOT NULL, type VARCHAR(16) NOT NULL COMMENT INCOME/EXPENSE, sort_order INT DEFAULT 0 ) ENGINEInnoDB COMMENT收支分类表;这部分表结构覆盖了一个财务管理系统最核心的数据模型。在实际项目中还可以扩展账单日、信用卡还款提醒、定期存款利率等字段但上述五张表已经可以支撑一个完整的收支管理闭环。6. 后端核心代码实现6.1 创建 Spring Boot 项目通过 Spring Initializr 创建项目时需要添加以下依赖Spring WebSpring Data JPAMySQL DriverValidationLombok可选pom.xml中的关键依赖如下dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency6.2 配置文件application.ymlserver: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/procura?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: your_password_here driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true带your_password_here的配置是必改项实际使用时换成自己的数据库密码。ddl-auto: update只适合开发阶段生产环境更推荐使用 Flyway 管理数据库版本。6.3 账户实体与转账事务交易记录的核心难点是记录支出时要扣减账户余额记录收入时要增加账户余额转账时要同时扣减一个账户并增加另一个账户。这三类操作都必须放在同一个事务中执行。这里以转账接口为例展示核心代码。package com.example.procura.service; import com.example.procura.entity.Account; import com.example.procura.entity.Transaction; import com.example.procura.entity.TransferPair; import com.example.procura.repository.AccountRepository; import com.example.procura.repository.TransactionRepository; import com.example.procura.repository.TransferPairRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDateTime; Service public class TransferService { private final AccountRepository accountRepository; private final TransactionRepository transactionRepository; private final TransferPairRepository transferPairRepository; public TransferService(AccountRepository accountRepository, TransactionRepository transactionRepository, TransferPairRepository transferPairRepository) { this.accountRepository accountRepository; this.transactionRepository transactionRepository; this.transferPairRepository transferPairRepository; } Transactional(rollbackFor Exception.class) public void transfer(Long fromAccountId, Long toAccountId, BigDecimal amount, String remark) { if (amount.compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(转账金额必须大于0); } if (fromAccountId.equals(toAccountId)) { throw new IllegalArgumentException(转出账户和转入账户不能相同); } Account fromAccount accountRepository.findById(fromAccountId) .orElseThrow(() - new RuntimeException(转出账户不存在)); Account toAccount accountRepository.findById(toAccountId) .orElseThrow(() - new RuntimeException(转入账户不存在)); if (fromAccount.getBalance().compareTo(amount) 0) { throw new RuntimeException(转出账户余额不足); } LocalDateTime now LocalDateTime.now(); // 转出流水 Transaction fromTransaction new Transaction(); fromTransaction.setAccountId(fromAccountId); fromTransaction.setType(TRANSFER); fromTransaction.setAmount(amount); fromTransaction.setRemark(转出: remark); fromTransaction.setTransactTime(now); fromTransaction transactionRepository.save(fromTransaction); // 转入流水 Transaction toTransaction new Transaction(); toTransaction.setAccountId(toAccountId); toTransaction.setType(TRANSFER); toTransaction.setAmount(amount); toTransaction.setRemark(转入: remark); toTransaction.setTransactTime(now); toTransaction transactionRepository.save(toTransaction); // 更新余额 fromAccount.setBalance(fromAccount.getBalance().subtract(amount)); toAccount.setBalance(toAccount.getBalance().add(amount)); accountRepository.save(fromAccount); accountRepository.save(toAccount); // 关联两条流水 TransferPair pair new TransferPair(); pair.setFromTransactionId(fromTransaction.getId()); pair.setToTransactionId(toTransaction.getId()); transferPairRepository.save(pair); } }这段代码中真正重要的是Transactional(rollbackFor Exception.class)。这句注解保证如果“更新账户余额”这一步发生异常前面已经写入的交易流水也会一起回滚不会出现“流水记录了但余额没变”的脏数据。6.4 月度收支汇总查询财务系统最常见的报表是月度收支汇总SQL 需要按分类分组统计。package com.example.procura.dto; import java.math.BigDecimal; public interface MonthlyCategorySummary { Long getCategoryId(); String getCategoryName(); BigDecimal getTotalAmount(); Integer getTransactionCount(); }对应的 Repository 查询方法package com.example.procura.repository; import com.example.procura.dto.MonthlyCategorySummary; import com.example.procura.entity.Transaction; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import java.time.LocalDateTime; import java.util.List; public interface TransactionRepository extends JpaRepositoryTransaction, Long { Query( SELECT t.categoryId AS categoryId, c.name AS categoryName, SUM(t.amount) AS totalAmount, COUNT(t.id) AS transactionCount FROM Transaction t LEFT JOIN Category c ON c.id t.categoryId WHERE t.type :type AND t.transactTime BETWEEN :start AND :end GROUP BY t.categoryId, c.name ORDER BY totalAmount DESC ) ListMonthlyCategorySummary summaryByCategory( Param(type) String type, Param(start) LocalDateTime start, Param(end) LocalDateTime end); }注意LEFT JOIN的使用。如果交易记录没有分类例如转账记录仍然需要返回一条记录否则报表里的总收支金额会因分类为空而漏掉数据。7. 前端核心代码实现前端使用 Vue3 Element Plus 构建核心是交易列表页和报表统计页。7.1 交易列表页面交易列表是整个日常记账操作频率最高的页面。用户希望在这个页面完成三件事按时间范围筛选流水、查看账户余额变化、快速新增一笔收入或支出。template div classtransaction-page el-card classfilter-card el-form :inlinetrue :modelqueryForm el-form-item label时间范围 el-date-picker v-modelqueryForm.dateRange typedaterange range-separator至 start-placeholder开始日期 end-placeholder结束日期 / /el-form-item el-form-item label类型 el-select v-modelqueryForm.type clearable placeholder全部类型 el-option label收入 valueINCOME / el-option label支出 valueEXPENSE / el-option label转账 valueTRANSFER / /el-select /el-form-item el-form-item el-button typeprimary clickloadTransactions查询/el-button /el-form-item /el-form /el-card el-card el-table :datatransactionList stripe el-table-column proptransactTime label交易时间 width160 / el-table-column propcategoryName label分类 width120 / el-table-column propaccountName label账户 width120 / el-table-column proptype label类型 width80 template #default{ row } el-tag :typetypeTag(row.type){{ typeLabel(row.type) }}/el-tag /template /el-table-column el-table-column propamount label金额 width120 alignright template #default{ row } span :classrow.type EXPENSE ? expense-amount : income-amount {{ row.type EXPENSE ? - : }}{{ row.amount }} /span /template /el-table-column el-table-column propremark label备注 / /el-table /el-card /div /template script setup import { ref, reactive, onMounted } from vue import axios from axios const queryForm reactive({ dateRange: [], type: }) const transactionList ref([]) function loadTransactions() { const params { type: queryForm.type || undefined } if (queryForm.dateRange queryForm.dateRange.length 2) { params.startDate queryForm.dateRange[0] params.endDate queryForm.dateRange[1] } axios.get(/api/transactions, { params }).then(res { transactionList.value res.data }) } function typeLabel(type) { return { INCOME: 收入, EXPENSE: 支出, TRANSFER: 转账 }[type] || type } function typeTag(type) { return { INCOME: success, EXPENSE: danger, TRANSFER: info }[type] || info } onMounted(loadTransactions) /script style scoped .expense-amount { color: #f56c6c; font-weight: bold; } .income-amount { color: #67c23a; font-weight: bold; } /style7.2 预算进度展示预算模块要解决的核心问题是这个月还剩下多少可花额度。把“已消费金额”与“预算金额”做比较用进度条直观显示超支风险。template div classbudget-card v-foritem in budgetList :keyitem.id div classbudget-header span{{ item.categoryName }}/span span¥{{ item.spent }} / ¥{{ item.amount }}/span /div el-progress :percentagepercentage(item.spent, item.amount) :statuspercentage(item.spent, item.amount) 100 ? exception : undefined / /div /template script setup import { computed } from vue const props defineProps({ budgetList: { type: Array, required: true } }) function percentage(spent, amount) { if (!amount || amount 0) return 0 return Math.min(Math.round((spent / amount) * 100), 100) } /script这里的percentage函数最多返回 100防止进度条溢出但视觉上超支状态会标记为红色。真正的超支金额数值仍然由后台统计接口计算前端只负责直观展示。7.3 导出 CSV 报表财务管理系统的数据导出功能在很多项目里被当成“锦上添花”的功能但对用户来说这是刚需。实现方式是在后端生成 CSV 文件流前端直接下载。package com.example.procura.controller; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.io.PrintWriter; import java.nio.charset.StandardCharsets; RestController RequestMapping(/api/export) public class ExportController { GetMapping(/transactions.csv) public void exportTransactions(HttpServletResponse response) throws IOException { response.setContentType(text/csv;charsetUTF-8); response.setHeader(Content-Disposition, attachment; filenametransactions.csv); PrintWriter writer response.getWriter(); // 写入 UTF-8 BOM避免 Excel 打开 CSV 中文乱码 writer.write(\ufeff); writer.write(交易时间,类型,金额,分类,备注\n); // 实际项目中改为从数据库查询 writer.write(2025-01-05 12:30:00,EXPENSE,35.50,午餐,食堂\n); writer.write(2025-01-06 09:00:00,INCOME,15000.00,工资,1月工资\n); writer.flush(); } }这里有一个实战中的经验点CSV 文件加上 UTF-8 BOM\ufeff后用 Excel 打开才不会乱码。如果不加 BOM很多 Windows 机器上会显示为乱码这是导出功能最容易踩的坑。8. 运行、测试与验证8.1 启动后端在项目根目录执行mvn spring-boot:run启动成功后控制台会打印 Spring Boot 的启动日志Tomcat 默认监听 8080 端口。8.2 启动前端cd procura-web npm run dev默认访问地址是http://localhost:5173。如果前端项目中配置了 API 代理需要在vite.config.js中配置import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })8.3 用 curl 验证接口后端启动后先用 curl 验证接口是否可访问。# 查询所有账户 curl http://localhost:8080/api/accounts # 新增一笔支出交易 curl -X POST http://localhost:8080/api/transactions \ -H Content-Type: application/json \ -d { accountId: 1, categoryId: 3, type: EXPENSE, amount: 68.50, remark: 超市购物, transactTime: 2025-01-06T19:30:00 }如果返回结果中包含交易 ID 和更新后的账户余额说明后端逻辑已经跑通。对于写操作新增、修改、删除一定要在数据库中确认数据变更符合预期特别是账户余额字段是否同步更新。8.4 如何判断系统运行正常从三个层面验证接口层面通过 Swagger 或 curl 调用接口确认返回状态码和数据结构正确。数据层面检查 MySQL 中transaction表和account表的金额是否一致。比如新增一笔 100 元支出后账户余额应该减少 100。前端层面在浏览器中完成一次完整的“新增账户 - 记一笔支出 - 查看月度报表”流程确认页面无报错、金额显示正确。9. 常见问题与排查方法问题现象可能原因排查方式解决方案中文乱码数据库连接未指定 UTF-8 编码检查application.yml中 JDBC URL 是否包含characterEncodingutf8mb4在连接 URL 末尾增加编码参数CSV 导出后 Excel 打开乱码CSV 未包含 UTF-8 BOM用记事本查看 CSV 文件前几个字符在输出流中写入\ufeff转账后余额不对转账事务未加Transactional查看转账接口的代码是否缺少事务注解添加Transactional(rollbackFor Exception.class)JPA 查询报Unknown column错误ddl-auto: update未自动更新表结构查看日志中的建表 SQL使用 Flyway 管理表结构变更前端跨域请求失败Vite 代理未配置或后端未开启 CORS打开浏览器 DevTools 查看 Network 报错在vite.config.js配置代理或添加 CORS 配置类10. 工程化实践与安全建议10.1 金额计算必须使用 BigDecimal这是财务系统的底线。Java 的float和double无法精确表示小数例如0.1 0.2的结果是0.30000000000000004在金额计算中绝对不能使用。数据库端使用DECIMALJava 端使用BigDecimal两者配合才能保证金额精度。10.2 所有写操作都要有日志审计财务系统里谁在什么时间修改了哪笔交易、哪个账户余额必须能追溯。最基础的做法是在交易表增加created_at和updated_at字段更严谨的做法是增加operator_id字段或者引入操作日志表记录每一次写请求。10.3 备份策略不能省个人系统可能不需要实时的主从复制但至少要保证每天自动备份数据库。可以在服务器上配置 cron 定时任务0 2 * * * mysqldump -u root -pYourPassword procura /backup/procura_$(date \%Y\%m\%d).sql注意生产环境不要把数据库密码直接写在命令行中可以使用 MySQL 的~/.my.cnf配置文件保存凭据并设置文件权限为 600。10.4 API 权限控制如果是单人使用可以暂时不做登录系统但不要把服务直接暴露到公网。最稳妥的方式是部署在内网或者使用 Nginx 添加 Basic Auth。如果项目后续要支持多人使用建议引入 Spring Security JWT 做认证并按用户维度隔离数据。10.5 使用 Docker Compose 简化部署前后端分离项目的部署流程推荐用 Docker Compose 整合version: 3.8 services: mysql: image: mysql:8.0 container_name: procura-mysql environment: MYSQL_ROOT_PASSWORD: your_password MYSQL_DATABASE: procura ports: - 3306:3306 volumes: - mysql_data:/var/lib/mysql backend: build: ./backend container_name: procura-backend depends_on: - mysql environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/procura?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai SPRING_DATASOURCE_USERNAME: root SPRING_DATASOURCE_PASSWORD: your_password ports: - 8080:8080 frontend: build: ./frontend container_name: procura-frontend ports: - 80:80 depends_on: - backend volumes: mysql_data:使用 Docker Compose 后一条docker-compose up -d命令即可启动整个系统。前端镜像用 Nginx 托管打包产物并通过 Nginx 配置反向代理到后端容器。11. 适合哪些人拿这个项目练手回到最初的问题Procura – Finance Manager 这个项目适合谁如果你是后端方向的学习者这个项目能帮你把 Spring Boot 的事务管理、JPA 查询、接口设计、数据校验这些知识点串起来。比起做一个“图书管理系统”财务项目的数据关联更复杂面试时能聊的点更多。如果你是个人开发者想做一套真正能用的记账系统这个项目的精髓不在前端多炫酷而在于数据模型的合理设计。有了规范的数据库结构和稳定的后端接口前端部分无论是用 Vue、React 还是小程序都可以快速实现。如果你已经在团队里负责业务系统开发这个项目也能给你不少启发预算控制本质上是一种业务规则引擎交易流水表是一场典型的“事件溯源”式设计报表模块则是常见的多表聚合查询场景。这些能力都是可以复用到工作项目里的。12. 值得继续深入的方向Procura – Finance Manager 的基础功能做完之后还有几个值得继续深入的方向第一多用户与家庭共享。增加用户表把账户、交易、预算都关联到用户维度再引入第三方登录就能把系统从“单人记账工具”升级成“家庭财务协同平台”。第二账单导入。许多银行和支付平台支持导出 Excel 或 CSV 格式的流水实现一个批量导入解析器会自动把银行流水按照备注、金额匹配到系统分类。这个功能会极大降低记账成本。第三投资资产追踪。在账户模型之上扩展“投资组合”表记录基金、股票、数字货币的持仓和成本价计算真实收益率。这一块会让系统从单纯的“记账工具”进化为“资产管理工具”。很多人会高估做记账系统的难度低估数据模型设计的复杂度。实际上把账户、交易、分类、预算这些表的关系理清楚再配合事务保证数据一致性整个系统就已经成功了一大半。前端要做的不过是把数据展示得更直观、操作更顺手罢了。如果你正准备从零搭建一个自己的 Finance Manager建议先把本文第 5 节的数据库表结构建好然后跑通一条核心链路新增账户 - 记录一笔支出 - 查看月度报表。这条路走通之后剩下的功能都是在主干上做加法。