☰
SpringBoot+Vue财务系统源码实战:从跑通到生产级改造
2026/10/27 7:39:24 网站建设 项目流程

简介:这是一套面向Java初学者与毕业设计学生的SpringBoot财务管理系统完整源码,适用于课程设计、毕设开发及企业级财务管理原型验证。系统覆盖员工管理、工资核算、固定资产管理、序时账与年度利润分析等12个核心模块,采用前后端分离架构(Java+Vue),兼顾业务逻辑完整性与工程实践规范性。资源包共449个文件,含123个Java后端业务类、47个Vue组件页面、27个JPG/PNG界面截图、21个JS交互脚本及16个XML配置文件,辅以SQL建表语句、YML配置、BAT部署脚本和说明文档,结构清晰便于分层学习与二次开发;压缩包大小为17.71MB。已有138人下载学习,读者可直接导入IDEA或Eclipse运行,结合Navicat操作MySQL数据库,并通过配套文档快速掌握环境搭建、模块调用与功能扩展路径,显著降低毕业项目落地门槛。

1. 这不是又一个“学生毕设模板”:SpringBoot+Vue财务系统源码包,为什么能跑通、怎么改得动、哪些模块真能进生产

你下载了一个叫“基于SpringBoot的财务管理系统源代码(Java+Vue+MySQL+说明文档+LW).zip”的压缩包,解压后看到backend/和frontend/两个文件夹、一堆.sql文件、一份 Word 格式的“论文(LW)”和一个README.md——但打开浏览器 localhost:8080 页面空白,控制台报Failed to load resource: net::ERR_CONNECTION_REFUSED;npm run serve启动后 Vue 页面能刷出来,却卡在登录页,F12 看 Network 里/login接口一直 pending;MySQL 导入finance_db.sql提示Unknown collation: 'utf8mb4_0900_ai_ci'……这不是玄学,是典型“可运行但不可维护”的财务系统源码包的真实切面。它不是玩具,而是真实业务逻辑的浓缩体:凭证录入需校验借贷平衡、科目树支持多级展开与禁用状态、报表生成依赖期初/期末/发生额三重数据聚合、权限粒度细到“只看不导出”——这些都不是 CRUD 模板能覆盖的。本文不讲“如何安装 Node.js”,而是带你从 ZIP 包落地那一刻起,把这套代码真正变成你手里的工具:识别哪些模块已稳定可用、哪些必须重构、MySQL 字符集怎么降级兼容、Vue 打包后如何嵌入 SpringBoot 静态资源路径、MyBatis-Plus 的@TableName注解为何在某些表上失效、以及最关键的——当客户突然要求增加“银行流水自动对账”功能时,你该在哪几个文件里动刀、改几行 SQL、加哪两个接口。适合刚接手这类项目的新手快速建立掌控感,也适合想复用其财务核心模型的老手跳过重复造轮子。


2. 后端启动:SpringBoot 项目结构解析与 MySQL 兼容性修复

2.1 识别真实项目结构:别被pom.xml里的spring-boot-starter-web蒙蔽

打开backend/pom.xml,先看<parent>标签下的 SpringBoot 版本号。常见陷阱是:压缩包里写的是2.7.18,但你的本地 Maven 仓库缓存了3.2.0,导致@RestController注解找不到。真实判断依据不是 pom.xml,而是src/main/resources/application.yml中的spring.profiles.active值——如果配置为dev,则实际加载application-dev.yml;若该文件存在且定义了server.port: 8081,那mvn spring-boot:run默认端口就是 8081,而非 8080。
再检查src/main/java/com/example/finance/下的主启动类(通常叫FinanceApplication.java),确认@SpringBootApplication是否包含scanBasePackages = "com.example.finance"。很多源码包为省事直接写scanBasePackages = "com.example",但实际 mapper 接口在com.example.finance.mapper,service 在com.example.finance.service,漏扫会导致@Autowired报NoSuchBeanDefinitionException。

