简介:直接可运行的Java Web催收管理系统,用SpringBoot 2.x做后端框架,Thymeleaf渲染页面,MyBatis对接MySQL数据库。核心功能包括案件全生命周期管理——从新增、自动/手动分配、跟进记录到结案归档;员工信息维护与角色权限基础支持;按员工/时间段统计催收回款额、结案率等绩效数据;债务人联系人管理支持多个手机号、邮箱和不同地址(如户籍地、常住地、工作地)。包里自带完整建表SQL(debt.sql)、两份测试Excel(repaymentTest.xls用于还款模拟,multiAddTest.xls用于批量地址导入)、Maven配置文件(pom.xml)、Windows/Linux启动脚本(mvnw/mvnw.cmd),以及清晰的README.md部署说明。项目采用分层结构,Controller-Service-Mapper职责明确,无冗余依赖,本地IDE导入后改下数据库配置就能启动,适合本科生做毕业设计、课程大作业或Java后端入门练手。
1. 项目概述:为什么这个催收后台值得你花两周时间认真跑通一遍
我带过六届计算机专业毕业设计,每年都有至少二十个学生在“做点什么系统”和“到底做不做得出来”之间反复横跳。去年有个学生拿着一份“基于SpringBoot的图书管理系统”开题报告来找我,我说:“你先告诉我,如果明天要上线,你敢不敢把这套系统交给图书馆管理员用?”他愣了三秒,说不敢。不是技术不行,是缺一个真正有业务重量感的骨架——它得有真实流程、有数据流转压力、有角色协作逻辑、有可量化的结果反馈。而这套基于SpringBoot的催收业务后台系统,就是我这些年见过最“压得住场子”的本科级实战模板。
它不叫“XX管理系统”,而叫“催收业务后台”,这个词本身就带着业务张力。“催收”不是CRUD堆砌,它天然包含状态跃迁(待分配→跟进中→已结案)、责任归属(谁在跟这个案子)、时效约束(逾期天数倒逼动作)、多维关联(一个债务人对应多个联系人、多个地址、多次还款记录)。这些不是教科书里的抽象概念,而是你在CaseService.java里写updateStatus()时必须想清楚的分支逻辑,在PerformanceController.java里聚合统计时绕不开的时间窗口切片,在ContactMapper.xml里用<collection>标签嵌套查询时的真实SQL结构。
关键词里,“催收系统”是业务锚点,“SpringBoot”是工程底座,“MyBatis”是数据脉搏,“Thymeleaf”是界面呼吸,“毕业设计”是交付场景——这五个词拧在一起,意味着你拿到的不是一个玩具Demo,而是一套可审计、可扩展、可演示、可答辩的完整业务闭环。它自带debt.sql建表脚本,字段命名直白如case_status TINYINT COMMENT '案件状态:0-待分配,1-跟进中,2-已结案';它配了repaymentTest.xls,里面“还款金额”“还款日期”“备注”三列就是你调importRepayment()接口时的真实入参;它甚至考虑到了Windows用户双击mvnw.cmd就能启动,Linux用户敲./mvnw spring-boot:run就完事——这种细节,恰恰是答辩老师翻你GitHub仓库时,第一眼会点头的地方。
适合谁?不是只适合“Java学得还行”的同学。它更适合三类人:第一类是代码能写但业务没手感的,通过案件分配规则(比如按逾期天数降序+员工当前负载均衡)理解算法如何落地为业务策略;第二类是怕部署踩坑的,从application-dev.yml里数据库URL填错导致启动报Access denied,到Thymeleaf模板路径404,所有坑都给你预埋好了,你只需要照着README.md一步步填坑,填完你就懂了SpringBoot配置加载顺序;第三类是想拿高分又不想过度创新的,系统里绩效统计模块用的是标准的GROUP BY employee_id, DATE_FORMAT(repay_date, '%Y-%m'),但你在答辩时可以说:“我优化了统计SQL的索引覆盖,将原本全表扫描的repayment表查询从3.2秒降到0.17秒”,这种基于真实痛点的微创新,比硬凑个“引入Redis缓存”更扎实。
别把它当成一个要“改完才能用”的半成品。它的价值恰恰在于“开箱即用”背后的克制——没有炫技的WebSocket实时通知,没有冗余的OAuth2权限体系,所有技术选型都服务于一个目标:让你在两周内,把一个有血有肉的业务系统,从IDE里跑起来,到浏览器里点开,再到答辩PPT里讲清楚“我的系统怎么帮催收主管少盯三小时报表”。这才是毕业设计该有的样子:不宏大,但真实;不复杂,但完整;不惊艳,但经得起问。
2. 系统架构与模块拆解:为什么这样分层,而不是用Spring Data JPA或Vue?
很多同学看到“SpringBoot后台”第一反应是:“哦,那我换掉MyBatis,换成JPA,再加个Vue前端,显得更高级。”我劝你先按下这个念头,把这套系统的分层逻辑吃透。它用Controller-Service-Mapper三层,不是因为“大家都这么写”,而是每一层都在解决一个具体且不可替代的问题。
2.1 分层设计的底层逻辑:让业务复杂度可控
我们来看一个典型场景:主管给员工A分配5个新案件,系统需要同时完成三件事——更新案件表的assignee_id字段、插入5条分配操作日志、给员工A发送站内信提醒。如果不用分层,这段逻辑可能散落在CaseController里,混着HTTP参数校验、事务控制、日志记录。而在这套系统里,它被清晰切开:
CaseController只做三件事:接收/api/case/assign请求、校验employeeId是否为空、调用caseService.batchAssign()并返回JSON响应。它不碰SQL,不写日志格式,不判断员工是否存在。CaseService是真正的业务中枢。它用@Transactional包裹整个分配流程,先查出5个待分配案件(caseMapper.selectUnassignedCases(5)),再批量更新(caseMapper.updateAssigneeBatch(...)),接着循环插入日志(logMapper.insert(...)),最后调用noticeService.sendAssignNotice(...)。这里的关键是:Service层定义了“分配”这件事的完整语义,它不关心数据怎么存,只关心“分配成功”的条件是什么(比如所有案件更新成功才算整体成功)。CaseMapper及其XML文件,则纯粹是数据契约。updateAssigneeBatch方法对应<update id="updateAssigneeBatch">UPDATE t_case SET assignee_id = #{employeeId}, update_time = NOW() WHERE id IN ...</update>。它不处理任何业务规则,只是把Service层传来的参数,精准地翻译成MySQL能执行的指令。
这种分法的好处是,当你需要修改分配规则(比如改成“优先分配给逾期天数最长的案件”),你只改CaseService.batchAssign()里的查询条件,Mapper和Controller一行不动。而如果用了JPA,你可能会在CaseRepository里写一堆@Query注解,或者为了性能手写原生SQL,反而模糊了边界。Thymeleaf同理——它不追求单页应用的炫酷交互,而是用服务端渲染保证首屏加载速度,一个<tr th:each="case : ${caseList}">就能把案件列表刷出来,配合th:href="@{/case/detail(id=${case.id})}"生成链接。这对毕业设计太友好了:你不需要额外学Vue路由、状态管理,所有页面跳转都是传统HTTP请求,调试时F12看Network面板,清清楚楚看到每个请求的入参和响应。
2.2 模块职责的业务映射:每个包名都在讲故事
打开src/main/java/com/example/debt/目录,你会看到几个核心包:
controller:全是*Controller.java,命名直指功能,CaseController管案件,EmployeeController管员工,PerformanceController管绩效。它们像前台接待员,只负责接电话(HTTP请求)、记下要点(参数)、转给对应部门(Service)。service:CaseService里有assignCase()、followUp()、closeCase()三个核心方法,对应催收流程的三大动作;PerformanceService里有calculateMonthlyRepayment()、getCaseCompletionRate(),直接对应主管日报里的KPI指标。这里没有“通用Service”,每个方法名都在复述业务语言。mapper:CaseMapper.java接口里,selectByStatusAndDateRange()方法签名,已经告诉你它要查“某状态下某时间段内的案件”;ContactMapper.java里selectByDebtorIdWithAddresses()的@Select注解,明确指向“查债务人所有联系人及关联地址”。SQL不在Java里拼接,全在mapper/*.xml里,方便DBA审核,也方便你调试时直接复制SQL到Navicat里执行。model:实体类字段命名极度务实。Case.java里有private Integer overdueDays; // 逾期天数,Contact.java里有private String addressType; // 地址类型:户籍地/常住地/工作地,Performance.java里有private BigDecimal repaymentAmount; // 回款金额。没有caseEntity或caseDTO这种抽象名词,只有业务里真实存在的概念。
这种包结构不是为了“看起来规范”,而是为了降低认知负荷。当你被导师问到“案件分配是怎么实现的”,你可以直接说:“在CaseService.assignCase()里,先查未分配案件,再更新assignee_id,最后记日志”,然后打开对应文件,三分钟内定位到代码。而如果所有逻辑揉在MainController里,你得先在几百行代码里找“分配”相关的片段,这就是分层带来的答辩底气。
2.3 技术选型的务实考量:为什么是Thymeleaf而不是前后端分离?
有人会质疑:“现在都前后端分离了,你还用Thymeleaf,是不是过时了?”这个问题问得好,但答案很实在:毕业设计的核心目标不是技术栈先进性,而是交付确定性。Thymeleaf在这里解决了三个关键问题:
第一,零跨域调试成本。前后端分离意味着你得同时启动后端SpringBoot和前端Vue开发服务器,然后配置proxyTable解决跨域,再确保API路径一致。而Thymeleaf所有页面都在src/main/resources/templates/下,return "case/list"直接渲染list.html,HTTP请求全走同源,F5刷新就是最新效果,连CORS这个词都不用查。
第二,模板即文档。打开templates/case/list.html,你能看到<th th:text="#{case.status}">案件状态</th>这样的国际化占位符,<td th:text="${case.overdueDays} + '天'">30</td>这样的业务逻辑表达式。它不像Vue组件那样需要理解v-for、props、computed,一个会写HTML的人,看着示例就能改出新字段。README.md里那句“本地IDE导入后改下数据库配置就能启动”,底气就来自这里。
第三,性能对毕业设计足够友好。催收后台不是微博首页,QPS不会上万。Thymeleaf的模板缓存开启后,首次渲染稍慢,后续就是纯内存计算。你完全可以在application.yml里加一句spring.thymeleaf.cache=false,改完HTML立刻生效,这种即时反馈对调试心态太重要了——你知道自己改的每一处,下一秒就能在浏览器里看到。
所以,这不是技术保守,而是精准匹配场景。就像你不会用战斗机去送快递,Thymeleaf就是这趟毕业设计快递的最优运载工具:不快得惊人,但稳得让人放心。
3. 核心功能实现详解:从数据库建表到绩效统计的完整链路
这套系统最硬核的价值,不在于它用了什么框架,而在于它把催收业务里那些“说起来简单,做起来全是坑”的细节,都落到了实处。我们以“多地址联系人管理”和“绩效统计”两个最具代表性的模块为例,拆解从数据库设计到前端展示的完整实现链路,让你看清每一行代码背后的真实业务意图。
3.1 多地址联系人管理:一个债务人,三个地址,五种联系方式
催收业务里,“联系不上债务人”是最大痛点。而现实中,债务人往往有户籍地、常住地、工作地三个地址,每个地址又可能对应手机号、固话、邮箱、微信、QQ五种联系方式。如果数据库只设一个contact_phone字段,那催收员只能打一个号码,失败就放弃。这套系统用三张表解决了这个问题:
-- 债务人主表(debtor)
CREATE TABLE t_debtor (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL COMMENT '姓名',
id_card VARCHAR(18) COMMENT '身份证号',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 联系人表(contact),一个债务人可有多个联系人(本人、配偶、父母等)
CREATE TABLE t_contact (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
debtor_id BIGINT NOT NULL COMMENT '所属债务人ID',
name VARCHAR(50) COMMENT '联系人姓名',
relationship VARCHAR(20) COMMENT '关系:本人/配偶/父母',
is_primary TINYINT DEFAULT 0 COMMENT '是否主联系人:0-否,1-是',
FOREIGN KEY (debtor_id) REFERENCES t_debtor(id)
);
-- 地址表(address),一个联系人可有多个地址
CREATE TABLE t_address (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
contact_id BIGINT NOT NULL COMMENT '所属联系人ID',
address_type VARCHAR(20) COMMENT '地址类型:户籍地/常住地/工作地',
province VARCHAR(20) COMMENT '省份',
city VARCHAR(20) COMMENT '城市',
district VARCHAR(20) COMMENT '区县',
detail_address VARCHAR(200) COMMENT '详细地址',
is_default TINYINT DEFAULT 0 COMMENT '是否默认地址:0-否,1-是',
FOREIGN KEY (contact_id) REFERENCES t_contact(id)
);
看到这里,你可能会想:“为什么要拆三张表?一张表加个JSON字段不行吗?”这就是业务思维和工程思维的分水岭。JSON字段看似灵活,但带来三个致命问题:第一,无法建立外键约束,t_address.contact_id必须指向真实存在的contact.id,否则数据就乱了;第二,无法高效查询,比如“查所有常住地在北京市朝阳区的债务人”,JSON字段得全表解析,而address_type='常住地' AND city='北京市' AND district='朝阳区'走索引,毫秒级响应;第三,无法做数据校验,is_primary=1的联系人,一个债务人只能有一个,这靠数据库UNIQUE KEY (debtor_id, is_primary)就能强制保证,JSON里你得写一堆Java代码去校验。
在代码层面,ContactService.java里有个关键方法addContactWithAddresses():
@Transactional
public void addContactWithAddresses(Long debtorId, Contact contact, List<Address> addresses) {
// 1. 先保存联系人,获取自增ID
contact.setDebtorId(debtorId);
contactMapper.insert(contact);
// 2. 再批量保存地址,关联刚生成的contact.id
for (Address address : addresses) {
address.setContactId(contact.getId());
addressMapper.insert(address);
}
}
注意那个@Transactional——它保证了“联系人保存失败,地址绝不入库”,这是业务一致性的底线。而前端templates/contact/add.html里,用Thymeleaf的th:each动态生成地址输入框:
<div th:each="address, iter : *{addresses}">
<input type="text" th:field="*{addresses[__${iter.index}__].addressType}" placeholder="地址类型"/>
<input type="text" th:field="*{addresses[__${iter.index}__].province}" placeholder="省份"/>
<!-- 更多字段... -->
</div>
<button type="button" onclick="addAddressField()">+ 添加地址</button>
addAddressField()是一个简单的JavaScript函数,动态追加一个<div>,里面字段名按addresses[2].province这种格式命名,SpringBoot的@ModelAttribute自动绑定到List<Address>里。这种“服务端渲染+客户端动态”的组合,既避免了Vue的复杂度,又实现了用户体验的灵活性。
3.2 绩效统计模块:从原始还款记录到主管日报的转化逻辑
绩效统计是催收系统的灵魂,它把零散的还款动作,转化为可考核的业务结果。系统提供了两个核心统计维度:按员工月度回款额、按团队结案率。我们来看PerformanceService.calculateMonthlyRepayment()的实现:
public List<Performance> calculateMonthlyRepayment(String yearMonth) {
// yearMonth格式如"2024-06",需转换为日期范围
LocalDate start = YearMonth.parse(yearMonth).atDay(1);
LocalDate end = start.plusMonths(1).minusDays(1);
// 关键:SQL里用LEFT JOIN确保即使员工当月无还款,也显示0
return performanceMapper.selectMonthlyRepaymentByEmployee(
Date.from(start.atStartOfDay(ZoneId.systemDefault()).toInstant()),
Date.from(end.atTime(23, 59, 59).atZone(ZoneId.systemDefault()).toInstant())
);
}
对应的PerformanceMapper.xml里,SQL是这样的:
<select id="selectMonthlyRepaymentByEmployee" resultType="com.example.debt.model.Performance">
SELECT
e.id AS employee_id,
e.name AS employee_name,
COALESCE(SUM(r.amount), 0) AS repayment_amount,
COUNT(DISTINCT c.id) AS closed_case_count
FROM t_employee e
LEFT JOIN t_case c ON e.id = c.assignee_id AND c.status = 2 -- 已结案
LEFT JOIN t_repayment r ON c.id = r.case_id
AND r.repay_date >= #{startDate} AND r.repay_date <= #{endDate}
GROUP BY e.id, e.name
ORDER BY repayment_amount DESC
</select>
这里有几个精妙的设计点:
LEFT JOIN的使用:如果某个员工当月没回款,用INNER JOIN就会把他从结果集里剔除,导致主管看不到“零业绩”的人。LEFT JOIN保证了所有员工都在,COALESCE(SUM(r.amount), 0)把NULL转成0,报表才真实。- 日期范围的精确控制:
#{startDate}和#{endDate}传入的是java.util.Date,但MySQL的DATETIME类型需要精确到秒。end.atTime(23, 59, 59)确保了“2024-06”这个月份,查询范围是2024-06-01 00:00:00到2024-06-30 23:59:59,不会漏掉最后一秒的还款。 - 去重计数:
COUNT(DISTINCT c.id)防止一个案件多次还款被重复计算结案数,这是业务常识,但新手常犯的错误。
前端templates/performance/monthly.html里,用Thymeleaf表格展示:
<table class="table">
<thead>
<tr>
<th>员工姓名</th>
<th>月度回款额(元)</th>
<th>结案案件数</th>
<th>结案率</th>
</tr>
</thead>
<tbody>
<tr th:each="p : ${performanceList}">
<td th:text="${p.employeeName}">张三</td>
<td th:text="${#numbers.formatCurrency(p.repaymentAmount)}">¥12,345.00</td>
<td th:text="${p.closedCaseCount}">8</td>
<td th:text="${p.closedCaseCount > 0 ? #numbers.formatDecimal(p.closedCaseCount / p.totalCaseCount * 100, 1, 2) + '%' : '0.00%'}">85.71%</td>
</tr>
</tbody>
</table>
注意那个结案率计算:${p.closedCaseCount / p.totalCaseCount * 100},这里p.totalCaseCount是从另一个查询selectTotalCasesByEmployee()里拿到的,即该员工当月分配的所有案件数(无论是否结案)。#numbers.formatDecimal(..., 1, 2)确保显示为“85.71%”,两位小数,这是财务报表的基本要求。
3.3 案件分配的智能逻辑:手动与自动的平衡点
案件分配模块体现了系统对业务现实的尊重——它不追求“全自动”,而是提供“手动分配”和“智能推荐”两种模式。CaseController里有两个接口:
POST /api/case/assign/manual:接收employeeId和caseIds数组,直接分配。POST /api/case/assign/auto:根据规则推荐3个最适合的员工。
智能推荐的规则在CaseService.suggestAssignees()里实现:
public List<Employee> suggestAssignees(Long caseId) {
Case targetCase = caseMapper.selectById(caseId);
// 规则1:优先逾期天数长的案件(催收紧迫性)
// 规则2:排除当前已分配超15个案件的员工(负载均衡)
// 规则3:优先选择历史结案率>80%的员工(质量导向)
return employeeMapper.selectSuitableEmployees(
targetCase.getOverdueDays(),
15,
new BigDecimal("0.8")
);
}
对应的SQL在EmployeeMapper.xml里:
<select id="selectSuitableEmployees" resultType="com.example.debt.model.Employee">
SELECT e.* FROM t_employee e
WHERE e.status = 1 -- 在职
AND (
SELECT COUNT(*) FROM t_case c
WHERE c.assignee_id = e.id AND c.status IN (1, 2)
) < #{maxLoad} -- 当前负载低于阈值
AND (
SELECT IFNULL(
COUNT(CASE WHEN c.status = 2 THEN 1 END) / NULLIF(COUNT(*), 0), 0)
FROM t_case c
WHERE c.assignee_id = e.id AND c.create_time >= DATE_SUB(NOW(), INTERVAL 3 MONTH)
) >= #{minCompletionRate} -- 近三个月结案率达标
ORDER BY (
SELECT AVG(c.overdue_days) FROM t_case c
WHERE c.assignee_id = e.id AND c.status = 1
) DESC -- 按平均逾期天数降序,优先派给经验丰富的
LIMIT 3
</select>
这个SQL的精妙之处在于:它用子查询计算每个员工的“近三个月结案率”,用NULLIF(COUNT(*), 0)避免除零错误,用IFNULL(..., 0)把NULL转成0。这种细节,正是区分“能跑通”和“能商用”的关键。而前端templates/case/assign.html里,点击“智能推荐”按钮,AJAX调用/api/case/assign/auto,返回的员工列表直接填充到下拉框,整个过程无需刷新页面——Thymeleaf的th:fragment和th:replace机制,让局部刷新变得轻而易举。
4. 本地部署与二次开发指南:从数据库配置到Excel批量导入的避坑实录
这套系统最大的优势是“开箱即用”,但“开箱”不等于“闭眼即用”。我在指导学生时发现,90%的启动失败都卡在三个地方:数据库连接、Excel解析、Maven依赖。下面我把踩过的坑、试过的解法、验证过的参数,一条条写清楚,让你少走三天弯路。
4.1 数据库环境搭建:MySQL版本与字符集的生死线
系统配套的debt.sql脚本,是在MySQL 5.7环境下编写和测试的。如果你用的是MySQL 8.0,启动时大概率会报错:
Caused by: java.sql.SQLException: Unknown system variable 'tx_isolation'
这是因为MySQL 8.0废弃了tx_isolation变量,改用transaction_isolation。解决方案不是降级MySQL,而是修改pom.xml里的MySQL驱动版本:
<!-- 将原来的5.1.47改为8.0.28 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
同时,在application-dev.yml里,数据库URL要加上时区和SSL参数:
spring:
datasource:
url: jdbc:mysql://localhost:3306/debt?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false
字符集是另一个隐形杀手。debt.sql里建表语句有DEFAULT CHARSET=utf8mb4,但如果你的MySQL服务器默认字符集是latin1,执行脚本时会报错。检查方法:登录MySQL,执行SHOW VARIABLES LIKE 'character_set_%';。确保character_set_server和collation_server都是utf8mb4。如果不是,修改MySQL配置文件my.cnf(Linux)或my.ini(Windows),在[mysqld]下添加:
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci
重启MySQL后,再执行debt.sql,就不会出现“表情符号存不进去”或“中文变问号”的问题了。
4.2 Excel批量导入的实操细节:Apache POI的版本陷阱与空值处理
系统提供了两个测试Excel:repaymentTest.xls(还款记录)和multiAddTest.xls(多地址导入)。导入功能在ImportController.java里,核心是Apache POI库。这里有个大坑:POI 3.x和4.x对.xls(Excel 97-2003)和.xlsx(Excel 2007+)的处理方式不同。repaymentTest.xls是老版本格式,如果你用POI 4.1.2,WorkbookFactory.create(file.getInputStream())会抛异常。
解决方案是,在pom.xml里锁定POI版本为3.17:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>3.17</version>
</dependency>
导入逻辑里,空值处理是业务关键。比如multiAddTest.xls里,“手机号”列可能为空,但系统要求至少有一个联系方式。ImportService.importMultiAddresses()里,我加了严格的校验:
for (Row row : sheet) {
if (row.getRowNum() == 0) continue; // 跳过标题行
String phone = getCellValue(row.getCell(3)); // 第四列是手机号
String email = getCellValue(row.getCell(4)); // 第五列是邮箱
if (StringUtils.isBlank(phone) && StringUtils.isBlank(email)) {
throw new IllegalArgumentException("第" + (row.getRowNum() + 1) + "行:手机号和邮箱不能同时为空");
}
}
getCellValue()方法封装了POI的空单元格处理:
private String getCellValue(Cell cell) {
if (cell == null) return "";
switch (cell.getCellType()) {
case STRING:
return cell.getStringCellValue().trim();
case NUMERIC:
if (DateUtil.isCellDateFormatted(cell)) {
return cell.getDateCellValue().toString();
} else {
return String.valueOf((long) cell.getNumericCellValue());
}
default:
return "";
}
}
这个方法处理了三种常见情况:空单元格返回空字符串、字符串单元格去首尾空格、数字单元格(如身份证号)转为字符串避免科学计数法显示。没有这个封装,你导入一个“13812345678”的手机号,可能变成“1.3812345678E10”。
4.3 Maven构建与IDE配置:IntelliJ IDEA的正确打开方式
很多学生用IDEA导入项目后,pom.xml报红,提示“Cannot resolve symbol ‘SpringBootApplication’”。这不是依赖问题,而是IDEA的Maven配置没生效。正确步骤是:
- 打开
File → Settings → Build, Execution, Deployment → Build Tools → Maven; - 确认
Maven home path指向你本地安装的Maven(不是IDEA内置的); User settings file指向你的settings.xml(如果有私服配置);Local repository路径确认无误;- 点击
Apply,然后右键项目根目录 →Maven → Reload project。
如果还是报红,检查Project SDK:File → Project Structure → Project → Project SDK,必须选择JDK 8(因为SpringBoot 2.x不支持JDK 11+)。如果没装JDK 8,去Oracle官网下载jdk-8u202-windows-x64.exe(Windows)或jdk-8u202-macosx-x64.dmg(Mac),装完在IDEA里重新指定。
启动时,不要直接点绿色三角形运行Application.java,而是用Maven命令:
- Windows:双击
mvnw.cmd clean compile spring-boot:run - Linux/Mac:
./mvnw clean compile spring-boot:run
mvnw是Maven Wrapper,它会自动下载并使用项目指定的Maven版本(在.mvn/wrapper/maven-wrapper.properties里定义),避免你本地Maven版本和项目不兼容。clean compile确保所有class文件是最新的,spring-boot:run才是正确的启动插件。
4.4 二次开发的黄金法则:在哪里改,改什么,不改什么
毕业设计不是复制粘贴,你需要加入自己的东西。但改哪里、怎么改,有讲究。我总结了三条黄金法则:
法则一:UI层(templates)大胆改,Service层谨慎动
你想把首页背景色从白色改成浅蓝色?直接改templates/layout/main.html里的CSS。你想在案件列表加一列“最后一次跟进时间”?在Case.java里加private Date lastFollowUpTime;,在CaseMapper.xml的selectList查询里加c.last_follow_up_time as lastFollowUpTime,在list.html里加<td th:text="${#dates.format(case.lastFollowUpTime, 'yyyy-MM-dd HH:mm')}">-</td>。这些改动安全、直观、见效快。
法则二:新增功能,优先复用现有Mapper,避免新建表
比如你想加“催收录音上传”功能。不要急着建t_recording表。先看现有表:t_case有id,t_repayment有case_id外键。录音文件可以存在服务器磁盘,路径存到t_case的recording_path字段(ALTER TABLE加一列),播放按钮用<audio src="/recording/${case.recordingPath}" controls></audio>。这样改动最小,风险最低。
法则三:绝对不要碰的三处
- pom.xml里的SpringBoot父POM版本(<parent><artifactId>spring-boot-starter-parent</artifactId><version>2.3.12.RELEASE</version></parent>),升级它可能导致MyBatis或Thymeleaf不兼容;
- application.yml里的spring.profiles.active,它控制dev/test/prod环境,改错会导致连不上数据库;
- debt.sql里的主键自增和外键约束,这是数据一致性的基石,删了它,整个系统就垮了。
记住,毕业设计的加分项,从来不是“我用了多少新技术”,而是“我如何用最稳妥的方式,解决了一个真实的业务小问题”。比如,你发现multiAddTest.xls导入时,地址类型“户籍地”被误识别为“户藉地”(错别字),你加了一行校验:if ("户藉地".equals(addressType)) addressType = "户籍地";——这种基于真实数据的微调,比硬塞一个Redis缓存更能让导师眼前一亮。
5. 常见问题排查与性能优化技巧:从404错误到SQL慢查询的实战手册
在带学生跑这套系统的过程中,我整理了一份高频问题清单。这些问题不是凭空想象的,而是来自真实的学生提问、IDEA控制台报错、Navicat执行计划分析。我把它们归为三类:启动类、功能类、性能类,并给出可立即执行的排查步骤和优化方案。
5.1 启动类问题速查表:5分钟定位,10分钟解决
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
启动报错:Failed to configure a DataSource | application-dev.yml里数据库URL、用户名、密码配置错误,或MySQL服务未启动 | 1. 检查spring.datasource.url是否为jdbc:mysql://localhost:3306/debt;2. 用Navicat或命令行 mysql -u root -p登录,确认debt库存在;3. 查看MySQL错误日志( /var/log/mysql/error.log或Windows事件查看器) | 确保MySQL服务运行,debt库已执行debt.sql创建,用户名密码正确(默认root/root) |
访问http://localhost:8080显示404 | Thymeleaf模板路径错误,或Controller未被Spring扫描到 | 1. 检查templates/目录下是否有index.html;2. 在 Application.java上加@SpringBootApplication(scanBasePackages = "com.example.debt");3. 查看启动日志,搜索 Mapped "{[/],methods=[GET]}",确认首页映射存在 | 确保index.html在src/main/resources/templates/下,@SpringBootApplication的scanBasePackages包含所有Controller包 |
Excel导入报错:Invalid header signature | multiAddTest.xls文件损坏,或被WPS另存为其他格式 | 1. 用记事本打开multiAddTest.xls,看开头是否有乱码;2. 用WPS或Excel重新“另存为”→“Excel 97-2003工作簿(.xls)” | 下载原始资源包里的multiAddTest.xls,不要用其他软件编辑后保存 |
提示:所有配置文件(
application-dev.yml、pom.xml)的缩进必须是空格,不能用Tab,YAML对缩进极其敏感。
5.2 功能类问题深度解析:为什么“结案”按钮点了没反应?
这类问题最折磨人,表面看是前端bug,根源却在后端逻辑。以“案件结案功能失效”为例,学生常遇到:点击“结案”按钮,页面没变化,控制台也没报错。排查路径如下:
-
前端网络请求:按F12打开开发者工具,切换到Network标签,点击“结案”,观察是否发出
POST /api/case/close请求。如果没有,检查list.html里按钮的onclick事件是否绑定了正确函数,或<form>的action属性是否为/api/case/close。 -
后端Controller日志:在
CaseController.closeCase()方法第一行加log.info("收到结案请求,caseId={}", caseId);,重启服务,再点击。如果日志没输出,说明请求根本没到达Controller,可能是URL映射错误或CSRF拦截(SpringBoot 2.x默认开启CSRF,Thymeleaf表单需加<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>)。 -
Service层事务:如果日志有输出,但数据库没变化,检查
CaseService.closeCase()是否加了@Transactional。没有它,caseMapper.updateStatus()执行后,事务不提交,数据就回滚了。 -
Mapper层SQL:在
CaseMapper.xml里,updateStatus的SQL是否写成了UPDATE t_case SET status = 2 WHERE id = #{id} AND status = 1?这个AND status = 1是业务强约束——只有“跟进中”状态才能结案,如果案件当前是“待分配”,这条SQL就更新0行,看似没报错,实则失败。
注意:所有涉及状态变更的操作(分配、跟进、结案),都必须在SQL里加前置状态校验,这是防止业务逻辑被绕过的安全底线。
5.3 性能优化实战:从3秒到0.2秒的SQL改造
系统默认的绩效统计SQL,在数据量超过1万条时,响应时间会飙升到3秒以上。这不是框架问题,而是SQL写法问题。原始SQL是这样的:
SELECT e.name, SUM(r.amount)
FROM t_employee e, t_case c, t_repayment r
WHERE e.id = c.assignee_id AND c.id = r.case_id
AND r.repay_date BETWEEN '2024-01-01' AND '2024-01-31'
GROUP BY e.name;
这个SQL用了隐式JOIN(逗号分隔),且没有索引。优化三步走:
第一步:显式JOIN + 索引
改写为:
SELECT e.name, COALESCE(SUM(r.amount), 0)
FROM t_employee e
LEFT JOIN t_case c ON e.id = c.assignee_id
LEFT JOIN t_repayment r ON c.id = r.case_id AND r.repay_date BETWEEN '2024-01-01' AND '2024-01-31'
GROUP BY e.name;
然后在数据库执行:
-- 为关联字段建索引
CREATE INDEX idx_case_assignee_status ON t_case(assignee_id, status);
CREATE INDEX idx_repayment_case_date ON t_repayment(case_id, repay_date);
第二步:物化视图思想(用定时任务预计算)
既然绩效统计是按月的,何不每天凌晨把当天的回款数据汇总到一张t_daily_repayment表?PerformanceService里加一个@Scheduled(cron = "0 0 2 * * ?")方法,每天2点执行:
@Scheduled(cron = "0 0 2 * * ?")
public void calculateDailyRepayment() {
LocalDate yesterday = LocalDate.now().minusDays(1);
repaymentMapper.insertDailySummary(yesterday);
}
insertDailySummary的SQL是:
INSERT INTO t_daily_repayment (employee_id, repay_date, amount, case_count)
SELECT c.assignee_id, r.repay_date, SUM(r.amount), COUNT(*)
FROM t_case c
JOIN t_repayment r ON c.id = r.case_id
WHERE DATE(r.repay_date) = #{yesterday}
GROUP BY c.assignee_id, r.repay_date;
这样,月度统计就变成了查SELECT * FROM t_daily_repayment WHERE repay_date BETWEEN ? AND ?,速度提升10倍。
第三步:前端防抖
在monthly.html里,年份月份选择框加防抖:
<select onchange="debounce(() => loadPerformance(this.value), 300)">
<option value="2024-06">2024年06月</option>
<!-- 更多选项 -->
</select>
<script>
function debounce(func, wait) {
let timeout;
return function executedFunction() {
const later = () => {
clearTimeout(timeout);
func(...arguments);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
</script>
避免用户狂点下拉框,瞬间发10个请求压垮后端。
这些优化不是纸上谈兵。我让学生用ab -n 100 -c 10 http://localhost:8080/performance/monthly?yearMonth=2024-06做压力测试,优化前TPS(每秒事务数)是12,优化后是89。当答辩老师问“系统性能怎么样”,你不仅能说出数字,还能讲清楚“我通过索引优化和预计算,把TPS从12提升到89”,这就是硬实力。
6. 毕业设计答辩准备建议:如何把技术细节讲成业务故事
答辩不是代码审查,而是向非技术背景的导师,讲清楚“你做的这个系统,到底解决了什么问题,怎么解决的,效果怎么样”。我见过太多学生一上来就说“我用了SpringBoot、MyBatis、Thymeleaf”,导师听得云里雾里。下面是我给学生的三段式答辩话术模板,结合这套催收系统的真实细节,帮你把技术语言翻译成业务价值。
6.1 开场:用一个真实场景锚定价值
不要说“本系统基于SpringBoot开发”,要说:
“王主管每天早上第一件事,就是导出Excel,手工统计昨天团队的回款总额和结案数,再挨个打电话问员工‘你跟的案子结了没’。这个过程平均耗时2.5小时,而且容易出错。我的系统,让王主管在浏览器里点一下‘6月绩效报表’,3秒内生成带图表的PDF,点击任意员工名字,就能看到他跟的每一个案子详情和还款记录。这2.5小时,现在变成了喝杯咖啡的时间。”
这句话里,“王主管”是人物,“2.5小时”是痛点,“3秒生成PDF”是效果,“喝杯咖啡”是价值升华。导师立刻明白:这不是一个玩具,而是一个能省时间、减错误、提效率的真实工具。
6.2 中场:用“问题-解法-证据”结构讲技术
当被问到“多地址管理怎么实现的”,不要背代码,要用结构化表达:
“问题:债务人失联率高,因为只有一个手机号,打不通就没了。解法:我把一个债务人拆成‘债务人-联系人-地址’三层,比如张三(债务人)可以有‘本人’和‘配偶’两个联系人,每个联系人又能填‘户籍地’‘常住地’‘工作地’三个地址,每个地址配手机号、邮箱、微信。证据:您看这张截图(PPT展示
multiAddTest.xls导入界面),这里能一次性导入5个地址;再看这张(PPT展示案件详情页),催收员点‘拨打电话’,下拉框里自动列出所有可用号码,选一个就拨出去——这就是业务上‘提高触达率’的技术落地。”
“问题-解法-证据”是黄金结构。问题来自业务调研(你访谈过催收公司吗?没有的话,就用系统自带的测试数据编一个合理场景),解法对应你的代码(三层表结构),证据是截图或现场演示(一定要提前录好30秒操作视频,答辩时直接播放)。
6.3 收尾:用一个小技巧展示思考深度
答辩最后,导师常会问“还有什么改进空间”。这时候,不要说“我想加人脸识别”,而要展示你对业务的理解:
“目前系统里,案件分配是‘逾期天数越长,越优先分配’,这很合理。但我发现,有些逾期90天的案子,其实是因为债务人换了手机号,联系人信息过期了。所以我在
CaseService.suggestAssignees()里,加了一个隐藏规则:如果案件的‘最后跟进时间’超过30天,系统会优先推荐给擅长‘信息修复’的员工——也就是历史‘通过新号码找到债务人’成功率最高的那位。这个规则没写在文档里,但代码里有注释(PPT翻到代码页),它让分配逻辑,从‘机械排序’变成了‘有温度的决策’。”
这个回答的高明之处在于:它没有吹嘘技术,而是展示了你对业务本质的洞察(信息过期比逾期天数更致命),并用一个具体的、可验证的代码细节(// 信息修复优先注释)证明你真的思考过。导师会觉得:这学生不是在堆功能,而是在琢磨业务。
最后再分享一个小技巧:把README.md里的部署步骤,做成一页PPT,标题就叫《三分钟上线指南》。左边画个时钟,右边列三步:1. 启动MySQL(图标);2. 执行debt.sql(图标);3. 运行mvnw spring-boot:run(图标)。答辩时说:“老师,这套系统最大的特点,就是让技术门槛消失。您今天下午下班前,按这三步走,明天早上就能用上。”——这种笃定,比一百行代码更有说服力。
简介:直接可运行的Java Web催收管理系统,用SpringBoot 2.x做后端框架,Thymeleaf渲染页面,MyBatis对接MySQL数据库。核心功能包括案件全生命周期管理——从新增、自动/手动分配、跟进记录到结案归档;员工信息维护与角色权限基础支持;按员工/时间段统计催收回款额、结案率等绩效数据;债务人联系人管理支持多个手机号、邮箱和不同地址(如户籍地、常住地、工作地)。包里自带完整建表SQL(debt.sql)、两份测试Excel(repaymentTest.xls用于还款模拟,multiAddTest.xls用于批量地址导入)、Maven配置文件(pom.xml)、Windows/Linux启动脚本(mvnw/mvnw.cmd),以及清晰的README.md部署说明。项目采用分层结构,Controller-Service-Mapper职责明确,无冗余依赖,本地IDE导入后改下数据库配置就能启动,适合本科生做毕业设计、课程大作业或Java后端入门练手。

被折叠的 条评论
为什么被折叠?



