免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

从零构建个人财务管理系统:Spring Boot + Vue 3 + JWT 全栈实践

从零构建个人财务管理系统:Spring Boot + Vue 3 + JWT 全栈实践 Procura 是一个面向个人和家庭场景的 Finance Manager 应用。开发这类系统时最常见的误区是把“能不能记账”当成核心目标结果功能上线后才发现统计报表、预算报警和分类调整都在跟最初的数据模型打架。本文以 Procura 的完整实现路径为线索从需求拆分、数据库设计、后端接口、前端页面到部署排查说明一个可复用的财务管理系统应该按什么结构落地。如果你准备从零搭建类似的记账、预算或资产管理应用可以把本文当成主线参考如果只是想补全现有项目也可以直接跳到第 3 节和第 7 节查找对应方案。技术栈选择上后端使用 Spring Boot 3 Spring Data JPA MySQL前端使用 Vue 3 Vite认证使用 JWT。这套组合不是唯一选择但模块边界、数据建模和接口设计思路可以迁移到其他语言和框架。下面先讲清楚核心链路再进入具体实现。1. 先理解 Finance Manager 的核心链路再写第一行代码1.1 财务应用不是记账本核心是数据闭环很多初学者会把 Finance Manager 理解成一个“增删改查”项目记录收入、记录支出、查看列表完事。实际上记账只是入口财务应用真正有价值的是数据闭环用户录入一笔收入或支出。数据落到账目记录表同时关联到分类。预算模块按周期统计该分类或总支出是否超支。报表模块从账目记录中聚合出月度收支、分类占比、趋势曲线。用户根据报表调整预算和消费行为再次产生新的账目记录。这个闭环里只要某一层的口径不一致后面所有统计都会错。比如记录时用了本地时间统计时用了数据库服务器时间月度报表就会在跨时区场景下出现数据偏移。所以设计阶段就要把“时间口径、金额精度、分类归属”三件事定死。1.2 功能模块怎么拆Procura 这类应用可以直接按业务能力拆成四个模块每个模块对应一个明确的数据出口模块主要负责数据出口典型问题用户与认证注册、登录、Token 管理当前登录用户身份敏感信息泄露、Token 过期处理分类管理收入分类、支出分类的维护分类树、下拉选项删除分类导致历史数据断裂账目记录每一笔收支的增删改查列表、明细、编辑入口金额精度丢失、重复记账预算与统计预算配置、超支计算、报表聚合仪表盘、图表、提醒统计周期口径不一致、聚合查询过慢在代码层面这四个模块可以分别对应auth、category、record、budget四个包互不越界。这样后面加人民币以外的币种、加多账户、加账单分期都是在单模块内扩展而不是把控制器越写越胖。1.3 一条完整业务链路先用一句话描述完整链路用户登录后进入仪表盘看到当前月支出、收入、预算剩余额度点击“记一笔”选择分类输入金额和时间保存后列表更新仪表盘统计重新计算如果该分类当月累计支出超过预算页面出现超支提示。这条链路贯穿了后端接口的每一个动作也决定了前端页面至少需要登录页、仪表盘页、记录列表页、记录表单、分类管理页、预算设置页。后续所有代码都围绕这条链路展开不要先写一堆无关的通用代码。2. 技术选型和项目结构决定后面改动成本2.1 选型思路后端选择 Spring Boot 3主要原因是它对 JPA、参数校验、全局异常处理的支持成熟适合快速搭出规整的服务端结构。MySQL 作为业务数据库负责账目、分类、预算和用户数据的持久化。前端使用 Vue 3 Vite状态管理用 Pinia路由用 Vue RouterHTTP 请求用 Axios。认证部分使用 JWT服务端不保存会话前端在请求头中携带 Token。学习环境可以换成 H2 内存数据库减少安装成本但生产环境至少要切到 MySQL 或 PostgreSQL因为内存数据库重启后数据会丢失。项目落地前要先确认 JDK 版本和 Spring Boot 版本是否匹配Spring Boot 3 要求 JDK 17 及以上。2.2 前后端目录结构后端建议按模块分包而不是按三层分包。下面是一个可参考的结构procura-server/ ├── pom.xml ├── src/main/java/com/procura/ │ ├── ProcuraApplication.java │ ├── common/ # 统一返回结果、异常处理、分页参数 │ ├── config/ # Security、JWT、跨域配置 │ ├── auth/ # 注册、登录、JWT 过滤器 │ ├── category/ # 分类实体、Repository、Service、Controller │ ├── record/ # 账目记录模块 │ ├── budget/ # 预算模块 │ └── report/ # 统计报表模块 └── src/main/resources/ └── application.yml前端目录对应如下procura-web/ ├── package.json ├── vite.config.js ├── index.html └── src/ ├── main.js ├── api/ # axios 实例和接口定义 ├── router/ # 路由表 ├── stores/ # Pinia 状态 ├── views/ # 页面级组件 ├── components/ # 通用组件 └── utils/ # 日期格式化、金额处理工具模块化目录的目的是让开发者拿到项目后不看全文也能猜到每个文件放在哪里。这一点在生产项目里比技术选型本身更重要。2.3 环境准备本地开发环境至少需要软件版本建议用途JDK17 或以上编译运行后端Maven3.8 或以上后端依赖管理MySQL8.0 或以上业务数据库Node.js18 或以上前端构建与开发npm / pnpm任意较新版本前端依赖管理后端启动前先创建数据库避免启动时连接失败CREATE DATABASE procura DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER procuralocalhost IDENTIFIED BY change-me; GRANT ALL PRIVILEGES ON procura.* TO procuralocalhost; FLUSH PRIVILEGES;数据库名称、用户名和密码要和application.yml保持一致。学习阶段可以把密码写进配置文件但生产环境一定要通过环境变量注入不能提交到 Git。3. 数据库设计表结构、字段类型和约束一次定好3.1 四张核心表财务系统的数据量通常不会特别大但字段语义必须严谨。Procura 的核心表可以设计为四张用户表、分类表、账目记录表、预算表。CREATE TABLE app_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(100) NOT NULL, email VARCHAR(100) NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE category ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, name VARCHAR(50) NOT NULL, type TINYINT NOT NULL COMMENT 1-收入 2-支出, sort_order INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_category (user_id, name, type), CONSTRAINT fk_category_user FOREIGN KEY (user_id) REFERENCES app_user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE transaction_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, category_id BIGINT NOT NULL, amount DECIMAL(12,2) NOT NULL, record_type TINYINT NOT NULL COMMENT 1-收入 2-支出, note VARCHAR(200) NULL, record_time DATETIME NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, record_time), KEY idx_user_category (user_id, category_id), CONSTRAINT fk_record_user FOREIGN KEY (user_id) REFERENCES app_user(id), CONSTRAINT fk_record_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE budget ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, category_id BIGINT NULL, period_type TINYINT NOT NULL COMMENT 1-月 2-年, amount DECIMAL(12,2) NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_budget_user FOREIGN KEY (user_id) REFERENCES app_user(id), CONSTRAINT fk_budget_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个设计决策要解释。transaction_record单独保存record_type是因为一笔记录虽然绑定分类但在查询时如果每次都要关联category才能知道是收入还是支出SQL 会更复杂也容易在关联条件上出错。冗余一个类型字段代价极小收益是统计查询可以直接按record_type过滤。3.2 字段类型和精度的取舍金额字段必须用DECIMAL(12,2)不能用FLOAT或DOUBLE。浮点数在二进制存储中有精度误差累计几百笔之后报表金额和实际金额可能差出几分钱。Java 实体中对应使用BigDecimal不要使用Double。record_time使用DATETIME前提是所有服务都在同一时区。如果项目面向多时区用户更稳妥的做法是使用TIMESTAMP或直接存储 UTC 时间展示时再按用户时区转换。这个决策需要在设计期定下来否则上线后统一改时间字段的迁移成本非常高。分类名、用户名这些短字符串统一使用VARCHAR不要用TEXT。TEXT无法直接加普通索引也会增加行存储开销。分类和用户名的组合唯一键uk_user_category可以防止同一个用户重复创建同名同类型分类。3.3 索引与外键设计查询最多的场景是“某个时间范围内的账目列表”和“某个月份的支出汇总”所以核心索引是(user_id, record_time)。统计接口通常会带分类过滤(user_id, category_id)也建议建立索引。外键约束在财务系统里建议保留尤其是分类删除时外键可以阻止误删被历史记录引用的分类。删除分类的业务逻辑应当是先检查是否有账目记录引用如果有标记为停用而不是物理删除。注意学习阶段可以使用ddl-autoupdate让 JPA 自动建表但生产环境应使用ddl-autovalidate并通过 Flyway 或 Liquibase 管理表结构变更避免实体和数据库结构不一致导致的线上事故。4. 后端实现接口先对齐业务再写 SQL4.1 认证与登录认证模块解决两个问题注册时密码如何安全存储登录后后续请求如何识别身份。密码存储使用 BCrypt不要使用 MD5 或 SHA-256 这类不带盐的散列算法。Service RequiredArgsConstructor public class AuthService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final JwtTokenProvider tokenProvider; Transactional public AuthResponse register(RegisterRequest request) { if (userRepository.existsByUsername(request.getUsername())) { throw new BusinessException(用户名已存在); } AppUser user new AppUser(); user.setUsername(request.getUsername()); user.setPasswordHash(passwordEncoder.encode(request.getPassword())); userRepository.save(user); return new AuthResponse(tokenProvider.createToken(user.getId(), user.getUsername())); } public AuthResponse login(LoginRequest request) { AppUser user userRepository.findByUsername(request.getUsername()) .orElseThrow(() - new BusinessException(用户名或密码错误)); if (!passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) { throw new BusinessException(用户名或密码错误); } return new AuthResponse(tokenProvider.createToken(user.getId(), user.getUsername())); } }JWT 过滤器负责从请求头中解析 Token并设置当前用户上下文。注意 Token 过期时间不要设置过长常见做法是 24 小时以内前端在 401 时引导用户重新登录。application.yml中对应的基础配置如下spring: datasource: url: jdbc:mysql://localhost:3306/procura?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: procura password: change-me jpa: hibernate: ddl-auto: validate open-in-view: false jackson: time-zone: Asia/Shanghai server: port: 8080 jwt: secret: ${JWT_SECRET} expire-hours: 24open-in-view: false是一个容易被忽略的配置。Spring Boot 默认开启open-in-view会延长数据库连接在请求期间的持有时间在高并发下容易出现连接池耗尽。业务代码中应该在 Service 层完成关联数据的组装而不是在 Controller 层懒加载实体关联对象。4.2 账目记录的增删改查实体类要和表结构一一对应Entity Table(name transaction_record) public class TransactionRecord { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name user_id, nullable false) private Long userId; Column(name category_id, nullable false) private Long categoryId; Column(nullable false, precision 12, scale 2) private BigDecimal amount; Column(name record_type, nullable false) private Integer recordType; Column(length 200) private String note; Column(name record_time, nullable false) private LocalDateTime recordTime; Column(name created_at, nullable false, updatable false) private LocalDateTime createdAt; }创建记录的 Service 方法需要考虑两点分类必须属于当前用户金额必须大于零。如果只是简单接收前端参数写入数据库攻击者可以传任意userId或categoryId造成越权写入。Transactional public TransactionRecord create(RecordCreateRequest request, Long userId) { Category category categoryService.getOwnedCategory(request.getCategoryId(), userId); if (request.getAmount() null || request.getAmount().compareTo(BigDecimal.ZERO) 0) { throw new BusinessException(金额必须大于 0); } TransactionRecord record new TransactionRecord(); record.setUserId(userId); record.setCategoryId(category.getId()); record.setAmount(request.getAmount()); record.setRecordType(request.getRecordType()); record.setNote(request.getNote()); record.setRecordTime(request.getRecordTime()); return recordRepository.save(record); }Controller 只需要做参数接收和身份取出RestController RequestMapping(/api/records) RequiredArgsConstructor public class RecordController { private final RecordService recordService; PostMapping public ResponseEntityTransactionRecord create( RequestBody Valid RecordCreateRequest request, AuthenticationPrincipal UserPrincipal user) { TransactionRecord record recordService.create(request, user.getId()); return ResponseEntity.status(HttpStatus.CREATED).body(record); } GetMapping public PageTransactionRecord page( RequestParam(defaultValue 1) int page, RequestParam(defaultValue 20) int size, AuthenticationPrincipal UserPrincipal user) { return recordService.pageByUser(user.getId(), page, size); } }删除和修改接口必须先校验记录归属否则会出现用户 A 删掉用户 B 数据的问题。不要在前端隐藏按钮后端必须做归属校验。4.3 预算与超支计算预算表保存了用户设置的周期和金额。超支计算需要把预算额度跟账目记录聚合值做对比SELECT COALESCE(SUM(t.amount), 0) FROM transaction_record t WHERE t.user_id :userId AND t.record_type 2 AND t.record_time BETWEEN :start AND :end在 JPA 中可以使用Query实现public interface TransactionRecordRepository extends JpaRepositoryTransactionRecord, Long { Query( SELECT COALESCE(SUM(t.amount), 0) FROM TransactionRecord t WHERE t.userId :userId AND t.recordType :type AND t.recordTime BETWEEN :start AND :end ) BigDecimal sumAmountByUserAndTypeAndTimeRange( Param(userId) Long userId, Param(type) Integer type, Param(start) LocalDateTime start, Param(end) LocalDateTime end ); }这里的统计口径是“自然月”从当月 1 日零点到下月 1 日零点。预算接口返回时可以附带一个已用比例字段含义示例budgetId预算 ID1001periodType1 按月2 按年1amount预算总额5000.00usedAmount已支出金额3200.50usePercent已用比例64.01%overflow是否超支false超支判断不要在前端用除法直接算完后端返回usedAmount和usePercent前端只负责展示这样可以保证不同页面看到的数据一致。4.4 统计报表的聚合查询月度报表可以按分类维度聚合Query( SELECT new com.procura.report.CategorySummary( c.id, c.name, SUM(t.amount) ) FROM TransactionRecord t JOIN Category c ON c.id t.categoryId WHERE t.userId :userId AND t.recordType :type AND t.recordTime BETWEEN :start AND :end GROUP BY c.id, c.name ORDER BY SUM(t.amount) DESC ) ListCategorySummary summarizeByCategory( Param(userId) Long userId, Param(type) Integer type, Param(start) LocalDateTime start, Param(end) LocalDateTime end );统计接口不要在前端把所有记录拉下来再分组计算数据量小时还能接受数据量上来之后会产生大量无效网络传输和内存消耗。聚合计算应交给数据库。5. 前端实现页面结构、状态管理和接口对接5.1 页面路由和组件划分Procura 前端按视图划分/login 登录页 /register 注册页 /dashboard 仪表盘展示当月统计和预算进度 /records 账目列表支持分页和关键字过滤 /records/new 记账表单 /categories 分类管理 /budget 预算设置路由需要加登录守卫。未登录访问受保护页面时跳转到登录页。Axios 请求拦截器统一从 Pinia 或 localStorage 中取出 Token加到请求头响应拦截器统一处理 401 和业务错误码。5.2 一个可复用的账目表单记账表单是使用频率最高的组件字段至少包括类型、金额、分类、时间、备注。类型切换时分类下拉列表要随之切换为收入分类或支出分类。template div classrecord-form select v-modelform.recordType changeloadCategories option :value2支出/option option :value1收入/option /select input v-model.numberform.amount typenumber step0.01 min0.01 placeholder金额 / select v-modelform.categoryId option v-foritem in categories :keyitem.id :valueitem.id {{ item.name }} /option /select input v-modelform.note typetext placeholder备注可选 / input v-modelform.recordTime typedatetime-local / button :disabledsubmitting clicksubmit {{ submitting ? 保存中 : 保存 }} /button /div /template script setup import { reactive, ref, onMounted } from vue; import { getCategories, createRecord } from ../api; const form reactive({ recordType: 2, amount: null, categoryId: null, note: , recordTime: }); const categories ref([]); const submitting ref(false); async function loadCategories() { const data await getCategories(form.recordType); categories.value data; if (data.length 0) { form.categoryId data[0].id; } } async function submit() { if (!form.amount || !form.categoryId) { return; } submitting.value true; try { await createRecord(form); // 返回列表页或仪表盘 } finally { submitting.value false; } } onMounted(loadCategories); /script这里容易忽略的是v-model.number输入金额会直接得到一个数字但 JavaScript 浮点运算会把0.1 0.2算成0.30000000000000004。前端发送金额要给后端传字符串或BigDecimal对应的字符串表示而不是先做浮点运算。展示金额时使用toFixed(2)做格式化不要依赖原生浮点精度。5.3 统计页面怎么接数据统计页面通过一个统一接口拿到数据例如/api/reports/monthly?month2025-06返回内容包含{ month: 2025-06, totalIncome: 15000.00, totalExpense: 8680.50, categorySummaries: [ { categoryId: 10, categoryName: 餐饮, amount: 3200.50 }, { categoryId: 11, categoryName: 交通, amount: 980.00 } ], budgetStatus: { totalBudget: 10000.00, usedAmount: 8680.50, usePercent: 86.81, overflow: false } }前端拿到 JSON 后直接渲染不要在组件内部重新统计数据。后端返回金额字段统一使用字符串格式避免大数精度问题。图表展示可以用 ECharts饼图展示分类占比折线图展示近六个月的收支趋势。6. 本地运行与联调验证6.1 后端启动步骤先确认 MySQL 已启动然后执行cd procura-server mvn spring-boot:run启动成功后应该能看到类似日志Tomcat started on port 8080 (http) Started ProcuraApplication in 3.2 seconds如果连接数据库失败先检查网络、用户名、密码和数据库是否存在。如果提示表不存在确认是否执行了第 3 节的建表 SQL或者检查ddl-auto配置。6.2 前端启动步骤cd procura-web npm install npm run devVite 默认端口是 5173。前后端联调需要配置代理在vite.config.js中把/api代理到后端import { defineConfig } from vite; export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });这一步解决了前端开发时的跨域问题。生产环境则由 Nginx 统一处理/api转发。6.3 核心流程验证清单跑通最小闭环后按下面的清单逐项验证序号验证场景预期结果1注册一个新用户返回 Token可以访问受保护接口2登录已存在用户返回 Token密码错误时提示错误3创建支出分类分类出现在支出下拉列表4记一笔支出列表出现该记录金额正常5设置当月预算仪表盘显示预算和已用比例6录入多笔后查看报表分类汇总和月度总额正确7用其他用户 Token 访问本用户记录返回 403 或空数据第 7 项最容易出问题开发时经常只测试了正常流程忽略了越权访问。建议在接口测试阶段就加入归属校验用例。7. 常见问题排查从现象倒推原因7.1 登录、权限和数据问题问题现象常见原因检查方式处理建议登录后请求接口返回 401Token 过期、请求头未携带 Token、过滤器未放行登录接口看请求头是否有Authorization: Bearer xxx前端请求拦截器统一携带 Token后端放行/api/auth/login和/api/auth/register接口能访问但返回空数据当前用户 ID 与数据归属不一致在日志中打印AuthenticationPrincipal的 ID所有查询都从用户上下文取 ID不从前端参数取中文乱码数据库字符集不是 utf8mb4或连接串缺少字符集参数执行SHOW CREATE TABLE查看字符集建库时指定utf8mb4连接串加characterEncodingutf8mb4分类删除失败分类已被账目记录引用外键约束阻止删除查看数据库外键错误日志改为软删除增加status字段历史记录仍然可查询7.2 金额精度问题现象是报表里总额和明细相加对不上或者出现0.30000000000000004这类值。排查顺序检查数据库字段是否为DECIMAL而不是FLOAT、DOUBLE。检查后端实体字段是否为BigDecimal而不是Double。检查前端是否在传参前做了浮点加法。检查统计 SQL 是否是SUM(amount)而不是先取出结果在 Java 里循环累加。解决方案金额链路从数据库到前端展示全部使用十进制类型前端展示用toFixed(2)接口传输用字符串。7.3 数据库连接和时区问题现象是启动时报时区错误或者统计报表月份偏移一天。The server time zone value std is unrecognized检查连接串中是否加入了serverTimezoneAsia/Shanghai同时确认 MySQL 服务端时区SELECT NOW(); SELECT global.time_zone, session.time_zone;如果数据库服务器和业务服务器在不同时区建议统一改为 UTC 存储展示时再转换。不要在代码里做一半本地时区、一半数据库时区的混合处理。7.4 开发环境能跑、生产环境报错的差异化排查检查项开发环境生产环境建表方式ddl-autoupdateddl-autovalidate Flyway数据库H2 或本地 MySQLMySQL/PostgreSQL 独立实例配置来源application.yml固定值环境变量或配置中心日志控制台输出文件日志 集中收集静态资源Vite 代理Nginx 托管并配置缓存如果开发环境正常、生产环境报错优先检查配置差异不要先改代码。最容易踩的是数据库连接串、字符集、环境变量未注入这三点。8. 生产环境部署与最佳实践8.1 部署结构生产环境可以采用 Docker Compose 编排后端、前端和数据库version: 3.8 services: mysql: image: mysql:8.0 environment: MYSQL_DATABASE: procura MYSQL_USER: procura MYSQL_PASSWORD: ${DB_PASSWORD} volumes: - mysql-data:/var/lib/mysql server: build: ./procura-server environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/procura SPRING_DATASOURCE_USERNAME: procura SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD} JWT_SECRET: ${JWT_SECRET} depends_on: - mysql web: build: ./procura-web ports: - 80:80 depends_on: - server volumes: mysql-data:JWT_SECRET和数据库密码通过.env文件或部署平台的环境变量注入不要写死在镜像里。前端 Nginx 配置需要把/api反向代理到server服务。8.2 安全加固生产环境至少要做以下加固密码使用 BCrypt 加密不要允许明文密码登录。JWT_SECRET长度不少于 32 字符定期轮换。所有接口强制 HTTPS登录接口不要使用 HTTP 明文传输。增加登录失败次数限制避免暴力破解。接口做参数校验金额、时间、分页参数都要限制范围。数据库账号使用最小权限不直接使用 root 连接业务库。定期备份数据库备份文件加密存放。对于财务系统审计日志也很重要。每笔记录的创建、修改、删除都要记录操作人、时间和变更内容方便后续对账和排查争议。8.3 发布前检查清单类别检查项数据备份是否完整恢复演练是否通过配置JWT_SECRET、数据库密码是否通过环境变量注入安全HTTPS 是否生效敏感接口是否有鉴权性能报表接口是否走了数据库聚合分页是否正常监控是否接入日志收集、接口耗时监控、错误告警回滚确定回滚方案镜像版本是否可快速切换数据迁移Flyway 脚本是否在测试环境执行通过后再应用到生产8.4 下一步扩展方向Procura 的完整链路跑通后可以按下面的顺序继续扩展多账户支持区分现金、银行卡、信用卡账户账目记录增加account_id。周期记账能力支持一次性、每周、每月自动生成流水。多币种和汇率转换金额表增加币种字段统计时统一换算。导入导出支持 Excel、CSV 对账文件导入。预算提醒通过 WebSocket 或短信在超支时通知用户。每个扩展方向都要回到最初的数据闭环检查录入、分类、预算、统计之间口径是否仍然一致。如果只加了账户字段却忘了在报表聚合中按账户维度分组新功能就会破坏原有统计。这也是 Finance Manager 类项目最值得反复验证的地方功能可以越加越多但核心数据链路必须始终稳定。
返回列表