news 2026/8/31 14:52:59

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零构建个人财务管理系统: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. 数据落到账目记录表,同时关联到分类。
  3. 预算模块按周期统计该分类或总支出是否超支。
  4. 报表模块从账目记录中聚合出月度收支、分类占比、趋势曲线。
  5. 用户根据报表调整预算和消费行为,再次产生新的账目记录。

这个闭环里,只要某一层的口径不一致,后面所有统计都会错。比如记录时用了本地时间,统计时用了数据库服务器时间,月度报表就会在跨时区场景下出现数据偏移。所以设计阶段就要把“时间口径、金额精度、分类归属”三件事定死。

1.2 功能模块怎么拆

Procura 这类应用可以直接按业务能力拆成四个模块,每个模块对应一个明确的数据出口:

模块主要负责数据出口典型问题
用户与认证注册、登录、Token 管理当前登录用户身份敏感信息泄露、Token 过期处理
分类管理收入分类、支出分类的维护分类树、下拉选项删除分类导致历史数据断裂
账目记录每一笔收支的增删改查列表、明细、编辑入口金额精度丢失、重复记账
预算与统计预算配置、超支计算、报表聚合仪表盘、图表、提醒统计周期口径不一致、聚合查询过慢

在代码层面,这四个模块可以分别对应authcategoryrecordbudget四个包,互不越界。这样后面加人民币以外的币种、加多账户、加账单分期,都是在单模块内扩展,而不是把控制器越写越胖。

1.3 一条完整业务链路

先用一句话描述完整链路:用户登录后进入仪表盘,看到当前月支出、收入、预算剩余额度;点击“记一笔”,选择分类,输入金额和时间;保存后列表更新,仪表盘统计重新计算;如果该分类当月累计支出超过预算,页面出现超支提示。

这条链路贯穿了后端接口的每一个动作,也决定了前端页面至少需要:登录页、仪表盘页、记录列表页、记录表单、分类管理页、预算设置页。后续所有代码都围绕这条链路展开,不要先写一堆无关的通用代码。

2. 技术选型和项目结构,决定后面改动成本

2.1 选型思路

后端选择 Spring Boot 3,主要原因是它对 JPA、参数校验、全局异常处理的支持成熟,适合快速搭出规整的服务端结构。MySQL 作为业务数据库,负责账目、分类、预算和用户数据的持久化。前端使用 Vue 3 + Vite,状态管理用 Pinia,路由用 Vue Router,HTTP 请求用 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 'procura'@'localhost' IDENTIFIED BY 'change-me'; GRANT ALL PRIVILEGES ON procura.* TO 'procura'@'localhost'; 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有几个设计决策要解释。transaction_record单独保存record_type,是因为一笔记录虽然绑定分类,但在查询时如果每次都要关联category才能知道是收入还是支出,SQL 会更复杂,也容易在关联条件上出错。冗余一个类型字段,代价极小,收益是统计查询可以直接按record_type过滤。

3.2 字段类型和精度的取舍

金额字段必须用DECIMAL(12,2),不能用FLOATDOUBLE。浮点数在二进制存储中有精度误差,累计几百笔之后,报表金额和实际金额可能差出几分钱。Java 实体中对应使用BigDecimal,不要使用Double

record_time使用DATETIME,前提是所有服务都在同一时区。如果项目面向多时区用户,更稳妥的做法是使用TIMESTAMP或直接存储 UTC 时间,展示时再按用户时区转换。这个决策需要在设计期定下来,否则上线后统一改时间字段的迁移成本非常高。

分类名、用户名这些短字符串统一使用VARCHAR,不要用TEXTTEXT无法直接加普通索引,也会增加行存储开销。分类和用户名的组合唯一键uk_user_category可以防止同一个用户重复创建同名同类型分类。

3.3 索引与外键设计

查询最多的场景是“某个时间范围内的账目列表”和“某个月份的支出汇总”,所以核心索引是(user_id, record_time)。统计接口通常会带分类过滤,(user_id, category_id)也建议建立索引。

外键约束在财务系统里建议保留,尤其是分类删除时,外键可以阻止误删被历史记录引用的分类。删除分类的业务逻辑应当是先检查是否有账目记录引用,如果有,标记为停用,而不是物理删除。

