资讯动态

催收业务系统设计与落地:从数据模型到分案、追踪与报表

发布时间:2026/10/12 22:49:52 来源:尧图企业网站定制
简介这是一份「一诺银华催收业务系统」的软件详细设计说明书及工程文件合集面向金融催收领域开发人员、Java企业级应用学习者及相关业务系统设计者。文档对系统整体概况、业务办理流程和功能需求进行了明确定义涵盖案件管理、催收作业、人员权限等模块并清晰说明了技术选型、实现环境与非功能性需求。rar压缩包共749个文件以Java源码、class编译文件、JSP页面、JavaScript脚本、XML配置文件及PNG界面截图为主辅以SQL数据库脚本与少量jar依赖资源整体约25.27MB目录结构完整便于按模块分析学习。目前已有3417人浏览学习适合用来拆解催收类业务系统的数据模型与分层设计思路也可为类似信贷管理系统的设计与开发提供直接参考。从内容预览可见核心类涵盖案件信息、员工操作、催收导出等业务模块覆盖控制层、业务层与数据访问层便于对照说明书快速定位关键实现。1. 一诺银华催收业务系统从“案件分不下去”到“每一分回款都有记录”做催收业务系统的人都有同感真正难的不是催收话术而是案件到了系统里之后怎么分配、怎么追踪、怎么不遗漏。一诺银华催收业务系统这类平台核心就是把“案件导入—分案—外呼—承诺—还款核销”这一长串动作收口到一套可配置、可审计的流程里让主管一眼看出哪个坐席在干活、哪个案件躺在池子里没人碰。对从业者来说它解决三件事案件不被漏分、外呼行为有痕迹、还款承诺到期有人追。本文按我做过同类系统的落地顺序来拆先讲数据模型怎么建再讲分案与触达策略接着讲工作台和追踪流程最后把上线后最容易翻车的几个坑和报表口径摊开讲。适合正在选型、准备自研或接手维护这类系统的读者。2. 先把地基立住催收业务领域模型与关键数据表2.1 催收案件、外呼任务、还款承诺三张核心表怎么设计我一般不会一上来就画几十张表的完整 ER 图而是先定三张“命根子”表案件表、外呼任务表、还款承诺表。其余的表都是围绕这三张表做扩展。案件表是主数据外呼任务表记录每一次触达动作还款承诺表则单独拆出来因为它决定了回款预测和后续追踪。案件表最容易被设计错的地方是把“借款人联系方式”直接存在案件表里几个手机号用逗号拼一个字段。这种做法在演示阶段没毛病但上了量之后你要做号码优先级、做免打扰过滤、做三网空号检测逗号分隔的字段根本没法支撑。正确做法是拆出联系人子表每个案件挂多个联系人每个联系人挂多条号码记录号码带类型标签主叫号码、备用号码、紧急联系人。CREATE TABLE collection_case ( case_id BIGINT PRIMARY KEY AUTO_INCREMENT, case_no VARCHAR(32) NOT NULL COMMENT 案件编号来自委托方, customer_name VARCHAR(64) NOT NULL COMMENT 借款人姓名, product_type VARCHAR(32) COMMENT 贷款产品类型, principal_total DECIMAL(12,2) NOT NULL COMMENT 剩余本金, overdue_principal DECIMAL(12,2) NOT NULL COMMENT 逾期本金, overdue_days INT NOT NULL COMMENT 逾期天数, bucket VARCHAR(8) NOT NULL COMMENT 账龄段 M1/M2/M3/M3, status VARCHAR(16) NOT NULL DEFAULT UNASSIGNED COMMENT UNASSIGNED/ASSIGNED/IN_WORK/PROMISED/CLOSED/TRANSFERRED, strategy_group VARCHAR(32) COMMENT 策略组编号, collector_id BIGINT COMMENT 当前处理坐席, promise_date DATE COMMENT 最近一次承诺还款日, promise_amount DECIMAL(12,2) COMMENT 最近一次承诺还款金额, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_bucket_status (bucket, status), KEY idx_collector_status (collector_id, status) ) COMMENT 催收案件主表;字段说明里重点说几个bucket不要用overdue_days直接当筛选条件因为账龄段在业务里是按“逾期天数区间”映射的M1 是 1-30 天M2 是 31-60 天M3 是 61 天以上但不同委托方口径偶尔有偏差单独存字段有利于后续按委托方调整status必须加索引因为几乎所有查询都带着状态过滤collector_id允许为空为空代表案件在公共池里等待被领取或分配。外呼任务表的重点是号码状态和通话结果要分开。号码状态描述这条号码记录本身的状态有效、空号、停机、拒接标记通话结果描述一次外呼动作的结果接通、无人接听、忙线、用户拒接、承诺还款、还款争议。这两个概念混在一起后续统计接通率时永远对不上数这是我从项目里踩出来的教训。还款承诺表单独拆出来的原因更实际承诺的兑现核销往往发生在 T 日之后的几天承诺本身和还款动作是一对多的关系一次承诺可能分多笔还款。如果承诺记录挂在案件表上一个案件多次承诺就变成覆盖历史后面的报表根本没法定量分析“承诺达成率”。CREATE TABLE payment_promise ( promise_id BIGINT PRIMARY KEY AUTO_INCREMENT, case_id BIGINT NOT NULL, collector_id BIGINT NOT NULL, promise_date DATE NOT NULL COMMENT 承诺还款日期, promise_amount DECIMAL(12,2) NOT NULL, actual_amount DECIMAL(12,2) DEFAULT 0 COMMENT 截至统计日实际已还金额, status VARCHAR(16) DEFAULT WAITING COMMENT WAITING/PARTIAL/DONE/BREACHED, source VARCHAR(16) COMMENT MANUAL/AUTO_STRATEGY, remark VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_case_promise (case_id, promise_date) ) COMMENT 还款承诺表;2.2 从案件导入到自动分案一条 UPDATE 管住状态流转案件导入阶段最常见的方式是委托方提供 Excel 或 CSV通过定时任务批量入库。批量入库的 SQL 尽量用INSERT ... ON DUPLICATE KEY UPDATE做幂等否则定时任务重跑一次案件就翻倍。案件导入时要处理的字段映射问题通常很琐碎日期格式不统一、金额带千分位逗号、手机号前面带前缀。我一般不会把这些清洗逻辑塞进存储过程而是先用 Python 脚本做一次标准化再入 MySQL。这样出了问题好排查重跑成本也低。分案动作本质是一次带条件的 UPDATE。一个案件从公共池进入某个坐席的任务列表SQL 可以写成-- 伪代码自动分案时把满足条件的案件锁给指定坐席 UPDATE collection_case SET collector_id :collectorId, status ASSIGNED, updated_at NOW() WHERE case_id :caseId AND status UNASSIGNED;这段 SQL 的AND status UNASSIGNED不是可有可无它是并发控制的关键。同一时间有新坐席在抢单、有定时任务在自动派单如果没有状态条件两条会话可能同时更新同一个case_id后提交的覆盖先提交的案件就被两个坐席同时看到。加上状态条件后即使并发执行也只有一个 UPDATE 能匹配到UNASSIGNED的记录另一个匹配 0 行从数据库层面杜绝重复分案。比单条更新更常见的是批量分案。批量分案时要注意分批大小一次 UPDATE 几千行没问题但一次 UPDATE 几万行会锁表直接影响坐席工作台的响应。常见的做法是每次取 200 条案件循环分案。这里还有另一个坑分案后案件状态变成了ASSIGNED但坐席工作台查询任务列表时如果只查status ASSIGNED坐席打开工作台看到的是未处理任务坐席点击“进入案件”时状态要再变成IN_WORK同时记录进入时间因为后续考核坐席响应时效要用到这段间隔。2.3 机构、角色与权限让外包坐席看不见不该看的案件催收系统中权限不是管理问题是合规底线。委托方、坐席组、坐席这三个层级对案件数据的可见范围完全不同。某个坐席组的坐席默认只能看到分到自己组下的案件组长能看到本组所有案件合规官能看到全部案件但不能外呼委托方只看得到自己委托的那个产品线。我把权限数据模型设计成“机构—角色—用户—案件范围”四层。案件范围用两个字段控制org_id数据归属机构和data_scope数据范围类型取值为本人、本组、本机构、全部。查询时在 SQL 里动态拼接可见条件。不要试图在代码里用if判断完所有组合后拼出漂亮的 SQL实际项目里权限条件很复杂稳妥方案是定义一个权限解析器把每个用户的可见范围解析成一段条件片段-- 伪代码坐席的可见条件解析结果 -- 本组范围示例 AND (c.collector_id :userId OR c.collector_id IN ( SELECT user_id FROM sys_group_user WHERE group_id :groupId) )权限模块还要注意一个细节案件从 A 组流转到 B 组后A 组坐席不应再看到案件详情。这里不要只改collector_id还要更新一个history_owner字段记录流转轨迹否则后续纠纷查证时说不清案件曾经由谁处理过。这些历史轨迹数据量不大但审计要依赖它属于必须做但平时没人看的数据。3. 分案与触达策略让系统决定先打谁、怎么打3.1 分案规则配置账龄、金额、地区与减免范围的组合逻辑分案规则在业务上是这样运作的每个案件进入系统池子后定时任务或者实时判单引擎根据预设规则决定它落入哪个坐席组、哪个坐席的工作队列。规则的本质是一组条件一个目标条件的字段可以从案件表里任意取目标是坐席组或坐席。配置化的做法是用 JSON 存规则定义比硬编码if-else可维护性高得多。例如一条规则“M3 且剩余本金大于 5000 的案件自动分配给‘资深催收组’”配置长这样{ rule_id: 1024, name: M3高金额案件分配, priority: 10, conditions: [ {field: bucket, op: IN, value: [M3, M3]}, {field: principal_total, op: , value: 5000} ], action: { type: ASSIGN_TO_GROUP, target: 资深催收组 }, enabled: true }这段 JSON 的priority字段是规则排序的关键。规则引擎执行时按优先级从低到高逐条判断先匹配到哪条就执行哪条执行完不再继续往下。实际项目里经常出现两条规则条件交叉的情况比如还有一条“所有 M3 案件分配给普通催收组”因为这条priority数值更大比如 20所以 M3 高金额案件会先命中优先级更高的规则不会落到普通组。规则的动态化必然带来一个问题规则配置改错了案件就全分乱了。我建议在规则引擎里加一个“生效前试算”功能规则保存时后台把最近 1000 个案件跑一遍模拟分案把模拟结果中“变更了去向”的案件数量展示给配置人确认。这个功能看起来简单但在实际使用中是救命功能不然每次改规则都是摸着石头过河。3.2 外呼优先级排序评分公式与号码校验分案解决的是“案件归谁”优先级排序解决的是“同一个坐席的队列里哪些案件先打”。算法不需要复杂一个加权评分公式就够了。我常用的是这么一套权重-- 伪代码案件外呼优先级评分 SELECT case_id, principal_total * 0.4 CASE bucket WHEN M3 THEN 25 WHEN M3 THEN 20 WHEN M2 THEN 10 ELSE 5 END - call_count_today * 3 - DATEDIFF(NOW(), last_call_time) * 0.5 AS priority_score FROM collection_case WHERE collector_id :userId AND status ASSIGNED ORDER BY priority_score DESC;这段 SQL 的设计逻辑是本金越大的案件分越高账龄越长的案件分越高今天已经打过多次的降权而距离上次外呼越久又加分目的是避免同一个案件一天被呼七八次把号码打废。实际使用时这几个权重系数需要根据回款数据反复调没有标准答案。唯一要提醒的是call_count_today一定要按“坐席-案件-日期”维度在内存缓存里统计不要每次排序都去 count 外呼任务表热点高峰期查询性能会很难看。号码校验是外呼前最后一道关卡我把它拆成三个层级第一层是格式校验手机号和座机号的正则第二层是免打扰校验在禁呼时段或用户明确投诉过的号码直接拦掉第三层是三网空号状态校验。其中第三层依赖运营商能力很多团队前两版会漏掉结果就是坐席大量外呼空号接通率数据惨不忍睹坐席的积极性也跟着崩了。3.3 策略引擎输出触达动作呼叫、短信、智能语音怎么编排分案完成之后触达动作本身也需要一套可配置的策略。最典型的场景是案件分给坐席后系统先自动给借款人发一条短信提醒同时把外呼任务排到坐席队列如果 24 小时内坐席没有完成有效外呼系统自动转一个智能语音外呼任务先探探路如果智能语音被接起且用户表达了还款意向这个案件再回到人工队列优先处理。策略引擎的数据结构可以抽象成“触发事件—策略条件—动作序列”例如event: ASSIGNED strategy: 首触策略 steps: - action: SEND_SMS params: template_id: 10001 delay_minutes: 0 - action: CREATE_CALL_TASK params: task_type: MANUAL priority: HIGH - action: WAIT_CONDITION params: condition: no_effective_call_within_24h - action: CREATE_IVR_TASK params: ivr_script_id: 10027我一般不建议在第一个版本就做事件驱动的规则引擎而是先用定时任务扫表按策略组每个案件生成触达指令。原因很简单定时任务扫表的方式逻辑清晰、出错好排查坐席和运营也容易理解事件驱动模式加 Kafka 加规则引擎出了乱子定位链路很长。等系统稳定跑了一个季度确认有高频实时触达需求了再迁移也不迟。只不过过程中要记住任何策略动作都必须带“幂等键”同一个案件不能因为状态一致就被重复生成两条短信。4. 把流程跑起来坐席工作台、承诺追踪与质检抽查4.1 坐席工作台的“一案一档”从进入案件到记录回款结果坐席端页面承载的动作就三个看案件信息、外呼、记录结果。这三个动作要在一个页面上完成不要让坐席来回跳页面。案件信息区要展示的内容包括欠款金额、逾期天数、历次外呼结果、最近一次承诺的内容以及这个案件已经拨打过哪些号码、各号码的最近一次状态。展示历史记录有几个容易被忽略的点一是要展示“最近一次承诺金额”让坐席知道上一通电话约定了什么二是要展示“最近三次通话结果”避免坐席重复打没有接通过的号码造成骚扰三是外呼号码要按优先级排序而不是按录入顺序。记录结果时结果类型不能只分“接通/未接通”两种至少要包括有效通话、无人接听、忙线、空号、停机、拒接、号码是第三方联系人已转达、用户明确表示不受理、协商还款中。每种结果对应不同的后续动作无人接听通常在 2 小时后自动生成回拨提醒空号和停机要触发号码状态更新后续外呼自动跳过该号码协商还款中进入承诺登记流程。这些动作可以做成“结果类型→后续动作”的映射表由运营维护而不是写死在代码里。-- 伪代码外呼结果记录后更新案件状态并生成后续任务 UPDATE collection_case SET status IN_WORK, last_call_result NO_ANSWER, last_call_time NOW() WHERE case_id :caseId; INSERT INTO call_reminder (case_id, collector_id, remind_at, reason) VALUES (:caseId, :userId, DATE_ADD(NOW(), INTERVAL 2 HOUR), RE_CALL_NO_ANSWER);4.2 承诺还款PTP追踪到期提醒与二次分案案件一旦登记了承诺还款日期就从“常规催收队列”进入“承诺追踪队列”。承诺追踪的核心是每天早上的到期扫描。扫描逻辑按承诺日期等于今天的案件生成提醒同时检查一个关键条件是否在承诺登记后又有新的还款行为。如果案件在承诺日期当天已经还清那就直接关闭案件不再生成提醒。到期未兑现的承诺不能简单地把案件退回原队列那样案件会沉底。我采用的做法是案件退回公共池并加一个“PTP_BREACHED”标记分案规则里配置一条“PTP 违约案件优先重新分配”让案件在公共池里待的时间不超过一个工作日。这里要注意的是重新分案时尽量不要分回原坐席除非原坐席主动认领。原因很现实原坐席对这个案件已经有固定判断重新跟进容易带情绪换一个人往往反而聊得开。-- 伪代码每日 09:00 扫描到期未兑现承诺标记违约并退回公共池 UPDATE collection_case c JOIN payment_promise p ON p.case_id c.case_id SET c.status UNASSIGNED, c.promise_breached 1, c.collector_id NULL WHERE p.promise_date CURDATE() AND p.status WAITING AND c.status PROMISED;这段 SQL 有个细节如果只是更新案件表而不同时把承诺表的状态改成BREACHED第二天扫描会再次命中同一个案件重复退池。所以这里要保证payment_promise.status同步更新。跨表更新如果用 ORM 来做容易遗漏事务建议直接用 SQL 事务包住。4.3 录音与质检不可全量听但要按规则抽质检模块是催收系统里最容易被压缩预算的部分但一旦出了纠纷或投诉质检记录是最有力的自证材料。录音文件一般存在对象存储里数据库只存录音地址和通话元数据。质检抽取不需要全量那样人力成本不可接受也不需要纯随机那样没有针对性。我建议的抽样规则是每个坐席每天至少抽 3 条通话所有超过 5 分钟的通话必须抽 20%所有产生投诉标记的通话 100% 抽检所有登记了承诺但最终未兑现的近 7 天案件通话抽 30%。质检评分表单独建一张按“话术规范性、态度、禁语、承诺记录一致性、是否提及减免政策”几个维度打分。其中“承诺记录一致性”很有意思它是把坐席在通话里说的承诺金额和系统里登记的承诺金额做比对用于发现坐席口头承诺了系统没登记或者反过来系统登记了但坐席在通话里根本没提。质检发现问题后处理流程是分数低于 60 的录音进入复审队列由质检组长复核确认违规的记录自动生成整改工单并同步给所在组的组长。-- 伪代码拉取待抽检的通话记录 SELECT cr.call_id, cr.case_id, cr.collector_id, cr.duration_seconds, cr.record_url FROM call_record cr LEFT JOIN quality_inspection qi ON qi.call_id cr.call_id WHERE qi.call_id IS NULL AND cr.duration_seconds 300 ORDER BY RAND() LIMIT 20;ORDER BY RAND()在千万级通话记录表上会全表随机性能很差实际项目里建议先缩小候选集比如先取当天有效通话的 ID 列表再随机抽样。这个 SQL 只是示意逻辑线上跑正式环境时我会用TABLESAMPLE或者先取 ID 范围再随机。5. 上线必踩的 5 个坑分案不均、空号、承诺超本金与并发冲突5.1 分案不均有人忙到爆有人队列空空现象自动分案上线后有些坐席队列里堆了五十几个案件有些坐席只分到两三个案件坐席组长天天来投诉。原因分案策略只考虑了案件条件完全没考虑坐席当前的负载水平。规则一跑所有符合条件的高金额案件全进了同一个组而组内分配是按坐席编号轮询的没有结合每个坐席当前未结案件数。解决在分配目标里增加“当前负载”维度。我最终的做法是引入一个分配权重坐席当前未结案件数越多被命中的概率越低。具体实现是在选坐席时加一个条件优先选择当前未结案件数低于组内平均值且 7 天内有有效外呼记录的坐席。这个逻辑不一定用复杂的算法一条子查询就能解决但必须做。5.2 号码空号海量接通率只有 15%现象上线第一周接通率不到两成坐席大量时间浪费在拨打空号上士气掉得很快。原因案件导入时只校验了号码格式没有校验号码状态。委托方给的号码本身就有相当比例是历史遗留的弃用号码而系统没有跟三网空号检测服务对接。解决在案件导入后增加一个异步的空号检测环节案件进入系统后 2 小时内完成号码状态检测检测结果回写联系人号码表的number_status字段外呼任务生成时排除状态为EMPTY/STOPPED的号码。加了这层过滤之后接通率一下子从 15% 拉到了 35%。需要注意的是空号检测服务返回结果有延迟且接口不完全准确系统里要保留“强制外呼”的开关给坐席在特殊场景下绕过的余地。5.3 承诺金额大于剩余本金系统居然通过了现象坐席在案件上登记承诺金额 5 万但剩余本金只剩 1 万后台报表的直接达成本超过了 100%数据一眼假。原因登记承诺金额时没有任何校验坐席为了完成承诺登记指标随手填了夸张的金额或者坐席和借款人约定了分期还款把分期的总额填了进去。解决在承诺表新增校验规则单次承诺金额不得超过剩余本金的 1.2 倍允许利息和手续费浮动一个案件累计承诺金额不得超过剩余本金的 1.5 倍。超限时系统直接拦截并提示坐席修改。这个规则再往细了做就是区分“本金承诺”和“分期总额承诺”两种承诺类型分别走不同的校验逻辑。5.4 定时任务跨天跑批PTP 违约判断错了一天现象早上 8 点的 PTP 扫描结果里前一天刚还了款的案件仍然被标记成违约坐席给已经全额还款的客户打了电话客户直接投诉。原因还款数据从委托方同步过来有 T1 延迟而我们系统的扫描时间定在早上 8 点此时昨天的还款数据还没同步进来系统以为用户没还。解决把 PTP 是否兑现的判断从“扫描时间点的还款累计状态”改成“延迟到当日 12:00 再判断”。另外加一个容错规则案件被标记为违约后12 小时内如果外部还款数据到达自动撤销违约标记并将案件状态恢复为已结清。这个“标记后允许撤销”的机制是跑批系统的常规设计但第一次做这类项目的人很容易漏。5.5 并发外呼同一个案件被两个坐席同时拨打现象坐席 A 和坐席 B 几乎同时点开同一个案件分别给借款人和联系人打了电话。第二天借款人投诉说收到两通不同陌生号码的催收电话。原因工作台前端在打开案件详情时案件状态就被改成了IN_WORK但状态更新没有做行级锁控制。两个坐席打开时都以为案件是自己的前端没有提示“该案件已被 X 坐席处理”。解决把案件详情的打开和状态锁定合并成一个原子操作UPDATE collection_case SET status IN_WORK, handler_id :userId WHERE case_id :caseId AND handler_id IS NULL受影响行数为 0 时前端直接提示案件已被他人处理并跳回队列。同时在外呼任务表建一个case_id call_time的唯一索引兜底防止重复外呼。6. 把报表当系统另一半接通率、PTP 达成率与账龄迁徙的统计口径报表口径这个话题放在最后是因为它是系统上线后最容易产生争议的地方。不是报表查不出来而是同一个指标市场部、运营部、委托方各有各的理解。口径不统一每周的例会都会变成扯皮会这套系统也会被认为“数据不靠谱”。我这里给出一个实践验证过的口径定义可以直接抄。接通率的分母是“在有效呼叫时段内已生成的外呼任务数”分子是“通话时长超过 30 秒且接通的外呼数”。空号、停机、拒接、无人接听都算分母但明确已标记为空号的号码在任务生成阶段就被过滤了所以不会占分母。如果分母里混入了系统未呼出的任务接通率就会虚低这个口径要写在系统说明里让所有角色确认。PTP 达成率要区分两个口径金额达成率和笔数达成率。金额达成率 统计周期内实际到账金额中关联到 PTP 的部分 / 统计周期内应到账的 PTP 总金额笔数达成率 已兑现承诺的笔数 / 到期承诺总笔数。实际运营时经常出现笔数高、金额低的情况因为借款人习惯小额多笔还款。报表里两个口径同时展示只展示一个就容易被挑毛病。账龄迁徙率是给委托方看的核心指标公式是期末处于 M2 的案件数 / 期初处于 M1 的案件数。这个指标要做对关键不在公式而在案件口径一个案件如果期中发生了部分还款但未结清它仍然留在原账龄段不做“降桶”处理只有完全结清才移出。这一点极易产生争议需要在需求阶段就和委托方确认清楚。最后一个建议是技术习惯日报定时任务在每天凌晨生成后先写入一张报表汇总表而不是每天早上现场跑大查询否则早高峰运营看报表时 MySQL 压力会很不稳定。我第一版就是现场跑查询结果每天早上 9 点都被组内同事追着问“报表怎么打不开”换成了凌晨预聚合之后彻底清净了。做报表和做核心交易一样都要考虑高峰时段的资源占用。希望这篇笔记能帮你在自研或接手这条路少绕几个弯把有限精力花在真正影响回款和合规的事情上希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