☰
催收业务系统核心拆解:分案、策略路由与合规留痕
2026/10/19 2:51:34 网站建设 项目流程

简介:这份说明书围绕一诺银华催收业务系统展开,是面向系统开发、测试与运维人员的详细设计文档,重点解决需求理解与设计落地的衔接问题。压缩包共749个文件,约25.27MB,包含266个png截图、126个js脚本、76个class编译类、71个java源文件及一批jsp页面、css样式、xml配置文件等,覆盖界面原型、前端逻辑、后端代码与部署配置,便于对照查阅。文档清晰定义了系统用户概况、功能需求、技术需求、实现环境及实现技术,并对软件在功能和性能上的有效性需求作了明确说明,适合作为催收业务系统开发或二次开发时的参考基准。内容预览中可见大量Action、Dao、Entity等关键类文件,有助于快速理解业务分层与核心流程。目前已有3417人学习下载,适用于需要系统掌握催收业务系统设计思路或进行项目文档编写的中高级研发人员。

1. 催收业务系统到底在管什么:不是打电话,是管案件流转和留痕

先说一个反直觉的结论:一套催收业务系统里,最复杂、最容易被改崩的部分通常不是外呼软电话,而是案件分案、策略路由和合规留痕这三条链路。很多团队第一次搭这类系统,重心全放在“能拨号、能录音”,上线两周后才发现案件分给谁、按什么规则分、承诺怎么追踪、质检怎么抽,才是真正决定催回率和合规风险的地方。这篇文章要拆的就是这套催收业务系统:从案件入队分案,到工作台外呼,再到录音质检和承诺管理,最后落到报表核对。适合正在搭建或接手催收系统的一线开发、实施和业务运营同学,看完能照着落地,也能避开几个常见的坑。

2. 案件分案与策略路由:让每个案件在正确时间找到正确的人

2.1 案件队列设计:账龄、余额、地区三个维度怎么落表

催收系统里几乎没有“所有案件在一个池子里”的做法,一般都会按队列(Queue)把案件拆开。常见队列包括首催队列(SPD)、次催队列(CPD)、法务队列、委外队列等。队列之间的边界条件,通常由余额、账龄、地区三个维度组合而成。这是整个分案体系的地基,表结构设计不扎实,后面策略路由再复杂也跑不对。

我一般会把队列配置独立成一张表,而不是把队列条件硬编码在分案代码里。下面是一个简化的建表 SQL,字段含义直接写在注释里:

CREATE TABLE case_queue ( queue_id INT PRIMARY KEY, queue_name VARCHAR(32) NOT NULL, -- 队列名称:SPD/CPD/LEGAL/OUTSOURCE min_balance DECIMAL(10,2), -- 最低余额门槛,NULL 表示不限 max_balance DECIMAL(10,2), -- 最高余额门槛,NULL 表示不限 min_days_on_book INT, -- 最小账龄(天),NULL 表示不限 max_days_on_book INT, -- 最大账龄(天),NULL 表示不限 region_code VARCHAR(16), -- 地区码,NULL 表示全国 priority INT NOT NULL, -- 队列优先级,数字越小越先被匹配 is_active TINYINT DEFAULT 1 -- 1=启用 0=停用 );

逻辑说明:这张表只负责“队列边界定义”,不负责决定某个案件具体进哪个队列。分案服务启动时会把所有 is_active=1 的队列加载到内存,按 priority 排序,然后逐个判断案件是否落在队列的余额和账龄区间内。region_code 是精确匹配,NULL 表示该队列面向全国,通常在委外场景里用得比较多。

案件入队动作本身是批量任务,一般在 T+1 凌晨跑。核心逻辑是把案件主表和队列配置表做一次范围 JOIN:

INSERT INTO case_queue_member (case_id, queue_id, assign_status, created_at) SELECT c.case_id, q.queue_id, 0, NOW() FROM case_main c JOIN case_queue q ON c.balance BETWEEN q.min_balance AND q.max_balance WHERE c.days_on_book BETWEEN q.min_days_on_book AND q.max_days_on_book AND q.is_active = 1 AND c.case_id NOT IN ( SELECT case_id FROM case_queue_member WHERE assign_status IN (0, 1) );

参数说明:assign_status 用 0 表示待分配、1 表示已分配、2 表示已回池。最后的 NOT IN 子查询是为了防止同一案件在已有未结任务时被重复入队,这是最容易漏掉的一步,漏了会出现一个案件同时在两个队列里被两个催员操作。

2.2 策略路由:规则表 + 命中跳出,比复杂算法更可靠

队列只解决“案件属于哪个池子”,真正决定案件流向的是一套路由规则。这套规则通常不写成代码 if-else,而是做成配置化的规则表,业务人员调整权重和阈值时不用发版。常见做法是规则按优先级从上往下执行,命中即跳出,不再继续匹配后续规则。

下面是一份规则配置的简化 JSON,表达的逻辑是:高余额长账龄转法务;近期有承诺的案件留在催收组;其余走默认队列。

{ "routeRules": [ { "ruleId": "R1001", "name": "高余额优先转法务", "condition": { "balance": { "gt": 50000 }, "daysOnBook": { "gte": 90 } }, "targetQueue": "LEGAL", "priority": 10 }, { "ruleId": "R1002", "name": "近期有承诺留催收组", "condition": { "hasPromise": true, "promiseDate": { "gte": "TODAY" } }, "targetQueue": "CALL_CENTER", "priority": 20 }, { "ruleId": "R9999", "name": "默认兜底", "condition": {}, "targetQueue": "CALL_CENTER", "priority": 999 } ] }

逻辑说明:路由引擎按 priority 从小到大取规则,逐条执行 condition 判断,第一个命中的规则直接返回 targetQueue。必须保留一条无条件命中的兜底规则,否则会出现案件在路由阶段被丢弃、停留在待分案池里变成黑匣子。condition 里的 gte、gt 是比较操作,TODAY 是运行时替换成当天日期的占位符。

几个参数细节值得注意。balance 判断建议使用案件当前未还本金加利息的总余额,而不是合同初始金额。daysOnBook 建议用放款日期到当前日期的差值,不要用账单日。hasPromise 的判定来自第 4 章要讲的承诺登记表,只要存在状态为 ACTIVE 且承诺日期大于等于今天的记录,就算 true。这个字段是实时查还是同步冗余到案件表,取决于你们的调用量,量小直接查,量大就冗余字段加定时刷新。

2.3 案件回收与手工转案:定时器 + 限量限次

分案之后不是万事大吉。催员领了案件不干活,或案件分配给已离职员工,都需要一套回收机制把案件重新送回池子。我的习惯是用“最后联系时间”作为回收判断的锚点,而不是用分配时间。

UPDATE case_queue_member m JOIN case_main c ON m.case_id = c.case_id SET m.assign_status = 2, m.refund_count = m.refund_count + 1 WHERE m.assign_status = 1 AND m.assigned_at < NOW() - INTERVAL 7 DAY AND (c.last_contact_at IS NULL OR c.last_contact_at < NOW() - INTERVAL 7 DAY) AND m.refund_count < 3;

逻辑说明:这个定时任务每周跑一次,把分配超过 7 天且没有任何联系记录的案件回收到案件池。refund_count 是回收次数上限,防止一个案件在池子和催员之间无限循环,最后谁都说不清责任。参数说明:7 天是默认值,业务上可以根据账龄分档,比如新案给 3 天,长账龄老案给 10 天,不建议一刀切。

手工转案稍微麻烦一点,必须有审批记录。转案的原因可能是客户投诉原催员、催员请假、案件转内部核销流程等。每个案件每天手工转案次数建议做成可配置参数,超过次数直接拒绝转案并提示走法务通道。转案时除了改 handler_id,还要在案件操作流水表插入一条带操作员和原因的记录,这一步对后续客诉处理非常重要,审计时拿得出手。