提示:不要盲目执行mvn clean install。先mvn dependency:tree -Dincludes=org.springframework.boot查看实际生效的 SpringBoot 版本,再比对application.yml中spring-boot-starter-parent的版本声明是否一致。不一致时,强制指定本地构建版本:mvn clean compile -Dspring-boot.version=2.7.18

2.2 MySQL 8.0 兼容性血泪现场:utf8mb4_0900_ai_ci必须降级

导入finance_db.sql失败?90% 是字符集问题。MySQL 8.0 默认排序规则utf8mb4_0900_ai_ci在 5.7 及部分云数据库(如阿里云 RDS MySQL 5.7)上不识别。不要重装 MySQL,直接改 SQL 文件:

# 在 finance_db.sql 文件开头插入以下两行(位置:CREATE DATABASE 语句之前) SET NAMES utf8mb4; SET CHARACTER SET utf8mb4; # 将所有出现的 'utf8mb4_0900_ai_ci' 替换为 'utf8mb4_unicode_ci' sed -i 's/utf8mb4_0900_ai_ci/utf8mb4_unicode_ci/g' finance_db.sql

更关键的是建表语句中的COLLATE子句。例如原 SQL:

CREATE TABLE `t_account_subject` ( `id` bigint NOT NULL AUTO_INCREMENT, `code` varchar(32) COLLATE utf8mb4_0900_ai_ci DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

必须改为:

CREATE TABLE `t_account_subject` ( `id` bigint NOT NULL AUTO_INCREMENT, `code` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

注意:utf8mb4_unicode_ci是 MySQL 5.7+ 安全兼容的排序规则,支持 emoji 且排序逻辑稳定。utf8_general_ci已废弃,勿用。

2.3 MyBatis-Plus 配置验证:@TableName与@TableId的隐式陷阱

财务系统中t_voucher(凭证主表)和t_voucher_detail(凭证分录表)必然存在一对多关系。检查TAccountVoucher.java实体类:

@TableName("t_voucher") // ✅ 显式指定表名,避免 MP 自动转下划线失败 public class TAccountVoucher { @TableId(type = IdType.ASSIGN_ID) // ✅ 使用雪花算法,非数据库自增 private Long id; @TableField("voucher_no") private String voucherNo; // ✅ 字段映射正确 // ❌ 错误示范:未标注 @TableField 的 createTime 字段 private Date createTime; // MP 默认按驼峰转 create_time,但数据库字段名是 create_time?还是 created_at? }

验证方法:启动项目后访问/vouchers?page=1&size=10,查看日志中打印的 SQL:

==> Preparing: SELECT id,voucher_no,create_time,... FROM t_voucher WHERE deleted = 0 LIMIT ?

如果 SQL 中字段名是create_time,但实体类字段是createTime,说明 MP 驼峰转换生效;如果 SQL 中是create_time但数据库字段是created_at,则必须加@TableField("created_at")。
血泪经验:财务系统时间字段命名极不统一——create_time/created_at/input_date都可能出现。逐个表检查application.yml中mybatis-plus.configuration.map-underscore-to-camel-case: true是否开启,并对异常字段显式标注@TableField。


3. 前端联调:Vue 项目启动、跨域配置与静态资源嵌入 SpringBoot

3.1 Vue 环境验证:绕过vue-cli-service serve的代理黑洞

frontend/package.json中"scripts": { "serve": "vue-cli-service serve" }是标准启动方式,但财务系统常因 API 基地址硬编码导致联调失败。先检查src/utils/request.js或src/api/index.js:

// ❌ 危险写法:绝对路径写死 const BASE_URL = 'http://localhost:8080/api'; // ✅ 安全写法:读取 .env.development const BASE_URL = process.env.VUE_APP_BASE_API;

对应frontend/.env.development应为:

VUE_APP_BASE_API = '/api'

此时vue-cli-service serve会将/api/**请求代理到http://localhost:8080(SpringBoot 后端)。但代理配置藏在vue.config.js:

module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', // ✅ 必须与后端实际端口一致 changeOrigin: true, pathRewrite: { '^/api': '' // ✅ 去掉 /api 前缀,后端 Controller 才能匹配 @RequestMapping("/login") } } } } }

提示:若后端端口是 8081,此处target必须同步修改,否则所有请求 404。

3.2 Vue 打包嵌入 SpringBoot:dist目录的正确安放位置

生产环境不跑npm run serve,而是npm run build生成dist/,再让 SpringBoot 托管静态资源。关键不是把dist放进resources/static,而是放进src/main/resources/static(注意路径层级)。
验证方法:打包后启动 SpringBoot,访问http://localhost:8080/应返回index.html,且浏览器开发者工具 Network 标签页能看到js/app.xxx.js、css/app.xxx.css加载成功。若 404,请检查:

  • src/main/resources/static下是否存在index.html(由npm run build生成)
  • src/main/resources/static/js/下是否有app.xxx.js(xxx 为哈希值)
  • SpringBoot 是否禁用了默认静态资源路径:检查application.yml中是否有spring.web.resources.static-locations=classpath:/static/,若被注释或修改,需恢复

3.3 登录流程穿透调试:从 Vue 表单到 MyBatis-Plus 查询链路

用户输入账号密码点击登录,前端发请求:

// src/api/login.js export function login(data) { return request({ url: '/auth/login', method: 'post', data // ✅ data 是 { username: 'admin', password: '123456' } }) }

后端AuthController.java接收:

@PostMapping("/login") public ResultVO login(@RequestBody LoginDTO loginDTO) { // ✅ @RequestBody 接收 JSON return authService.login(loginDTO); // ✅ 调用 service }

AuthService.java中关键逻辑:

public ResultVO login(LoginDTO dto) { // 1. 根据 username 查询用户(注意:不是 email!财务系统多用工号/账号) TUser user = userMapper.selectOne(new QueryWrapper<TUser>().eq("username", dto.getUsername())); if (user == null) { return ResultVO.fail("用户名不存在"); } // 2. 密码校验:源码常用 BCryptPasswordEncoder.matches(dto.getPassword(), user.getPassword()) if (!passwordEncoder.matches(dto.getPassword(), user.getPassword())) { return ResultVO.fail("密码错误"); } // 3. 生成 JWT Token(财务系统敏感,Token 有效期建议 ≤ 2h) String token = jwtUtil.generateToken(user); return ResultVO.success().data("token", token); }

排查卡点:若登录按钮无反应,F12 查 Network →auth/login→ Preview,看返回内容。若为空白,说明后端没返回;若返回{"code":500,"msg":"Internal Server Error"},查 SpringBoot 控制台日志,重点看userMapper.selectOne是否抛出NullPointerException(userMapper未注入)或SQLSyntaxErrorException(表名写错)。


4. 财务核心模块避坑:凭证、科目、报表三大高频翻车点

4.1 凭证录入:借贷平衡校验的两种实现与致命漏洞

财务系统最基础也最易崩的逻辑是“有借必有贷,借贷必相等”。源码中常见两种校验方式:

方式一:前端 JS 校验(危险!)
src/views/voucher/CreateVoucher.vue中:

checkBalance() { const debit = this.details.reduce((sum, d) => sum + parseFloat(d.debitAmount || 0), 0); const credit = this.details.reduce((sum, d) => sum + parseFloat(d.creditAmount || 0), 0); if (Math.abs(debit - credit) > 0.01) { // ✅ 用 0.01 避免浮点误差 this.$message.error('借贷不平,请检查!'); return false; } return true; }

致命漏洞:用户禁用 JS 后可绕过校验,直接提交非法数据。必须后端二次校验。

方式二:后端 Service 层校验(必须)
VoucherService.java中:

@Transactional public void saveVoucher(TAccountVoucher voucher, List<TVoucherDetail> details) { // ✅ 关键:先计算总借方、总贷方 BigDecimal totalDebit = details.stream() .map(d -> d.getDebitAmount() != null ? d.getDebitAmount() : BigDecimal.ZERO) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal totalCredit = details.stream() .map(d -> d.getCreditAmount() != null ? d.getCreditAmount() : BigDecimal.ZERO) .reduce(BigDecimal.ZERO, BigDecimal::add); // ✅ 用 BigDecimal.compareTo() 避免浮点误差 if (totalDebit.compareTo(totalCredit) != 0) { throw new BusinessException("凭证借贷不平:" + totalDebit + " ≠ " + totalCredit); } // 继续保存... }

4.2 科目树加载:递归查询的 N+1 问题与 MyBatis-Plus 解法

TAccountSubject表结构通常含id,parent_id,code,name,level,is_disabled。前端需要渲染多级科目树(如:1001 现金 → 1001.01 库存现金 → 1001.02 银行存款)。
常见翻车:Controller 直接调用subjectService.list()返回全部科目,前端用filter+find递归组装树 —— 数据量大时卡死。
正确做法:后端一次查出所有科目,Java 层递归组装:

@Service public class AccountSubjectServiceImpl extends ServiceImpl<AccountSubjectMapper, TAccountSubject> implements AccountSubjectService { @Override public List<TAccountSubject> listSubjectTree() { // 1. 一次性查出所有启用的科目 List<TAccountSubject> allSubjects = this.list( new QueryWrapper<TAccountSubject>().eq("is_disabled", 0) ); // 2. 构建 parentId → 子科目列表 的 Map Map<Long, List<TAccountSubject>> childrenMap = allSubjects.stream() .filter(s -> s.getParentId() != null) // 过滤根节点(parent_id 为 null) .collect(Collectors.groupingBy(TAccountSubject::getParentId)); // 3. 找出根节点(parent_id 为 null 或 0) List<TAccountSubject> roots = allSubjects.stream() .filter(s -> s.getParentId() == null || s.getParentId() == 0) .sorted(Comparator.comparing(TAccountSubject::getCode)) .collect(Collectors.toList()); // 4. 递归设置 children roots.forEach(root -> buildChildren(root, childrenMap)); return roots; } private void buildChildren(TAccountSubject parent, Map<Long, List<TAccountSubject>> childrenMap) { List<TAccountSubject> children = childrenMap.get(parent.getId()); if (children != null && !children.isEmpty()) { children.sort(Comparator.comparing(TAccountSubject::getCode)); parent.setChildren(children); children.forEach(child -> buildChildren(child, childrenMap)); } } }

4.3 报表生成:期初/期末/发生额的 SQL 聚合陷阱

资产负债表、利润表依赖t_voucher_detail表按科目汇总。源码中常见错误 SQL:

-- ❌ 错误:用 SUM(CASE WHEN ... THEN amount ELSE 0 END) 但未 GROUP BY 科目 SELECT subject_code, SUM(CASE WHEN debit_amount IS NOT NULL THEN debit_amount ELSE 0 END) AS debit_total, SUM(CASE WHEN credit_amount IS NOT NULL THEN credit_amount ELSE 0 END) AS credit_total FROM t_voucher_detail WHERE voucher_date BETWEEN '2024-01-01' AND '2024-12-31' GROUP BY subject_code; -- ✅ 必须有 GROUP BY

更致命的是期初余额计算:财务系统要求“期初余额 = 上期期末余额”。源码常写成:

-- ❌ 错误:直接查上月最后一天凭证,忽略未记账凭证 SELECT subject_code, SUM(debit_amount - credit_amount) as balance FROM t_voucher_detail WHERE voucher_date < '2024-01-01' GROUP BY subject_code;

正确逻辑:期初余额应取t_account_balance(余额表)中balance_date = '2023-12-31'的数据。若无余额表,则必须按会计期间闭账逻辑计算——即所有voucher_status = 'posted'(已过账)的凭证。
血泪教训:某次客户上线前测试,发现利润表收入为 0,查日志发现 SQL 中voucher_status = 'draft'(草稿态),而生产环境凭证默认是posted。根源是开发环境未模拟真实过账流程。


5. 权限与安全加固:从角色菜单到敏感操作二次验证

5.1 RBAC 权限模型落地:菜单与按钮级控制的双层校验

财务系统权限必须细粒度。源码中sys_menu表通常含menu_id,parent_id,path,component,perms(如voucher:add,report:export)。
前端控制:src/router/index.js动态路由:

// 根据后端返回的 perms 列表过滤路由 const asyncRoutes = routes.filter(route => { if (route.meta && route.meta.perms) { return store.getters.perms.some(perm => route.meta.perms.includes(perm)) } return true })

后端控制(必须!):@PreAuthorize("hasAuthority('voucher:delete')")注解仅校验权限,不校验数据归属。例如删除凭证时,还需校验该凭证是否属于当前用户:

@DeleteMapping("/vouchers/{id}") @PreAuthorize("hasAuthority('voucher:delete')") public ResultVO deleteVoucher(@PathVariable Long id) { TAccountVoucher voucher = voucherMapper.selectById(id); if (voucher == null) { return ResultVO.fail("凭证不存在"); } // ✅ 二次校验:凭证创建人是否为当前登录用户 if (!voucher.getCreateBy().equals(SecurityUtil.getCurrentUserId())) { return ResultVO.fail("无权删除他人凭证"); } voucherMapper.deleteById(id); return ResultVO.success(); }

5.2 敏感操作二次验证:财务核心动作的防误触设计

“删除凭证”、“结账”、“反结账”等操作必须二次确认。源码中常见简单弹窗:

this.$confirm('确认删除?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => { // 执行删除 })

但这是 UI 层防护,后端必须做相同校验:

  1. 前端提交时带confirmCode(如当前时间戳 MD5)
  2. 后端接口校验confirmCode是否在 60 秒内生成且未使用过
  3. 记录操作日志(谁、何时、删了哪条凭证)
@PostMapping("/vouchers/{id}/delete-confirm") public ResultVO deleteConfirm(@PathVariable Long id, @RequestParam String confirmCode) { // 1. 校验 confirmCode 有效性(Redis 缓存 + 时间戳) String cacheKey = "delete_confirm:" + id; String cachedCode = redisTemplate.opsForValue().get(cacheKey); if (!Objects.equals(cachedCode, confirmCode)) { return ResultVO.fail("确认码无效"); } redisTemplate.delete(cacheKey); // ✅ 用后即焚 // 2. 执行删除 voucherService.removeById(id); return ResultVO.success(); }

5.3 数据脱敏与审计日志:财务系统的合规底线

财务数据严禁明文传输。源码中TUser实体类的password字段必须加@TableField(select = false),且toString()方法排除敏感字段。
审计日志必备字段:操作人 ID、操作时间、IP 地址、操作模块(如“凭证管理”)、操作类型(新增/修改/删除)、操作前数据快照、操作后数据快照。
落地技巧:用 Spring AOP 统一拦截:

@Around("@annotation(org.springframework.web.bind.annotation.PostMapping)") public Object logBeforeReturning(ProceedingJoinPoint joinPoint) throws Throwable { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()) .getRequest(); String ip = getClientIp(request); // 获取被调用方法上的 @LogAnnotation MethodSignature signature = (MethodSignature) joinPoint.getSignature(); LogAnnotation annotation = signature.getMethod().getAnnotation(LogAnnotation.class); if (annotation != null) { AuditLog log = new AuditLog(); log.setOperatorId(SecurityUtil.getCurrentUserId()); log.setIp(ip); log.setModule(annotation.module()); log.setOperation(annotation.operation()); log.setCreateTime(LocalDateTime.now()); // 记录参数(JSON 序列化) log.setParams(JSON.toJSONString(joinPoint.getArgs())); auditLogMapper.insert(log); } return joinPoint.proceed(); }

6. 进阶实战:给现有系统增加“银行流水自动对账”功能

财务人员每天要手工核对银行回单与凭证,效率低易出错。客户临时提出需求:“下周上线银行流水自动对账”。你只有 3 天时间,不能重写系统,必须基于现有代码扩展。以下是我在三个同类项目中验证过的最小可行路径。

6.1 数据模型扩展:新增 2 张表,不动原有结构

在finance_db.sql中追加:

-- 银行流水表(客户上传 CSV 解析后存入) CREATE TABLE `t_bank_statement` ( `id` bigint NOT NULL AUTO_INCREMENT, `bank_name` varchar(50) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '银行名称', `account_no` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '银行账号', `trade_date` date DEFAULT NULL COMMENT '交易日期', `amount` decimal(18,2) DEFAULT NULL COMMENT '金额', `remark` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '摘要', `statement_no` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '流水号', `status` tinyint DEFAULT '0' COMMENT '0-未对账,1-已对账,2-已冲销', `voucher_id` bigint DEFAULT NULL COMMENT '关联凭证ID', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_account_date` (`account_no`,`trade_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; -- 对账明细表(记录每笔流水与凭证的匹配关系) CREATE TABLE `t_reconciliation_detail` ( `id` bigint NOT NULL AUTO_INCREMENT, `statement_id` bigint NOT NULL, `voucher_id` bigint NOT NULL, `match_type` tinyint DEFAULT '1' COMMENT '1-金额+日期,2-摘要关键词,3-人工指定', `match_score` decimal(5,2) DEFAULT '0.00' COMMENT '匹配得分(0-100)', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_statement_voucher` (`statement_id`,`voucher_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

注意:t_bank_statement.status字段复用现有系统状态码风格(0/1/2),避免新增枚举类;t_reconciliation_detail.match_type为后续智能匹配留扩展空间。

6.2 后端接口开发:3 个核心接口,复用现有技术栈

接口 1:上传银行流水 CSV

@PostMapping("/bank-statements/upload") public ResultVO uploadStatement(@RequestParam("file") MultipartFile file) { try { // 1. 读取 CSV(用 OpenCSV,避免 Apache Commons CSV 的空行 bug) List<BankStatementDTO> statements = CsvUtils.readBankStatement(file.getInputStream()); // 2. 批量插入(MyBatis-Plus 的 saveBatch) List<TBankStatement> entities = statements.stream() .map(dto -> { TBankStatement bs = new TBankStatement(); bs.setBankName(dto.getBankName()); bs.setAccountNo(dto.getAccountNo()); bs.setTradeDate(dto.getTradeDate()); bs.setAmount(dto.getAmount()); bs.setRemark(dto.getRemark()); bs.setStatementNo(dto.getStatementNo()); return bs; }).collect(Collectors.toList()); bankStatementService.saveBatch(entities); return ResultVO.success().data("count", entities.size()); } catch (Exception e) { log.error("上传银行流水失败", e); return ResultVO.fail("上传失败:" + e.getMessage()); } }

接口 2:执行自动对账(核心算法)
对账逻辑分三层匹配:

匹配层级规则权重示例
L1:金额+日期精确匹配amount相等且trade_date = voucher_date100%流水 1000 元,2024-05-20 → 凭证 1000 元,2024-05-20
L2:金额模糊匹配+摘要关键词ABS(amount - voucher_amount) < 10且remark LIKE '%工资%'ANDvoucher_summary LIKE '%工资%'80%流水 9998.5 元,摘要“工资”,凭证摘要“员工工资发放”
L3:金额区间匹配amount在voucher_amount * 0.95 ~ voucher_amount * 1.05内60%流水 1050 元,凭证 1000 元(含手续费)
@PostMapping("/bank-statements/reconcile") public ResultVO reconcile() { // 1. 查出所有未对账流水 List<TBankStatement> unMatched = bankStatementService.list( new QueryWrapper<TBankStatement>().eq("status", 0) ); // 2. 查出近30天未对账凭证(借贷方含银行科目) List<TAccountVoucher> vouchers = voucherService.list( new QueryWrapper<TAccountVoucher>() .ge("voucher_date", LocalDate.now().minusDays(30)) .eq("status", "posted") // 已过账凭证 .inSql("id", "SELECT DISTINCT voucher_id FROM t_voucher_detail WHERE subject_code LIKE '1001%' OR subject_code LIKE '1002%'") ); // 3. 三层匹配(伪代码,实际用 Stream API 实现) for (TBankStatement bs : unMatched) { Optional<TAccountVoucher> matched = vouchers.stream() .filter(v -> matchLevel1(bs, v)) // 金额+日期 .findFirst(); if (matched.isPresent()) { // 创建对账明细,更新流水状态 reconciliationDetailService.save(new TReconciliationDetail(...)); bankStatementService.update( new UpdateWrapper<TBankStatement>().eq("id", bs.getId()).set("status", 1).set("voucher_id", matched.get().getId()) ); } } return ResultVO.success(); }

接口 3:人工干预对账

@PostMapping("/reconciliation/manual-match") public ResultVO manualMatch(@RequestBody ManualMatchDTO dto) { // dto 包含 statementId, voucherId, matchType // 1. 检查是否已存在对账记录 int count = reconciliationDetailService.count( new QueryWrapper<TReconciliationDetail>() .eq("statement_id", dto.getStatementId()) .eq("voucher_id", dto.getVoucherId()) ); if (count > 0) { return ResultVO.fail("已存在匹配关系"); } // 2. 创建人工匹配记录 TReconciliationDetail detail = new TReconciliationDetail(); detail.setStatementId(dto.getStatementId()); detail.setVoucherId(dto.getVoucherId()); detail.setMatchType(dto.getMatchType()); detail.setMatchScore(new BigDecimal(100)); // 人工指定为满分 reconciliationDetailService.save(detail); // 3. 更新流水状态 bankStatementService.update( new UpdateWrapper<TBankStatement>() .eq("id", dto.getStatementId()) .set("status", 1) .set("voucher_id", dto.getVoucherId()) ); return ResultVO.success(); }

6.3 前端集成:复用现有菜单与权限体系

在src/router/index.js中新增路由:

{ path: '/reconciliation', component: Layout, redirect: '/reconciliation/list', name: 'Reconciliation', meta: { title: '银行对账', icon: 'money', perms: ['reconciliation:list'] }, children: [ { path: 'list', name: 'ReconciliationList', component: () => import('@/views/reconciliation/List'), meta: { title: '对账列表', perms: ['reconciliation:list'] } }, { path: 'upload', name: 'ReconciliationUpload', component: () => import('@/views/reconciliation/Upload'), meta: { title: '上传流水', perms: ['reconciliation:upload'] } } ] }

对应sys_menu表插入:

INSERT INTO sys_menu (menu_name, parent_id, path, component, perms, order_num, status) VALUES ('银行对账', 0, '/reconciliation', 'Layout', 'reconciliation:list', 6, 0), ('对账列表', 100, '/reconciliation/list', 'reconciliation/List', 'reconciliation:list', 1, 0), ('上传流水', 100, '/reconciliation/upload', 'reconciliation/Upload', 'reconciliation:upload', 2, 0);

关键技巧:前端List.vue页面直接复用VoucherList.vue的表格组件,只替换api调用地址和列定义(statement_no,amount,trade_date,status),节省 80% 开发时间。

我做过最狠的一次:客户周五下午提需求,我周六上午拉出源码包,下午完成数据库变更和后端接口,周日早上交付前端页面,周一上午培训财务人员使用。核心就三点:不动原有架构、只增不改、用好 MyBatis-Plus 的 Wrapper 和 Stream API。那些说“老系统没法改”的人,往往只是没找到杠杆支点。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询