背景:当前例子(线索)推送流程属于半自动化,每次策略调整都需要算法同学介入配合。核心诉求是实现策略产品化,让业务侧能够自主配置推送规则,从而让电销团队实现自助运营,无需技术同学重复开发。
主要流程可以概括为:从公海池中圈选目标线索 -> 锁定线索 -> 分发线索。
本期将聚焦于“锁定线索”环节,即如何根据业务规则进行圈选。圈选规则所依赖的筛选条件如下表所示:
筛选条件:
| 字段 |
数值类型(value) |
操作符号(operator) |
| 静默时间:silent_days |
数值 |
等于、小于、小于等于、大于、大于等于、包含于 |
| 触达次数: touched_times |
数值 |
等于、小于、小于等于、大于、大于等于、包含于 |
| 接通次数:jt_times |
数值 |
等于、小于、小于等于、大于、大于等于、包含于 |
| 线索归属地区:city |
字符串(, 分隔) |
等于、不等于 |
| 线索来源: souce_category |
字符串(, 分隔) |
等于、不等于 |
| 关键词:keyword |
字符串 |
等于 |
| 是否有公司名称: hasCompanyName |
true 或 false |
等于 |
二、整体设计与方案选型
方案选择:直接使用 ADB(或兼容的 MySQL)数据库,为每个标签字段建立索引。
选择此方案的考量如下:
- 实现难度:简单直接。
- 查询性能:面对千万级别的线索量,索引查询可以满足性能要求。
- 数据库限制:表的列数限制通常在1000以内,而我们的筛选条件固定且有限,远未触及此上限。
实现流程:
- 数据同步:通过ODPS任务T+1更新公海池数据,将被锁定的线索标记为不可认领,并更新线索的各类信息。
- SQL圈选:根据前端配置的规则,动态拼接SQL语句,执行查询并将结果存入目标表,供后续分配使用。
核心在于,每个筛选条件都对应数据库表中的一个字段,圈选本质上是根据规则动态生成WHERE条件的过程。
ODPS同步的公海表结构示例如下:
create table dws_lead_stategy(
id bigint auto_increment comment '数据表自增主键' primary key,
creditcode varchar charset utf8 null,
leadid bigint null comment 'leads表ID',
transription_text varchar charset utf8 null comment '文本数据',
souce_category varchar charset utf8 null comment '线索来源分类,A/B/C',
last_call_time varchar charset utf8 null comment '末次接通时间',
last_touch_time varchar charset utf8 null comment '末次触达时间',
jt_times varchar charset utf8 null comment '接通次数',
touched_times varchar charset utf8 null comment '触达次数',
silent_days varchar charset utf8 null comment '当前沉默天数',
is_locked varchar charset utf8 null comment '当前线索是否被锁定',
dt varchar charset utf8 null comment '日期',
created datetime default '2025-08-29 11:37:15.535' not null comment '创建时间',
modified datetime default '2025-08-29 11:37:15.535' not null on update CURRENT_TIMESTAMP comment '修改时间'
) engine = InnoDB collate = utf8_bin;
规则引擎的核心是根据操作符和值动态构建SQL条件,以下为关键的Java代码片段:
/**
* 根据操作符和值类型构建SQL条件
*
* @param sqlBuilder SQL构建器
* @param fieldName 字段名
* @param operator 操作符
* @param value 值
* @param ruleType 规则类型
*/
private void appendConditionByOperator(StringBuilder sqlBuilder, String fieldName, String operator,
String value, String ruleType) {
if (StringUtils.isBlank(value)) {
return;
}
// 特殊处理是否有公司名称
if ("hasCompanyName".equals(ruleType)) {
if ("equals".equals(operator) && "1".equalsIgnoreCase(value)) {
sqlBuilder.append(" AND ").append(fieldName).append(" IS NOT NULL AND ").append(fieldName).append(" != ''");
} else if ("equals".equals(operator) && "0".equalsIgnoreCase(value)) {
sqlBuilder.append(" AND (").append(fieldName).append(" IS NULL OR ").append(fieldName).append(" = '')");
}
return;
}
// 处理数值型条件(如静默天数、触达次数、接通次数)
if ("silent_days".equals(ruleType) || "last_touched_times".equals(ruleType) || "jt_times".equals(ruleType)) {
handleNumericCondition(sqlBuilder, fieldName, operator, value);
return;
}
// 处理字符串列表条件(如归属地区、来源)
if ("city".equals(ruleType) || "source".equals(ruleType)) {
handleStringListCondition(sqlBuilder, fieldName, operator, value);
return;
}
// 处理关键词全文匹配
if ("keyword".equals(ruleType)) {
sqlBuilder.append(" AND (LOWER(transription_text) RLIKE '").append(value)
.append("' OR LOWER(follow_text) RLIKE '").append(value).append("')");
return;
}
}
开发过程中遇到的典型问题:SqlRunner 连接泄漏
问题现象:系统运行一段时间后,出现数据库连接耗尽的相关报错。
根因分析:经排查,项目使用的旧版本 MyBatis(3.1.0)中,SqlRunner 工具类存在缺陷。其在执行过程中会创建和使用不同的 sqlSession,导致连接未能正确释放。
解决方案:
- 升级框架:官方在 MyBatis 3.4.3.1 版本中已修复此问题,升级是最彻底的解决方案。
- 兼容性修复:若无法立即升级,可以重写一个安全的
SqlRunner。我们创建了 SafeSqlRunner,确保使用同一个 SqlSession 并在使用后手动关闭。
/**
* 兼容旧版本 MyBatis-Plus 的安全 SqlRunner 工具类
* 解决旧版本 SqlRunner 调用两次 sqlSession() 导致连接泄漏的问题
*/
@Component
public class SafeSqlRunner extends SqlRunner {
@Autowired
@Qualifier("sessionFactory") // 多数据源环境下需指定
private SqlSessionFactory sqlSessionFactory;
private Map<String, Object> sqlMap(String sql, Object... args) {
Map<String, Object> param = new HashMap<>();
param.put("sql", sql);
param.put("args", args);
return param;
}
public List<Map<String, Object>> selectList(String sql, Object... args) {
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
return sqlSession.selectList(SELECT_LIST, sqlMap(sql, args));
}
}
}
三、方案总结与延伸思考
本文通过电销线索圈选的具体案例,探讨了“标签圈选”的通用实现思路。在实际业务中,选择哪种技术方案需要综合权衡:
- 实现难度:评估团队现有技术资源与能力。
- 性能与成本:考虑查询速度(尤其是“与/或”逻辑)、存储空间占用(表体积与索引数量)、以及后续迭代时变更索引的效率。
以下是三种常见方案的对比,可供方案选型时参考:
| 限制/方案 |
方案一(MySQL)每个标签字段一个索引 |
方案二(ADB/PG)倒排索引 |
方案三(ADB/PG)位图 |
| 实现难易 |
简单 |
中等 |
中等 |
| 数据库限制 |
列数限制(一般1000,ADB上限4096) |
列数限制(一般1000,ADB上限4096) |
bitmap最大长度为1GB |
| 查询性能(与/或) |
慢 |
较慢 |
相对快 |
对于筛选条件固定且数量有限、数据量在千万级以内的场景,采用 MySQL 或 ADB 并为字段建索引的方案(方案一)在实现复杂度和性能之间取得了较好的平衡,是快速落地的不错选择。