Java毕业设计实战:基于SpringBoot的催收业务后台系统(含案件分配、绩效统计与多地址联系人管理)

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:直接可运行的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)。
  • serviceCaseService里有assignCase()followUp()closeCase()三个核心方法,对应催收流程的三大动作;PerformanceService里有calculateMonthlyRepayment()getCaseCompletionRate(),直接对应主管日报里的KPI指标。这里没有“通用Service”,每个方法名都在复述业务语言。
  • mapperCaseMapper.java接口里,selectByStatusAndDateRange()方法签名,已经告诉你它要查“某状态下某时间段内的案件”;ContactMapper.javaselectByDebtorIdWithAddresses()@Select注解,明确指向“查债务人所有联系人及关联地址”。SQL不在Java里拼接,全在mapper/*.xml里,方便DBA审核,也方便你调试时直接复制SQL到Navicat里执行。
  • model:实体类字段命名极度务实。Case.java里有private Integer overdueDays; // 逾期天数Contact.java里有private String addressType; // 地址类型:户籍地/常住地/工作地Performance.java里有private BigDecimal repaymentAmount; // 回款金额。没有caseEntitycaseDTO这种抽象名词,只有业务里真实存在的概念。

这种包结构不是为了“看起来规范”,而是为了降低认知负荷。当你被导师问到“案件分配是怎么实现的”,你可以直接说:“在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-forpropscomputed,一个会写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:002024-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:接收employeeIdcaseIds数组,直接分配。
  • 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:fragmentth: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_servercollation_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配置没生效。正确步骤是:

  1. 打开File → Settings → Build, Execution, Deployment → Build Tools → Maven
  2. 确认Maven home path指向你本地安装的Maven(不是IDEA内置的);
  3. User settings file指向你的settings.xml(如果有私服配置);
  4. Local repository路径确认无误;
  5. 点击Apply,然后右键项目根目录 → Maven → Reload project

如果还是报红,检查Project SDKFile → 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.xmlselectList查询里加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_caseidt_repaymentcase_id外键。录音文件可以存在服务器磁盘,路径存到t_caserecording_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 DataSourceapplication-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显示404Thymeleaf模板路径错误,或Controller未被Spring扫描到1. 检查templates/目录下是否有index.html
2. 在Application.java上加@SpringBootApplication(scanBasePackages = "com.example.debt")
3. 查看启动日志,搜索Mapped "{[/],methods=[GET]}",确认首页映射存在
确保index.htmlsrc/main/resources/templates/下,@SpringBootApplicationscanBasePackages包含所有Controller包
Excel导入报错:Invalid header signaturemultiAddTest.xls文件损坏,或被WPS另存为其他格式1. 用记事本打开multiAddTest.xls,看开头是否有乱码;
2. 用WPS或Excel重新“另存为”→“Excel 97-2003工作簿(.xls)”
下载原始资源包里的multiAddTest.xls,不要用其他软件编辑后保存

提示:所有配置文件(application-dev.ymlpom.xml)的缩进必须是空格,不能用Tab,YAML对缩进极其敏感。

5.2 功能类问题深度解析:为什么“结案”按钮点了没反应?

这类问题最折磨人,表面看是前端bug,根源却在后端逻辑。以“案件结案功能失效”为例,学生常遇到:点击“结案”按钮,页面没变化,控制台也没报错。排查路径如下:

  1. 前端网络请求:按F12打开开发者工具,切换到Network标签,点击“结案”,观察是否发出POST /api/case/close请求。如果没有,检查list.html里按钮的onclick事件是否绑定了正确函数,或<form>action属性是否为/api/case/close

  2. 后端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}"/>)。

  3. Service层事务:如果日志有输出,但数据库没变化,检查CaseService.closeCase()是否加了@Transactional。没有它,caseMapper.updateStatus()执行后,事务不提交,数据就回滚了。

  4. 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(图标)。答辩时说:“老师,这套系统最大的特点,就是让技术门槛消失。您今天下午下班前,按这三步走,明天早上就能用上。”——这种笃定,比一百行代码更有说服力。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:直接可运行的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后端入门练手。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值