3. 催收工作台与外呼链路:把一通电话拆成 7 种结果和 5 个状态

3.1 任务领取与并发锁:乐观锁防止重复拨打

催收工作台的核心是任务列表。一个案件分配给某催员后,生成一条任务记录。但如果支持多端登录,催员在 Web 端和移动端同时打开列表,点击同一个案件时如果不加并发控制,两个人会抢到同一条任务,出现同一案件同一时间被拨打两次的翻车现场。

数据库乐观锁是最稳妥的解法。更新时同时校验任务当前状态和持有者,更新的影响行数为 1 表示抢占成功,为 0 表示已被别人领取,需要重新刷新列表。

UPDATE case_task SET handler_id = :userId, status = 1, claimed_at = NOW() WHERE task_id = :taskId AND handler_id IS NULL AND status = 0;

逻辑说明:status 0 表示待领取、1 表示已领取待处理。这条 UPDATE 的 WHERE 条件保证了只有 handler_id 为空且状态仍为 0 的任务才会被更新。参数说明::taskId 和 :userId 是预处理参数,实践中强烈建议用预编译语句,一是防注入,二是让数据库执行计划复用。

任务领取后并不是永远锁定。如果催员领取后 30 分钟没有产生任何外呼动作,任务应该被后台定时任务回收并允许其他催员领取。回收逻辑跟 2.3 节类似,只是时间窗口改成了 30 分钟。这个超时参数建议做成系统级配置项,业务可以根据早晚班节奏调整,早班交接的时候 30 分钟容易误回收。

3.2 外呼结果回写:结果码、状态机、下次联系时间三件事一起更新

外呼不是拨完电话就结束。一通电话从发起外呼到完成后续处理,任务状态需要经历一组状态流转。我习惯把它设计成五状态状态机:READY(待外呼)、DIALING(呼叫中)、CONNECTED(已接通)、AFTER_CALL(通话结束待处理)、DONE(已完成)。后处理动作(登记承诺、填写备注)必须在 AFTER_CALL 状态完成,催员填完才能置为 DONE。

外呼结果本身建议用结果码收口,不要存自由文本。结果码至少覆盖七类:接通、无人接听、拒接、空号、停机、占线、无效号码。

CALL_RESULT = { 1: {'label': 'CONNECTED', 'retryable': False, 'next_action': 'after_call'}, 2: {'label': 'NO_ANSWER', 'retryable': True, 'next_action': 'schedule_retry'}, 3: {'label': 'REJECTED', 'retryable': False, 'next_action': 'schedule_retry'}, 4: {'label': 'WRONG_NUMBER', 'retryable': False, 'next_action': 'invalid'}, 5: {'label': 'STOPPED', 'retryable': False, 'next_action': 'invalid'}, 6: {'label': 'BUSY', 'retryable': True, 'next_action': 'schedule_retry'}, 7: {'label': 'INVALID', 'retryable': False, 'next_action': 'invalid'}, }

逻辑说明:retryable 字段决定外呼策略服务是否允许对该号码再次发起自动外呼。比如空号和停机标记为不可重试,无人接听和占线可以进入回拨队列。next_action 告诉系统下一步动作是进入后处理还是安排回拨,还是直接标记为无效号码。参数说明:这里的重试间隔建议按结果码差异化,无人接听一般隔 2 小时再拨,占线隔 30 分钟,不要用一个固定值覆盖所有场景。

回写时还有一个平常容易忽略的字段叫 next_contact_time,也就是下次联系时间。催员在通话结束时选择的“明天上午再联系”必须同步写回任务表。这个字段的价值在于,它可以驱动工作台列表按优先级排序,系统把“该今天联系但还没联系”的案件排在“明天才需要联系”的案件前面,避免催员凭感觉随便翻案子。

