1. 项目背景与核心需求
在企业管理信息化进程中,费用结算管理模块作为财务系统的核心组件,承担着企业日常运营中各类费用审批、核算与支付的关键职能。传统基于Excel或手工记账的方式存在数据孤岛、流程不透明、易出错等痛点,而采用SpringBoot构建的B/S架构解决方案能有效解决这些问题。
这个毕业设计项目的核心目标是通过SpringBoot框架实现一个具备完整生命周期的费用结算管理系统,具体需要满足以下业务场景:
- 多角色协同工作(申请人、部门主管、财务人员、系统管理员)
- 全流程电子化审批(从费用申请到最终支付)
- 智能校验与规则引擎(防止超额报销、重复报销等违规操作)
- 多维度的统计分析与报表生成
提示:在实际企业环境中,费用结算模块往往需要与OA系统、ERP系统进行集成,毕业设计版本可适当简化,但需要保留必要的扩展接口。
2. 技术选型与架构设计
2.1 技术栈组成
基础框架选择SpringBoot 2.7.x版本,这是目前企业级开发中最稳定的选择。配套技术组件包括:
- 持久层:MyBatis-Plus 3.5.x(简化CRUD操作)
- 数据库:MySQL 8.0(事务型场景的首选)
- 前端:Thymeleaf + Bootstrap 5(适合毕业设计的轻量级方案)
- 安全控制:Spring Security(基于角色的访问控制)
- 工作流引擎:Activiti 7(处理审批流程)
- 缓存:Redis(高频访问数据缓存)
2.2 系统架构设计
采用典型的三层架构,但针对费用结算场景做了特殊优化:
code复制表现层(Web)
↓
业务逻辑层(Service)
↓
数据访问层(DAO)
↑
基础服务层(审批流引擎、规则引擎、消息通知)
特别需要注意的是费用结算业务特有的"双流分离"设计:
- 业务流:申请→审批→支付→记账
- 凭证流:电子发票验证→凭证生成→凭证归档
2.3 数据库设计要点
核心表结构设计示例(简化版):
sql复制CREATE TABLE `expense_application` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`applicant_id` varchar(32) NOT NULL COMMENT '申请人ID',
`department_id` varchar(32) NOT NULL COMMENT '部门ID',
`expense_type` tinyint NOT NULL COMMENT '费用类型(1差旅 2采购 3招待...)',
`amount` decimal(12,2) NOT NULL COMMENT '申请金额',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态(0草稿 1审批中 2已批准...)',
`attachment_ids` varchar(512) DEFAULT NULL COMMENT '附件ID集合',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_applicant` (`applicant_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='费用申请表';
注意:实际项目中需要考虑分表策略,特别是交易流水表可能快速增长的情况。
3. 核心功能实现细节
3.1 审批流程集成
使用Activiti实现可配置的审批流程是项目的关键难点。建议采用以下实现方案:
- 流程定义配置(bpmn文件示例):
xml复制<process id="expenseApproval" name="费用审批流程">
<startEvent id="start"/>
<userTask id="deptLeaderAudit" name="部门领导审批"/>
<sequenceFlow sourceRef="start" targetRef="deptLeaderAudit"/>
<exclusiveGateway id="gateway1"/>
<sequenceFlow sourceRef="deptLeaderAudit" targetRef="gateway1"/>
<sequenceFlow sourceRef="gateway1" targetRef="financeAudit">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${amount >= 5000}]]>
</conditionExpression>
</sequenceFlow>
<userTask id="financeAudit" name="财务复核"/>
<sequenceFlow sourceRef="gateway1" targetRef="end">
<conditionExpression xsi:type="tFormalExpression">
<![CDATA[${amount < 5000}]]>
</conditionExpression>
</sequenceFlow>
<endEvent id="end"/>
</process>
- SpringBoot集成代码示例:
java复制@RestController
@RequestMapping("/process")
public class ProcessController {
@Autowired
private RuntimeService runtimeService;
@PostMapping("/start")
public Result startProcess(@RequestBody ExpenseApplication application) {
Map<String, Object> variables = new HashMap<>();
variables.put("applicantId", application.getApplicantId());
variables.put("amount", application.getAmount());
ProcessInstance instance = runtimeService.startProcessInstanceByKey(
"expenseApproval",
application.getId().toString(),
variables
);
return Result.success(instance.getId());
}
}
3.2 费用规则引擎实现
为了防止违规报销,需要实现智能校验规则。推荐采用Drools规则引擎:
- 规则定义示例(drl文件):
drl复制rule "差旅费每日限额"
when
$e : ExpenseApplication(expenseType == 1, amount > 1000)
then
throw new BusinessException("差旅费单日限额1000元");
end
rule "部门预算检查"
when
$e : ExpenseApplication()
$b : Budget(departmentId == $e.departmentId,
remainingAmount < $e.amount)
then
throw new BusinessException("部门预算不足");
end
- SpringBoot集成配置:
java复制@Configuration
public class DroolsConfig {
@Bean
public KieContainer kieContainer() {
KieServices ks = KieServices.Factory.get();
KieFileSystem kfs = ks.newKieFileSystem();
kfs.write(ResourceFactory.newClassPathResource("rules/expense-rules.drl"));
KieBuilder kb = ks.newKieBuilder(kfs);
kb.buildAll();
return ks.newKieContainer(kb.getKieModule().getReleaseId());
}
}
4. 典型问题与解决方案
4.1 并发审批冲突处理
当多个审批人同时处理同一申请时可能出现状态冲突。解决方案:
- 乐观锁实现:
java复制@Transactional
public void approve(Long applicationId, String approverId) {
ExpenseApplication app = applicationMapper.selectById(applicationId);
if (app.getStatus() != 1) {
throw new BusinessException("申请状态已变更");
}
app.setStatus(2);
app.setApproverId(approverId);
app.setVersion(app.getVersion() + 1); // 版本号控制
int affected = applicationMapper.updateById(app);
if (affected == 0) {
throw new OptimisticLockingFailureException("审批冲突,请刷新重试");
}
// 记录审批日志
approvalLogService.recordApproval(app);
}
- 分布式锁方案(使用Redisson):
java复制public void approveWithLock(Long applicationId, String approverId) {
RLock lock = redissonClient.getLock("expense:approve:" + applicationId);
try {
boolean locked = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (locked) {
// 执行业务逻辑
approve(applicationId, approverId);
}
} finally {
lock.unlock();
}
}
4.2 大数据量导出优化
费用报表导出可能涉及大量数据,需要特殊处理:
- 分页查询+流式导出:
java复制@GetMapping("/export")
public void exportExpenses(HttpServletResponse response,
@RequestParam Map<String, Object> params) {
response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment;filename=expenses.xlsx");
try (ExcelWriter writer = ExcelUtil.getWriter();
OutputStream out = response.getOutputStream()) {
int pageSize = 5000;
int pageNum = 1;
while (true) {
Page<ExpenseVO> page = new Page<>(pageNum, pageSize);
IPage<ExpenseVO> result = expenseService.pageExpenses(params, page);
if (pageNum == 1) {
writer.writeHeadRow(getHeaderRow());
}
if (result.getRecords().isEmpty()) {
break;
}
writer.write(result.getRecords());
if (result.getRecords().size() < pageSize) {
break;
}
pageNum++;
}
writer.flush(out, true);
}
}
- 异步导出方案:
java复制@PostMapping("/async-export")
public Result asyncExport(@RequestBody ExportRequest request) {
String taskId = UUID.randomUUID().toString();
CompletableFuture.runAsync(() -> {
// 执行导出逻辑
exportService.doExport(taskId, request);
}, exportExecutor);
return Result.success(taskId);
}
@GetMapping("/export-status/{taskId}")
public Result getExportStatus(@PathVariable String taskId) {
ExportStatus status = exportService.getStatus(taskId);
return Result.success(status);
}
5. 项目扩展与优化方向
5.1 微服务化改造
当系统规模扩大时,可考虑拆分为独立服务:
- 用户服务(统一认证)
- 费用服务(核心业务)
- 审批流服务(流程引擎)
- 报表服务(数据分析)
使用Spring Cloud Alibaba实现方案:
yaml复制# application.yml示例
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
sentinel:
transport:
dashboard: 127.0.0.1:8080
5.2 前端架构升级
从Thymeleaf迁移到Vue.js前后端分离架构:
- 后端接口改造:
java复制@RestController
@RequestMapping("/api/expense")
public class ExpenseApiController {
@GetMapping
public PageResult<ExpenseVO> list(ExpenseQuery query) {
IPage<ExpenseVO> page = expenseService.queryExpenses(query);
return new PageResult<>(page);
}
@PostMapping
public Result create(@Valid @RequestBody ExpenseDTO dto) {
Long id = expenseService.createExpense(dto);
return Result.success(id);
}
}
- 前端API调用示例(Vue3 + Axios):
javascript复制import axios from 'axios';
const api = axios.create({
baseURL: '/api',
timeout: 10000
});
export const getExpenseList = (params) => {
return api.get('/expense', { params });
};
export const createExpense = (data) => {
return api.post('/expense', data);
};
5.3 监控与性能优化
集成Prometheus监控指标:
java复制@Configuration
public class MetricsConfig {
@Bean
MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags(
"application", "expense-system"
);
}
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
@Service
public class ExpenseService {
@Timed(value = "expense.process.time",
description = "Time taken to process expense")
public void processExpense(Expense expense) {
// 业务逻辑
}
}
对于高频访问的预算余额查询,可采用多级缓存策略:
- 本地缓存(Caffeine):<100ms
- Redis集群缓存:<300ms
- 数据库查询:最后保障
实现代码示例:
java复制public BigDecimal getDepartmentBudget(String deptId) {
return budgetCache.get(deptId, () -> {
String redisKey = "budget:" + deptId;
String cached = redisTemplate.opsForValue().get(redisKey);
if (cached != null) {
return new BigDecimal(cached);
}
Budget budget = budgetMapper.selectByDept(deptId);
if (budget != null) {
redisTemplate.opsForValue().set(
redisKey,
budget.getRemainingAmount().toString(),
5, TimeUnit.MINUTES
);
return budget.getRemainingAmount();
}
return BigDecimal.ZERO;
});
}
在开发过程中,我发现几个特别容易出错的地方值得注意:
- 金额计算必须使用BigDecimal,禁止使用double类型
- 审批流版本升级时,需要处理运行中的流程实例
- 导出功能必须做好防注入处理,特别是SQL注入和文件路径注入
- 定时任务需要做好幂等处理,防止重复执行
