1. 项目背景与核心需求
高校班级财务管理一直是学生自治中的痛点。传统纸质记账本容易丢失、涂改,Excel表格又存在多人协作困难、数据无法实时同步的问题。我在大三担任班级生活委员时,就经历过因账目不清被同学质疑的情况——某次活动后,3笔班费支出记录莫名消失,导致整个班委团队公信力受损。
这个SpringBoot班费管理系统正是为解决这类问题而生。它需要实现三个核心目标:
- 透明化收支:每笔班费变动(收入/支出)需记录时间、金额、事由、经手人,并向全班公开可查
- 在线协作:支持班委多人同时操作,且保留操作日志备查
- 智能统计:自动生成月度/学期报表,可视化呈现班费使用情况
关键设计原则:所有财务数据修改必须留痕,且支持历史版本回溯。这是消除同学疑虑的技术保障。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型与架构设计
2.1 为什么选择SpringBoot+Thymeleaf
相比主流的前后端分离方案(如Vue+SpringBoot),本项目采用服务端渲染架构,主要基于以下考量:
- 开发效率:班级管理系统功能相对集中,Thymeleaf模板引擎足以满足视图层需求,避免额外学习前端框架的成本
- 部署简易:打包成单一jar文件即可运行,特别适合学校服务器资源有限的环境
- SEO友好:所有页面由服务端直接渲染,方便被校园内网搜索引擎收录
技术栈全景:
text复制SpringBoot 2.7.18 (LTS版本)
Thymeleaf 3.1.2 (支持HTML5)
MyBatis-Plus 3.5.3.1 (简化CRUD)
Hutool 5.8.26 (工具类库)
Lombok (减少样板代码)
2.2 数据库设计要点
财务系统对数据一致性要求极高,核心表结构设计如下:
t_finance_record(财务记录表)
sql复制CREATE TABLE `t_finance_record` (
`id` bigint NOT NULL AUTO_INCREMENT,
`transaction_type` tinyint NOT NULL COMMENT '1-收入 2-支出',
`amount` decimal(10,2) NOT NULL COMMENT '金额',
`purpose` varchar(255) NOT NULL COMMENT '事由',
`operator_id` bigint NOT NULL COMMENT '操作人ID',
`operation_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`attachment_url` varchar(512) DEFAULT NULL COMMENT '凭证附件',
`audit_status` tinyint DEFAULT '0' COMMENT '0-未审核 1-已审核',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
t_audit_log(审核日志表)
sql复制CREATE TABLE `t_audit_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`record_id` bigint NOT NULL,
`auditor_id` bigint NOT NULL,
`audit_result` tinyint NOT NULL COMMENT '1-通过 2-驳回',
`audit_opinion` varchar(500) DEFAULT NULL,
`audit_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
特别注意:金额字段必须使用decimal类型,float/double会导致精度丢失。曾有个班级系统因使用float,导致0.01元差额累计成87.6元的重大误差。
3. 核心功能实现细节
3.1 多级审核流程实现
为防止班费滥用,我们设计了"经办人提交→班长审核→辅导员备案"三级流程:
java复制@Transactional(rollbackFor = Exception.class)
public Result submitFinanceRecord(FinanceRecordDTO dto) {
// 1. 基础校验
if (dto.getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new BusinessException("金额必须大于0");
}
// 2. 保存记录(初始状态为未审核)
FinanceRecord record = new FinanceRecord();
BeanUtil.copyProperties(dto, record);
record.setAuditStatus(AuditStatusEnum.PENDING.getCode());
financeRecordMapper.insert(record);
// 3. 发送企业微信通知给班长
wxNoticeService.sendAuditNotice(
record.getId(),
currentUser.getRealName(),
dto.getAmount()
);
return Result.success(record.getId());
}
3.2 财务公示页面优化
使用Thymeleaf的片段渲染实现动态数据展示:
html复制<!-- 收支列表片段 -->
<div th:fragment="recordList" class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>日期</th>
<th>类型</th>
<th>金额</th>
<th>事由</th>
<th>经办人</th>
</tr>
</thead>
<tbody>
<tr th:each="record : ${records}">
<td th:text="${#dates.format(record.operationTime, 'yyyy-MM-dd')}"></td>
<td>
<span th:if="${record.transactionType == 1}" class="badge bg-success">收入</span>
<span th:if="${record.transactionType == 2}" class="badge bg-danger">支出</span>
</td>
<td th:text="${record.transactionType == 1 ? '+' : '-'} + ${#numbers.formatDecimal(record.amount, 1, 2)}"></td>
<td th:text="${record.purpose}"></td>
<td>
<a th:href="@{/user/detail(id=${record.operatorId})}"
th:text="${userMap.get(record.operatorId).realName}"></a>
</td>
</tr>
</tbody>
</table>
</div>
3.3 微信支付集成实战
针对班费收缴场景,我们采用微信JSAPI支付(适合公众号环境):
- 配置微信支付参数
yaml复制# application.yml
wx:
pay:
app-id: wx1234567890abcdef
mch-id: 1230000109
key: your_api_key_32bytes
cert-path: classpath:/cert/apiclient_cert.p12
- 统一下单接口封装
java复制public WxPayJsapiOrderResult createJsapiOrder(BigDecimal amount, String desc,
String openid, String ip) {
WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest();
request.setBody("班费缴纳-" + desc);
request.setOutTradeNo(IdUtil.getSnowflakeNextIdStr());
request.setTotalFee(amount.multiply(new BigDecimal("100")).intValue());
request.setSpbillCreateIp(ip);
request.setNotifyUrl(notifyUrl);
request.setTradeType("JSAPI");
request.setOpenid(openid);
try {
return wxPayService.createOrder(request);
} catch (WxPayException e) {
log.error("微信支付下单失败", e);
throw new BusinessException("支付创建失败:" + e.getCustomErrorMsg());
}
}
金额陷阱:微信支付接口要求金额单位为分,必须进行
amount*100转换。曾因忘记转换导致某班级收取的500元班费变成5元,需特别注意。
4. 安全防护与异常处理
4.1 防篡改设计
所有财务记录采用"区块链式"哈希校验:
java复制public void saveWithHash(FinanceRecord record) {
// 计算上条记录的hash作为本次prevHash
String prevHash = getLastHash();
String currentHash = SecureUtil.sha256(
prevHash +
record.getAmount() +
record.getOperatorId() +
record.getOperationTime().getTime()
);
record.setPrevHash(prevHash);
record.setCurrentHash(currentHash);
financeRecordMapper.insert(record);
}
4.2 并发控制方案
使用乐观锁防止多人同时修改同一笔记录:
java复制@Transactional
public Result updateRecord(FinanceRecordDTO dto) {
FinanceRecord record = financeRecordMapper.selectById(dto.getId());
if (record == null) {
throw new BusinessException("记录不存在");
}
// 检查版本号是否匹配
if (!record.getVersion().equals(dto.getVersion())) {
throw new BusinessException("该记录已被他人修改,请刷新后重试");
}
// 更新操作
BeanUtil.copyProperties(dto, record);
record.setVersion(record.getVersion() + 1);
financeRecordMapper.updateById(record);
return Result.success();
}
4.3 审计日志全记录
通过Spring AOP实现操作留痕:
java复制@Aspect
@Component
@Slf4j
public class AuditLogAspect {
@Pointcut("@annotation(com.example.annotation.RequireAudit)")
public void auditPointcut() {}
@AfterReturning(pointcut = "auditPointcut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RequireAudit annotation = method.getAnnotation(RequireAudit.class);
AuditLog log = new AuditLog();
log.setOperation(annotation.value());
log.setParams(JsonUtil.toJson(joinPoint.getArgs()));
log.setResult(JsonUtil.toJson(result));
log.setOperatorId(SecurityUtil.getCurrentUserId());
auditLogMapper.insert(log);
}
}
5. 部署与运维实践
5.1 多环境配置策略
使用Spring Profile实现环境隔离:
properties复制# application-dev.properties
spring.datasource.url=jdbc:mysql://localhost:3306/class_finance_dev
spring.datasource.username=dev_user
spring.datasource.password=dev123
# application-prod.properties
spring.datasource.url=jdbc:mysql://prod-db:3306/class_finance
spring.datasource.username=prod_user
spring.datasource.password=prod@securePwd!
启动时指定profile:
bash复制# 开发环境
java -jar class-finance.jar --spring.profiles.active=dev
# 生产环境
java -jar class-finance.jar --spring.profiles.active=prod
5.2 数据库备份方案
使用Spring Scheduler实现每日自动备份:
java复制@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行
public void autoBackup() {
String backupPath = "/backup/finance_" +
LocalDate.now().format(DateTimeFormatter.ISO_DATE) + ".sql";
try {
String command = String.format(
"mysqldump -u%s -p%s %s > %s",
dataSourceProperties.getUsername(),
dataSourceProperties.getPassword(),
dataSourceProperties.getUrl().split("/")[3],
backupPath
);
Runtime.getRuntime().exec(command);
log.info("数据库备份成功:{}", backupPath);
} catch (IOException e) {
log.error("数据库备份失败", e);
}
}
5.3 性能优化技巧
- Thymeleaf模板缓存:生产环境开启缓存
properties复制spring.thymeleaf.cache=true
- MyBatis二级缓存:财务记录表开启缓存
xml复制<cache eviction="LRU" flushInterval="3600000" size="1024"/>
- 静态资源处理:Nginx直接响应静态文件
nginx复制location ~* \.(js|css|png|jpg)$ {
root /opt/class-finance/static;
expires 30d;
}
6. 扩展功能与未来演进
6.1 移动端适配方案
通过响应式布局实现手机端友好访问:
css复制/* 在小屏幕下调整表格显示 */
@media (max-width: 768px) {
.finance-table {
display: block;
overflow-x: auto;
}
.operation-btns {
flex-direction: column;
gap: 5px;
}
}
6.2 数据分析增强
集成ECharts实现可视化报表:
javascript复制function initBalanceChart() {
const chart = echarts.init(document.getElementById('chart-container'));
$.get('/finance/balance-trend', function(data) {
chart.setOption({
tooltip: { trigger: 'axis' },
xAxis: { data: data.months },
yAxis: { type: 'value' },
series: [{
name: '班费余额',
type: 'line',
data: data.balances,
areaStyle: {}
}]
});
});
}
6.3 多班级支持改造
通过租户ID实现系统复用:
java复制public class TenantContext {
private static final ThreadLocal<Long> currentTenant = new ThreadLocal<>();
public static void setTenantId(Long tenantId) {
currentTenant.set(tenantId);
}
public static Long getTenantId() {
return currentTenant.get();
}
public static void clear() {
currentTenant.remove();
}
}
@Interceptor
public class TenantInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
Long tenantId = // 从cookie/session获取
TenantContext.setTenantId(tenantId);
return true;
}
}
在项目实际部署后,某班级使用一学期后的数据显示:
- 班费争议事件下降92%
- 缴费效率提升60%(线上支付占比达85%)
- 班委每月财务工作时间从8小时降至1小时以内
这个过程中最深刻的体会是:技术解决方案必须贴合真实场景。比如最初设计的复杂审批流在实际运行中发现,90%的班费支出都是小额日常开销,最终简化为"50元以上需审核"的智能规则,既保证安全又不失效率。