3.3 短信与催收函:模板变量、频次限制、送达回执

短信触达是外呼之外的主要补充方式。催收短信属于提醒类消息,模板必须先经过审核。模板中允许插入少量变量,但必须整体固定,不能由催员自由编撰无模板文本,这是合规硬约束。

【XX】{customer_name}您好,您在我司的账单已于{due_date}到期,当前应还金额{amount}元。为免影响您后续使用,请尽快处理。如有疑问请致电{hotline}。回T退订。

逻辑说明:发送服务调用时只需要传入变量名和值,模板引擎渲染后提交到短信通道。参数说明:due_date 是账单到期日,amount 保留两位小数,hotline 在系统级配置里统一维护。变量值在渲染前要统一做 HTML 转义和敏感词过滤,防止客户名里带特殊符号把模板撑坏或触发通道拦截。

短信频次限制是另一个高发坑点。同一客户每天触达上限建议做成系统配置,默认 3 条,超过后直接拒绝发送并记录 trigger 日志。服务类和营销类短信要走不同通道,服务类短信不受 8 点到 20 点的时间限制,但要避开晚上 9 点后的打扰时段,营销类短信则严格限制在 9 点到 18 点。送达回执状态保留四类:提交成功、到达、失败、未知。未知状态需要第二天做一次对账回捞,确认是否真实送达,避免催员误以为客户收到短信而产生策略误判。

4. 合规留痕与承诺管理:录音、承诺、审批三条主链路怎么设计

4.1 录音质检抽检策略:比例 + 定向优先 + 关键词扫描

催收业务对录音的依赖程度极高,客诉发生时第一件事就是拉录音。所有外呼通话必须全量录音存储,这一点没有商量余地。但全量录不等于全量质检,质检抽检要解决的是“在成本可控的前提下,把高风险通话优先抽出来”。

抽检策略我建议做成两层:第一层是定向优先,第二层是随机兜底。定向条件一般是客户投诉过、承诺金额高、催员近期被举报过等。下面是一份抽检规则配置:

{ "samplingConfig": { "totalRatio": 0.05, "dailyLimit": 200, "prioritize": [ { "field": "hasComplaint", "value": true, "weight": 100 }, { "field": "promiseAmount", "gte": 10000, "weight": 80 }, { "field": "handlerRecentRiskFlag", "value": true, "weight": 60 } ] } }

逻辑说明:每天先跑优先组,把 hasComplaint、promiseAmount 达到阈值的通话全部拉入质检池,占掉当天的部分额度,剩余额度再用随机数补足到 totalRatio。参数说明:totalRatio 是整体抽样比例,dailyLimit 是每天质检上限,两个参数同时生效。如果优先组案件数超过 dailyLimit,就要按 weight 从高到低截断,而不是把随机组挤掉。weight 设计的思路是:投诉风险的优先级大于金额大小,金额大小又大于操作者风险标记。

关键词扫描是在语音转文本之后做的。威胁类关键词、辱骂类关键词、诱导客户以贷养贷类关键词各维护一个词表。命中关键词后自动给质检任务打标并提高评级优先级。词表要定期更新,业务人员和合规人员都有提交入口,评估后生效。这个模块上线后要回看一段时间内“命中关键词但质检评分正常”的案例,必要的时候调低权重阈值,宁可多抽不可漏抽。

4.2 还款承诺登记:从“客户说周一还”到“系统周一提醒”

催收业务里还款承诺是最有价值的线索,也是最容易丢的信息。客户说“我周一发工资还”和“我会想办法凑”是两种完全不同的承诺强度,如果不结构化登记,靠催员手写备注基本等于没记。

承诺登记表的核心字段包括承诺日期、承诺金额、承诺渠道、状态。状态机要覆盖:ACTIVE(生效中)、FULFILLED(已兑现)、BREACHED(违约)、CANCELLED(取消)。