注意:学习阶段可以使用ddl-auto=update让 JPA 自动建表,但生产环境应使用ddl-auto=validate,并通过 Flyway 或 Liquibase 管理表结构变更,避免实体和数据库结构不一致导致的线上事故。

4. 后端实现:接口先对齐业务,再写 SQL

4.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?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/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: 24

open-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 方法需要考虑两点:分类必须属于当前用户,金额必须大于零。如果只是简单接收前端参数写入数据库,攻击者可以传任意userIdcategoryId,造成越权写入。

@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 ResponseEntity<TransactionRecord> create( @RequestBody @Valid RecordCreateRequest request, @AuthenticationPrincipal UserPrincipal user) { TransactionRecord record = recordService.create(request, user.getId()); return ResponseEntity.status(HttpStatus.CREATED).body(record); } @GetMapping public Page<TransactionRecord> 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 JpaRepository<TransactionRecord, 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预算 ID1001
periodType1 按月,2 按年1
amount预算总额5000.00
usedAmount已支出金额3200.50
usePercent已用比例64.01%
overflow是否超支false

超支判断不要在前端用除法直接算完,后端返回usedAmountusePercent,前端只负责展示,这样可以保证不同页面看到的数据一致。

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 """) List<CategorySummary> 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 class="record-form"> <select v-model="form.recordType" @change="loadCategories"> <option :value="2">支出</option> <option :value="1">收入</option> </select> <input v-model.number="form.amount" type="number" step="0.01" min="0.01" placeholder="金额" /> <select v-model="form.categoryId"> <option v-for="item in categories" :key="item.id" :value="item.id" > {{ item.name }} </option> </select> <input v-model="form.note" type="text" placeholder="备注(可选)" /> <input v-model="form.recordTime" type="datetime-local" /> <button :disabled="submitting" @click="submit"> {{ 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?month=2025-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 dev

Vite 默认端口是 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,连接串加characterEncoding=utf8mb4
分类删除失败分类已被账目记录引用,外键约束阻止删除查看数据库外键错误日志改为软删除:增加status字段,历史记录仍然可查询

7.2 金额精度问题

现象是报表里总额和明细相加对不上,或者出现0.30000000000000004这类值。

排查顺序:

  1. 检查数据库字段是否为DECIMAL,而不是FLOATDOUBLE
  2. 检查后端实体字段是否为BigDecimal,而不是Double
  3. 检查前端是否在传参前做了浮点加法。
  4. 检查统计 SQL 是否是SUM(amount),而不是先取出结果在 Java 里循环累加。

解决方案:金额链路从数据库到前端展示全部使用十进制类型,前端展示用toFixed(2),接口传输用字符串。

7.3 数据库连接和时区问题

现象是启动时报时区错误,或者统计报表月份偏移一天。

The server time zone value 'std' is unrecognized

检查连接串中是否加入了serverTimezone=Asia/Shanghai,同时确认 MySQL 服务端时区:

SELECT NOW(); SELECT @@global.time_zone, @@session.time_zone;

如果数据库服务器和业务服务器在不同时区,建议统一改为 UTC 存储,展示时再转换。不要在代码里做一半本地时区、一半数据库时区的混合处理。

7.4 开发环境能跑、生产环境报错的差异化排查

检查项开发环境生产环境
建表方式ddl-auto=updateddl-auto=validate+ 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 安全加固

生产环境至少要做以下加固:

  1. 密码使用 BCrypt 加密,不要允许明文密码登录。
  2. JWT_SECRET长度不少于 32 字符,定期轮换。
  3. 所有接口强制 HTTPS,登录接口不要使用 HTTP 明文传输。
  4. 增加登录失败次数限制,避免暴力破解。
  5. 接口做参数校验,金额、时间、分页参数都要限制范围。
  6. 数据库账号使用最小权限,不直接使用 root 连接业务库。
  7. 定期备份数据库,备份文件加密存放。

对于财务系统,审计日志也很重要。每笔记录的创建、修改、删除都要记录操作人、时间和变更内容,方便后续对账和排查争议。

8.3 发布前检查清单

类别检查项
数据备份是否完整,恢复演练是否通过
配置JWT_SECRET、数据库密码是否通过环境变量注入
安全HTTPS 是否生效,敏感接口是否有鉴权
性能报表接口是否走了数据库聚合,分页是否正常
监控是否接入日志收集、接口耗时监控、错误告警
回滚确定回滚方案,镜像版本是否可快速切换
数据迁移Flyway 脚本是否在测试环境执行通过后再应用到生产

8.4 下一步扩展方向

Procura 的完整链路跑通后,可以按下面的顺序继续扩展:

  1. 多账户支持,区分现金、银行卡、信用卡账户,账目记录增加account_id
  2. 周期记账能力,支持一次性、每周、每月自动生成流水。
  3. 多币种和汇率转换,金额表增加币种字段,统计时统一换算。
  4. 导入导出,支持 Excel、CSV 对账文件导入。
  5. 预算提醒,通过 WebSocket 或短信在超支时通知用户。

每个扩展方向都要回到最初的数据闭环检查:录入、分类、预算、统计之间口径是否仍然一致。如果只加了账户字段,却忘了在报表聚合中按账户维度分组,新功能就会破坏原有统计。这也是 Finance Manager 类项目最值得反复验证的地方:功能可以越加越多,但核心数据链路必须始终稳定。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 14:52:34

Claude Code实战:权限、输入与会话的工程化控制

做 Claude Code 实战时&#xff0c;最影响稳定性的往往不是模型能力&#xff0c;而是权限边界、输入通道和会话生命周期这三个工程细节。权限没配好&#xff0c;CLI 会一直在确认和拒绝之间反复横跳&#xff1b;输入没控制好&#xff0c;长文本、管道数据、多行指令会在中间断掉…

作者头像 李华
网站建设 2026/8/31 14:52:22

Spring Boot项目从ZIP包到成功运行:环境配置与部署避坑指南

简介&#xff1a;这是一套基于SpringBoot开发的校园组团平台完整项目源码&#xff0c;面向高校计算机专业学生、Java后端初学者及Web全栈学习者&#xff0c;旨在解决大学生线上组队开展兴趣活动、学习互助与社会实践的数字化需求。资源包共789个文件&#xff0c;涵盖109个Java后…

作者头像 李华
网站建设 2026/8/31 14:52:15

超声图像标注自动去除的MATLAB实现与批量处理方案

简介&#xff1a;本资源是一套面向医学图像处理初学者与科研人员的MATLAB自动化工具&#xff0c;专为解决超声图像边缘手写标注、测量标记等干扰信息的批量去除问题而设计。适用于超声影像预处理、AI模型训练前的数据清洗及计算机辅助诊断系统开发等场景&#xff0c;无需深度学…

作者头像 李华
网站建设 2026/8/31 14:51:06

YOLOv8固定翼无人机检测:从数据集制作到PyQt部署全流程

简介&#xff1a;本资源面向计算机视觉初学者与无人机应用开发者&#xff0c;提供一套开箱即用的小型固定翼无人机YOLOv8检测解决方案&#xff0c;解决目标检测模型训练难、数据集稀缺、部署界面缺失等实际问题。压缩包共2000个文件&#xff0c;含1892个YOLO格式标注txt文件&am…

作者头像 李华
网站建设 2026/8/31 14:50:18

Hypermesh新界面六面体网格划分案例:分块、扫掠与质量优化

做结构分析的人拿到 Hypermesh 新界面时&#xff0c;第一反应通常不是“界面挺好看”&#xff0c;而是&#xff1a;六面体网格划分这套老本领&#xff0c;在新界面下还能不能按原来的思路做下去&#xff1f;我在用新界面跑完一个带孔支座的六面体网格案例后&#xff0c;一个很直…

作者头像 李华
网站建设 2026/8/31 14:50:15

量化因子库收官:从数据流到可复用因子库的工程化实践

量化金融里最难的不是把一个因子算出来&#xff0c;而是让因子从原始行情数据一路走到因子库之后&#xff0c;还能被信任、被复用、被持续更新。这篇是“365天量化金融”因子阶段的收官内容&#xff0c;核心就两个字&#xff1a;数据流。很多人做量化做了几个月&#xff0c;因子…

作者头像 李华