🫶标签圈选:例子推送

45 阅读4分钟

一、前言

背景:当前例子推送属于半自动,每次策略调整都需要算法同学配合调整,诉求是将电销例子推送策略产品化,由业务侧配置推送规则,实现电销团队自助运营无需技术同学介入。 image.png


主要流程:从公海池子里圈选 -> 锁定例子 -> 分发例子

image.png


本期聚焦于锁定例子,即圈选规则,圈选规则如下image.png

筛选条件

字段数值类型(value)操作符号(operator)
静默时间:silent_days数值等于、小于、小于等于、大于、大于等于、包含于
触达次数: touched_times数值等于、小于、小于等于、大于、大于等于、包含于
接通次数:jt_times数值等于、小于、小于等于、大于、大于等于、包含于
例子归属地区:city字符串(, 分隔)等于、不等于
例子来源: souce_category字符串(, 分隔)等于、不等于
关链词:keyword字符串等于
是否有公司名称: hasCompanyNametrue 或 false等于

二、整体设计

方案选择:直接使用 ADB(MySQL 也可):每个标签字段一个索引。

1、实现难度:简单

2、查询性能:例子数量千万级别

3、数据库限制:列限制 1000以内。圈选例子的复杂度、规则条件常年不变:筛选条件有限


实现流程: image.png

1、ODPS T+1 更新公海池,将被锁定的例子标识不可认领 / 更新例子信息

2、执行 SQL 圈选例子,并存表,用于之后分配

  • 每个筛选条件对应一个字段
  • 筛选规则:拼接 SQL
  • 执行 SQL 写入对应代码

ODPS推送的公海表,SQL:

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

/**
 * 根据操作符和值类型构建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)) {
        handleNumericCondition(sqlBuilder, fieldName, operator, value);
        return;
    }
    
    // 处理鮋达次数
    if ("last_touched_times".equals(ruleType)) {
        handleNumericCondition(sqlBuilder, fieldName, operator, value);
        return;
    }
    
    // 处理接通次数
    if ("jt_times".equals(ruleType)) {
        handleNumericCondition(sqlBuilder, fieldName, operator, value);
        return;
    }
    
    // 处理例子归属地区
    if ("city".equals(ruleType)) {
        handleStringListCondition(sqlBuilder, fieldName, operator, value);
        return;
    }
    
    // 处理例子来源
    if ("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;
    }
}

开发过程中遇到的BUG:SqlRunner 连接不释放

报错如下

image.png

究其原因:创建和使用的不是同一个 sqlSession,导致没释放。

image.png

查看 mybatis 版本:3.1.0

image.png

解决如下

1、升级版本:官方 3.4.3.1 已经更修复

image.png

2、自己重写 SqlRunner 个,为 SafeSqlRunner,并手动释放。

/**
 * 兼容旧版本 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));
        }
    }
}

三、小结

本文通过圈选例子,抛砖引玉:标签圈选。

标签圈选根据不同业务场景,选择适合的方案:思考维度

1、实现难度:现有资源支持

2、数据库配置性能:与/或 查询速度、空间占用(表 / 索引)、以及迭代需要创建索引的速度

总结有三种方案,方案如下

限制/方案方案一(MySQL)每个标签字段一个索引方案二(ADB/PG)倒排索引方案三(ADB/PG)位图
实现难易简单中等中等
数据库限制列数限制:一般1000ADB 上限:4096列数限制:一般1000ADB 上限:4096bitmap最大长度为1GB
查询性能(与 / 或)较慢相对快