CREATE TABLE promise_record ( promise_id INT PRIMARY KEY AUTO_INCREMENT, case_id INT NOT NULL, handler_id INT NOT NULL, promise_date DATE NOT NULL, promise_amount DECIMAL(10,2) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE', created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL );

逻辑说明:承诺登记的触发点是外呼结束时的后处理界面,催员在通话结束页选择“客户承诺还款”,然后填写日期和金额。参数说明:promise_amount 不一定要等于账单全额,允许填写部分金额,但要小于等于案件当前总余额,系统校验超出则拒绝保存。

承诺日期的提醒由定时任务驱动。每天凌晨扫一次,把 promise_date 在当天和次日的 ACTIVE 承诺记录生成提醒任务,推给对应催员工作台。承诺日期当天结束时仍未还清,定时任务要把状态从 ACTIVE 更新为 BREACHED,同时把案件优先级提升一档或直接转入升级队列。这一步如果漏做,承诺就变成了一张废纸。

4.3 减免与分期审批:分级授权、审批链、状态闭环

减免和分期是催收系统里操作风险和合规风险最集中的模块。设计原则只有一个:权限不能大锅饭,审批必须留痕。常见做法是按照减免金额区间和折扣率做分级授权。

减免金额范围审批权限附加条件
0 - 1000 元队列组长折扣率不低于 0.9
1000 - 5000 元运营主管需提供客户还款能力说明
5000 元以上合规复核需附录音片段和文字摘要

审批状态机建议设计为 DRAFT(草稿)→ SUBMITTED(已提交)→ APPROVED(已批准)→ EXECUTED(已执行),或者 SUBMITTED → REJECTED(已驳回)。驳回后允许修改重提,但同一案件当天重提次数限制为 3 次。

审批链路里最容易被忽略的是“执行闭环”。审批批准了,减免方案要在外呼结果回写或还款入账时真正生效。要检查催员是不是拿到了批准文号后没有在系统里执行减免动作。建议加一个定时校验任务:查所有 APPROVED 状态超过 24 小时仍未 EXECUTED 的减免单,告警到审批人。这条监控线能堵住不少流程漏洞。

5. 上线避坑指南:五个真实翻车配置与修复记录

5.1 分案权重翻车:规则命中顺序被意外覆盖

现象:一批高余额案件没有进法务队列,反而进了催收组普通池,导致催回率数据异常。

原因:新增了一条路由规则,优先级数字设成了 5,比高余额规则 priority 10 还小,而路由是按 priority 从小到大执行的。新规则的匹配条件是“余额大于 5000”,高余额案件全被这条先匹配走了。

解决:把规则优先级改成 15,放在高余额规则之后执行。从那以后我给所有路由规则加了一个强制约束:同一队列的兜底规则优先级必须大于 100,业务规则区间固定在 10 到 99,并且每次上线前做一次规则优先级冲突检查。

5.2 外呼显号与回拨失败

现象:客户看到来电号码无法回拨,部分客户投诉“显示的是本地号码,拨打后是空号”,直接影响接通率和客户信任度。

原因:外呼线路配置了多个显号,但显号与线路绑定关系只配置了主叫侧,没有配置回拨路由。客户回拨时号码进入了未绑定线路,而该线路没有对应的落地中继,于是听到空号提示。

解决:显号池和回拨路由必须成对配置。每新增一个显号,验证时除了测外呼,还要从另外一个手机回拨这个显号,确认能回到坐席或正确的 IVR。这个验证步骤我后来写进了所有外呼项目的上线清单里,列为必测项。

5.3 节假日与晚时段打扰投诉

现象:某项目上线一周内收到多条投诉,客户说“晚上 10 点给我打电话”“节假日早上 8 点就打”。

原因:系统默认按 8:00-20:00 执行外呼,但节假日策略没有单独配置,周五晚上的外呼和周日早上的外呼都没有拦截。

解决:外呼时间策略从全局固定时间改成时间策略表驱动,按工作日、周末、节假日三套时段分别配置。节假日由运营在节前手动维护或直接引用法定节假日数据。同时把呼叫结果里的 STOPPED(客户明确要求不再联系)状态联动到时间策略里,这类案件任何时段都不再触发自动外呼。

5.4 承诺日期过期不提醒

现象:一批承诺记录到了承诺日,系统没有给催员任何提醒,案件也没有升级,等于承诺白登记了。

原因:定时任务扫描条件写的是 promise_date = CURDATE(),但任务在凌晨 0:30 跑,把状态从 ACTIVE 改成 BREACHED 的更新逻辑依赖一个慢查询,执行时部分记录还未更新,部分记录因为跨时区问题日期不一致被漏扫。

解决:扫描区间改成 promise_date BETWEEN YESTERDAY AND TODAY,状态更新用批量 UPDATE 按 promise_id 分段执行。从那以后我要求所有定时任务都不允许使用“相等”判断日期,必须使用区间,这是这类系统最廉价的防呆手段。

5.5 质检录音拉取超时

现象:质检员在质检页面打开录音时频繁超时,部分超过 10 分钟的录音文件根本加载不出来。

原因:录音文件存储在对象存储远端,质检页面是同步拉取到后端再转给前端,网关超时限制 30 秒,大文件解析加传输必然超时。

解决:改成异步预取方式。质检任务生成时后台提前把录音文件从对象存储拉取到本地缓存,质检员打开页面时直接读缓存。超时 3 天的缓存文件清理。还有一个配套参数是录音下载分片阈值,超过 20MB 的文件一律走分片下载,避免内存溢出。

6. 报表口径与 T+1 核对:用数据而不是感觉验证系统是否正常

催收系统上线后,最怕的不是单笔功能 bug,而是数据口径漂移。同样一个“催回率”,运营算出来和系统报表显示的不一致,谁都说服不了谁。问题通常出在分子分母定义不统一。下面这张口径表建议贴在项目群置顶,作为统一语言。

指标分子分母注意事项
首次联系率首次外呼接通案件数本期分案案件数接通指 CONNECTED,不含短信触达
催回率本期还款入账金额本期账单到期金额按实际入账时间,不用承诺时间
承诺兑现率已兑现承诺数到期承诺总数到期指 promise_date 小于等于统计日
投诉率本期投诉案件数本期外呼接通案件数投诉按案件维度去重,不按通话维度

报表之外,我更建议做一张 T+1 核对任务,每天凌晨自动校验分案数与任务生成数是否一致。这个核对不复杂,但能一次性揭开三个隐性故障:分案漏了、任务生成幂等被破坏、队列配置被误改。

SELECT a.assign_day, COUNT(DISTINCT a.case_id) AS assigned_case_count, b.task_cnt FROM case_queue_member a LEFT JOIN ( SELECT DATE(created_at) AS task_day, COUNT(*) AS task_cnt FROM case_task GROUP BY DATE(created_at) ) b ON DATE(a.created_at) = b.task_day WHERE DATE(a.created_at) = CURDATE() - INTERVAL 1 DAY GROUP BY a.assign_day, b.task_cnt HAVING assigned_case_count != b.task_cnt;

逻辑说明:左边是分案数量,右边是任务生成数量,两边按天对齐后如果对不上,说明链路中间有断点。参数说明:HAVING 子句的 != 也可能因为 RIGHT JOIN 补零而失效,所以实际项目里我更建议把两边分别算成两张临时表再 FULL JOIN,避免 NULL 比较。做一个带告警的查询,连续两天不一致就要人工介入排查。

这个核对脚本是我自己的习惯,每次上线新策略或新队列都会强制走一遍,连续跑三天没问题才算策略上线完成。系统的真实状态,只有 T+1 数据能说清楚,页面上的数字都可以粉饰。希望这篇拆解帮到你,少走几个我已经踩过的坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询