ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

客户唤醒自动化实践:定时任务+企微API触达链路全解析

客户唤醒自动化实践:定时任务+企微API触达链路全解析 做客户运营的朋友应该都有同感拉新那一下大家都舍得花钱但拉来之后呢名单躺在 CRM 里超过三十天没有互动从“高意向客户”悄悄变成了“沉默客户”。销售靠手机通讯录一个个手动跟进一天撑死一百多条还要被各种“在忙”“再考虑”挡回来。我去年接手客户唤醒这块时第一件事就是把“定时任务企微消息API”这套自动化唤醒触达链路搭起来每天固定时间自动扫描沉默客户列表生成带有明确理由的唤醒文案通过企业微信应用消息直接触达客户再把客户的回应和点击行为回收回来。这篇文章是系列第二篇重点讲调度方案选型、企微消息接口接入以及链路跑通之后那些文档里不会写的坑。适合自己搭 CRM 客户池、或者负责客户触达系统的技术和运营同学参考。1. 先搞清楚唤醒对象沉默客户不是一个文件夹1.1 沉默阶段的分层标准很多团队把“超过 N 天没互动”一刀切成沉默客户然后统一走同一套话术这是最大的浪费。不同沉睡深度的客户心理状态和唤醒成本完全不一样。我把客户池按“距最近一次有效互动”拆成了四层层级定义唤醒难度建议策略流失预警期7-14天未互动低轻量提醒以服务/关怀为主已沉默期15-30天未互动中权益类或利益点触达深度静默期31-90天未互动高强钩子人工衔接流失期超过90天未互动极高不主动推转召回类活动这套分层是运营团队反复校准的结果。预警期客户往往只是“最近忙忘了”一条温和的消息就能拉回来深度静默期堆再多“亲好久不见”也没用需要给出足够具体的理由和利益。计算沉默天数时一定要注意不要拿“最后登录时间”当唯一口径对 B 端客户来说“有效互动”应该至少包含登录/访问、工单反馈、销售通话、企微会话回复这几类事件里的任意一种。1.2 每条唤醒消息都必须有一个“理由”这是最容易踩的坑定时任务每天准点给沉默客户发“早安优惠券”发了一个月发现触达率越来越高回复率越来越低甚至有人直接拉黑。原因很简单客户不关心你建了多漂亮的唤醒系统他关心的是“你凭什么在这个时间点打扰我”。所以我在设计唤醒任务时给每条策略绑定了触发理由没有理由的触达一律不建客户上次咨询过某产品但 15 天没有后续动作 → 跟进型唤醒客户账户里有一张即将到期的权益券/积分 → 到期提醒型唤醒客户常用的产品有版本更新或新能力 → 内容型唤醒大促/活动与客户历史偏好匹配 → 活动型唤醒每次生成消息时任务系统必须能从数据里拼出这个理由拼不出来就跳过。宁可少发一条也不能发一条“没头没脑”的消息。客户唤醒的本质不是“提醒他你还在”而是“重新给出一个让他愿意回应的由头”。1.3 用一张唤醒计划表管住全部状态唤醒任务不能做成“每天临时查一次、发完就拉倒”那样没法统计、没法排重、也没法追踪效果。我习惯在数据库里维护一张唤醒计划主表字段大致是CREATE TABLE wakeup_plan ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id varchar(64) NOT NULL COMMENT 企微UserID, customer_id varchar(64) DEFAULT NULL COMMENT CRM客户ID, wakeup_stage tinyint(4) NOT NULL COMMENT 沉默阶段1预警 2沉默 3深度静默, strategy_code varchar(32) NOT NULL COMMENT 策略编码如FOLLOW_UP, next_send_date date NOT NULL COMMENT 计划触达日期, task_id bigint(20) DEFAULT NULL COMMENT 本次执行的调度任务ID, msg_id varchar(64) DEFAULT NULL COMMENT 企微消息ID, send_status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待发送 1已发送 2发送失败, reply_status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0未回复 1已回复, open_status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0未打开企业微信 1已打开, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_task_user (task_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;唤醒计划表的核心作用是让“定时扫描”和“触达执行”解耦。每天定时任务只负责筛选客户、插入待触达记录发送任务再拉着计划表里的待发记录去批量调企微 API。这两个环节分开出错时处理起来非常清晰——是筛选逻辑的问题还是发送环节的问题一眼就能定位。2. 定时任务选型单机调度、XXL-Job 还是 Serverless2.1 别一上来就上重武器Spring Boot Scheduled 够用场景如果你的团队还在起步阶段客户量不大也不存在多实例同时跑任务的问题那 Spring Boot 自带的Scheduled是最低成本的起步方案。代码就几行Component public class WakeupScanScheduler { Scheduled(cron 0 30 9 * * ?) public void scanSilentCustomers() { // 1. 查询符合条件的沉默客户列表 // 2. 写入 wakeup_plan 表 // 3. 触发发送执行器 } }cron表达式里0 30 9 * * ?表示每天早上 9 点 30 分执行一次。这个时间不是随便定的我建议把唤醒消息的发送时间放在上午 9 点 30 到 11 点之间避开刚上班的半小时也避开午休。这个细节直接拉高触达后的回复率。但Scheduled有几个明显的坑集群部署时每个实例都会执行一次同样的任务不做分布式锁就会出现重复触达。任务执行到一半进程重启没有任务日志没法追溯“上次跑到哪了”。没有运维界面任务参数调整要改代码重新发版。怎么判断自己是否需要升级看两点应用实例数是否大于 1以及每天要处理的任务量是否达到几千条以上。两个条件都不满足老老实实用Scheduled加 Redis 锁就能撑住早期阶段。2.2 Spring Cloud 架构下的实际选择XXL-Job如果你的系统是 Spring Cloud 微服务架构多个服务实例同时部署这时候再用Scheduled就是给自己埋雷。我这边最终选的是 XXL-Job原因很务实它和 Spring Boot/Spring Cloud 亲和度最好社区活跃文档也全。XXL-Job 的核心用法是调度中心独立部署执行器嵌在业务服务里通过注册中心自动发现。我用到的能力有三个分片广播按客户 ID 哈希取模拆分成多个分片每个执行器处理自己负责的那一片解决“几万条客户一次性扫不完”的问题。调度日志每次任务跑完调度中心能看到每个分片的执行结果、耗时、异常堆栈排查问题效率高很多。失败重试可以配置失败重试次数和间隔对于企微 API 偶发超时有实际意义。核心代码大致是这个结构XxlJob(silentCustomerWakeup) public ReturnTString silentCustomerWakeup(String param) throws Exception { int shardIndex XxlJobHelper.getShardIndex(); int shardTotal XxlJobHelper.getShardTotal(); // 按分片拉取客户 ListLong customerIds customerMapper.selectSilentCustomersByMod(shardTotal, shardIndex); for (Long customerId : customerIds) { // 生成唤醒计划、发送企微消息 wakeupService.process(customerId); } return ReturnT.SUCCESS; }注意这里用customerId % shardTotal shardIndex的方式来路由分片而不是直接用进程内随机分配。原因是保证同一个客户每次任务都由同一台机器处理后续做状态校验和幂等会轻松很多。XXL-Job 的“调度中心”本身也是一个服务得单独部署。我见过不少团队嫌麻烦后来用了 Oracle 的 Scheduler 或者自研调度表最后都绕回来自建或者上现成框架原因一致分布式任务最缺的不是“到点触发”而是“失败可查、重试可控、任务不重复”。2.3 轻量场景的另一种答案Serverless 定时触发器如果你的客户唤醒链路没那么重比如只是每天早上从数据库取一批名单调企微 API 发一条消息没有复杂的审批流转那用 Serverless 定时触发器也很合适。主流云厂商都提供定时触发器配置一个 Cron 表达式到点自动拉起云函数。好处是不用维护调度中心没有常驻服务器成本。天然单实例执行不会出现多机重复跑的问题前提是别把触发间隔压得太极限。弹性伸缩任务量上去也不怕。我看最近很多人在讨论 Serverless 定时任务比如拿它做每日签到类的自动化其实思路完全一致定时触发 HTTP 调用外部接口 记录执行结果。做唤醒触达也只是把“外部接口”换成了企微 API。需要注意 Serverless 的执行时长限制一般默认几分钟如果名单扫描 消息发送超过这个时间就要把任务拆成“扫描一个批次、发完一批再触发下一批”的接力模式。2.4 我的选型建议总结拿我们实际遇到的场景举个例一开始只有一台服务器每天跑一次 Scheduled后来业务扩容到 3 个实例立刻发现在凌晨任务时间点大家同时开跑客户收到两条一模一样的消息。后来切到 XXL-Job 分片广播问题才真正解决。所以选型不是越高级越好而是跟着系统规模走。方案适用场景优点注意事项Spring Boot Scheduled单实例、小数据量零成本、上手快集群下要加锁无任务日志XXL-JobSpring Cloud 多实例、任务量中等偏上分片、调度日志、失败重试需额外部署调度中心Serverless 定时触发轻量独立任务、不想维护调度组件免运维、按量付费执行时长受限无状态化设计Axum/Rust 生态定时任务技术栈是 Rust 的团队性能好、可嵌业务生态相对小众团队要熟悉这套对比不仅适用于唤醒触达其他任何“到点跑批”的业务都能复用判断框架先看实例数再看任务量最后看你在不在意任务日志。3. 企微消息 API把唤醒消息送到客户面前的完整链路3.1 接入前的必要准备自建应用、可见范围、可信 IP要在企微侧发主动消息最常用的是“自建应用消息”通道。它的逻辑是企业微信内部创建一个应用这个应用被授权可以主动给成员也就是你的客户发消息。准备步骤依次是登录企业微信管理后台进入“应用管理 - 应用 - 自建”创建一个应用。创建后你会拿到AgentId和Secret这两个参数是所有消息接口的前提。配置“应用可见范围”。这一步最容易被忽略坑也最大自建应用只能给可见范围内的成员发消息。如果你把可见范围设成了某个部门那这个部门之外的客户 UserID 即使存在消息也发不出去。配置“企业可信 IP”。企微 API 对来源 IP 有校验只允许可信 IP 地址调用。服务端如果是动态公网 IP需要固定入口或者走代理网关。如果要用回调接收消息状态还需要配置回调 URL、Token 和 EncodingAESKey。准备好这三样东西等于给你的“唤醒触达”开了一条合规、稳定的消息通道。3.2 access_token 的获取与全局缓存企微 API 的鉴权方式是先拿 access_token再拿它去调消息接口。有个关键细节access_token 的有效期是 7200 秒而且整个企业同一应用只有一个 access_token它会被并发获取的请求互相覆盖处理不当会出现“刚拿到的 token 突然失效”的诡异问题。正确做法是全局缓存 加锁刷新Component public class WeComTokenManager { private static final String TOKEN_KEY wecom:access_token; private final StringRedisTemplate redisTemplate; private final RestTemplate restTemplate; public String getAccessToken() { String cached redisTemplate.opsForValue().get(TOKEN_KEY); if (StringUtils.hasText(cached)) { return cached; } // 双检锁防止多个线程同时刷新 token synchronized (weComTokenLock) { cached redisTemplate.opsForValue().get(TOKEN_KEY); if (StringUtils.hasText(cached)) { return cached; } String token fetchTokenFromWeCom(); redisTemplate.opsForValue().set(TOKEN_KEY, token, 7000, TimeUnit.SECONDS); return token; } } private String fetchTokenFromWeCom() { String url https://qyapi.weixin.qq.com/cgi-bin/gettoken ?corpid corpId corpsecret secret; // 返回结果中取 access_token return restTemplate.getForObject(url, JsonNode.class).get(access_token).asText(); } }这里有两个小心思缓存时间设置成 7000 秒而不是 7200 秒留出刷新余量用 Redis 而不是本地内存缓存保证多实例环境下拿到的是同一个 token不会你自己刷新一次把别人的 token 顶掉。企微 API 调用返回码里40014是“不合法的 access_token”42001是“access_token 过期”。我建议在发送消息前先检查返回码如果遇到这两个错误码立即清掉 Redis 缓存重新拉取再重试一次能把大部分偶发失败直接救回来。3.3 发送应用消息接口链路与参数细节拿到 token 之后发送应用消息的接口是POST /cgi-bin/message/send请求体长这样{ touser: zhangsan, msgtype: text, agentid: 1000002, text: { content: 您好您上次咨询的报价方案我们已更新点击查看最新权益 } }核心参数说明touser接收成员的 UserID一次调用可以传多个用竖线分隔最多 1000 个。但从控制节奏的角度我不建议一次塞满后面会讲原因。msgtype消息类型。文字场景用text就够想带点排版和链接样式可以换markdown类型图文卡片可以用news。agentid自建应用的 AgentId。text.content消息正文。text 消息的内容长度限制在 2048 字节以内包含超链接文本时链接本身也会占长度写文案时记得留余量。注意这个接口发送的是“应用消息”客户在企业微信里会收到一条来自对应应用的通知消息而不是像个人微信那样直接显示在聊天列表里。这是渠道特性不是 bug。从唤醒场景看应用消息的优势是带应用标识和跳转链接配合图文形式比普通聊天消息更有“服务感”。3.4 通过回调拿到送达结果只是把 send 接口调用成功还不够“成功”只代表企微接受了不代表客户看到了。要判断“客户是否打开了这条消息”需要配置企微回调。在自建应用的回调配置里订阅“接收消息”事件后企微会把一条消息是否被点击、成员会话状态等事件推送到你的回调服务。实际操作中最有价值的两个事件是用户点击了应用消息里的链接或按钮用户直接在企微会话里回复了该应用收到回调后去更新 wakeup_plan 表的open_status和reply_status。这一步是整个唤醒系统真正能闭环的关键只有知道“哪些客户看了没回、哪些回了”后续的二次触达和人工跟进才有依据。我见过不少团队把唤醒做到“发出去”就结束了然后说“唤醒没效果”其实是链路没闭合——你根本不知道哪些消息被打开了自然没法优化文案和时机。4. 一条完整唤醒链路从名单扫描到行为回收4.1 整体任务编排逻辑把定时任务和企微消息链路拼起来之后一天的实际执行流程是这样的早晨 9 点XXL-Job 触发“名单扫描任务”按沉默分层标准查出当天需要触达的客户。将客户写入 wakeup_plan 表状态为待发送同时给每个客户匹配策略和文案模板。9 点 30 分触发“消息发送任务”从计划表取待发记录按批次发送企微应用消息。发送成功后更新 send_status 已发送记录 msg_id。全天内企微回调持续监听客户是否打开、是否回复更新对应字段。当天跑完运营后台能看到整体触达率、打开率、回复率。这个流程最关键的设计是“扫描”和“发送”分开原因很实际扫描阶段如果 SQL 写错了或者数据源有问题只影响计划表的生成不会误发消息发送阶段如果企微接口抽风只需要重跑发送任务不需要重新筛选客户。4.2 候选筛选去重、接触上限、黑名单筛选逻辑不是一句“查最近互动超过 30 天”那么简单。我沉淀下来的三个必要过滤条件策略周期排重同一个客户同一策略周期内只能触达一次。比如“超期未回访”策略15 天之内不能对同一个人重复触发防止客户连续两周各收到一条差不多的“您在吗”。全局频控上限每个客户每周最多接收 2 条唤醒类应用消息超过就走人工跟进渠道。这个上限最初是拍脑袋定的跑了一个月之后发现回复率下降的拐点正好在 2 条就保留了下来。排除名单已回复客户、已退订客户、黑名单客户必须第一时间从候选集剔除。这些名单维护在单独的配置表里扫描 SQL 里 join 排除。筛选 SQL 的骨架大概是SELECT c.id, c.user_id FROM customer c LEFT JOIN wakeup_plan wp ON wp.user_id c.user_id AND wp.task_id ? -- 本次任务ID LEFT JOIN blocklist b ON b.user_id c.user_id WHERE c.last_active_time DATE_SUB(NOW(), INTERVAL 30 DAY) AND c.is_black 0 AND b.id IS NULL AND NOT EXISTS ( SELECT 1 FROM wakeup_plan w WHERE w.user_id c.user_id AND w.strategy_code FOLLOW_UP AND w.created_at DATE_SUB(NOW(), INTERVAL 15 DAY) )这套 SQL 有一个隐含假设task_id和user_id的组合唯一。所以我在建表时特意加了联合唯一索引防止发送任务重跑时插入重复计划这是幂等设计的第一层保险。4.3 触达节奏与文案结构设计唤醒触达最容易犯的错是“想一次把话说尽”。文案里又放问候、又放产品介绍、又放优惠券、又放链接结果客户根本不知道你来找他干嘛。我通常用三段式结构第一句说清楚身份和来由。比如“您好我是XX客户成功团队的小周”或者“您账户里有张优惠券快要到期了”。第二句给出具体理由或利益点。这里一定要落到客户身上而不是你们的活动名称。第三句给一个极低门槛的行动指令。“回复 1 查看详情”“点开链接领取”不要让客户思考下一步做什么。发送节奏上我强烈建议做“周轮询”而不是“天天发”。例如周一发跟进型周三发权益提醒型周五发内容型。当一个客户连续三周被唤醒都没有任何回应就别再自动发消息了转给人工做最后一轮电话/线下触达。自动化的价值在于规模化覆盖但最后的破冰动作往往是需要温度的。4.4 发送状态与客户行为回收发送完成后wakeup_plan 表的状态更新逻辑要明确状态字段取值更新时机send_status1 已发送send 接口返回成功open_status1 已打开企微回调收到打开事件reply_status1 已回复企微回调收到客户回复消息这里有一个细节客户回复企业微信应用消息时企微回调推给我们的数据类型是普通消息事件需要在回调里解析发送者 UserID再回写 wakeup_plan 表。客户回复的内容不一定要做复杂的 NLP 分析可以先把“是否回复”当作关键信号回复内容推给销售人工判断即可。这一步真正的价值是运营团队可以看实时看板比如“今天发了 800 条打开率 42%回复率 11%”第二天做投放策略调整就有了数据依据而不是凭感觉。5. 生产环境踩过的坑频控、token 失效、重复触达5.1 企微接口的频控不是“总条数”是“人均条数”刚开始自测时我用一个测试号连发几十条消息都没事以为接口没有限制。上线第一天客户量一上来消息发送任务跑了几分钟就开始报45009——接口调用超过频率限制。仔细看错误码对应的文档才发现企微应用消息的限频是按“每个成员每分钟最多接收 2 条”这个维度卡控的不是你企业的总发送量。也就是说同一个客户如果正好同时出现在两个任务里一前一后两条消息发过去第二条很可能直接限频失败。解决方案有两层第一任务调度层面给每个客户设置全局频控保证同一客户在同一分钟窗口内最多只会进一个发送批次第二发送任务里对企微返回的频控错误码做特殊处理遇到45009就把这条记录标记为“延后处理”放到下一个批次再发而不是直接标记失败并丢弃。5.2 重跑任务造成重复触达唤醒任务因为是“扫描发送”两步任何一步失败都会有人选择“重跑任务”。如果发送环节没有做幂等重跑一遍全量计划客户收到的就是两遍完全相同的消息运营和销售的投诉电话会立刻打爆。我的处理是两层幂等数据库层task_id user_id联合唯一索引插入计划表时重复记录直接失败。发送层发送前先查 wakeup_plan 表send_status 1的记录直接跳过。5.3 串行发送慢到“任务超时”第一版发送执行器图省事用 for 循环一条条调企微接口。1 万条消息差不多要跑 20 分钟一旦中间网络抖动整个任务就卡死。后来改成“分批 线程池 队列”的方式public void batchSend(ListWakeupPlan plans) { // 每批 100 条用固定大小线程池并发调用 ExecutorService executor Executors.newFixedThreadPool(10); ListListWakeupPlan batches Lists.partition(plans, 100); for (ListWakeupPlan batch : batches) { CountDownLatch latch new CountDownLatch(batch.size()); for (WakeupPlan plan : batch) { executor.submit(() - { try { weComMessageClient.send(plan); } finally { latch.countDown(); } }); } latch.await(); Thread.sleep(500); // 小幅休眠避免触底限频 } }核心是单批次并发、批次间休眠、整体受控。线程数不要贪多10 个对企微接口已经足够再多反而容易触发对端限频。实测同等数据量下这个方案的发送时间从串行的 20 分钟压到 4 分钟左右。5.4 一条必看的自查清单给你一份上线前自查清单每条都是坑换来的自建应用可见范围是否包含全部目标客户服务端出口 IP 是否已配置为企业可信 IPaccess_token 是否走了全局缓存加锁刷新计划表是否有唯一键防止重复插入发送任务是否做了批次并发和频控休眠回调地址是否配置、是否订阅了消息事件是否对常见错误码40014、42001、45009做了重试或延后逻辑6. 唤醒效果怎么算指标口径与后续优化6.1 真正该盯的三个指标唤醒任务上线后运营最关心的往往是“今天发了多少条”。但“发出去”不产生价值我一般只看三个指标打开率已发送消息中被打开的比例反映文案吸引力和触达时机。回复率已打开消息中客户有实际回复的比例反映利益点是否打动人。有效唤醒率回复后 7 天内有实质互动下单/跟进/建立联系的比例这是最终业务价值。打开率低先调发送时间和文案开头回复率低重点调利益点和行动指令有效唤醒率低问题往往出在前端筛选——客户本身已不是有效线索别在这个池子里烧钱。6.2 从自动化群发走向人工衔接自动化任务跑稳之后下一步一定是“机器找到有意向的人人工跟进真正重要的事”。我会把 wakeup_plan 表里回复状态为“已回复”的客户记录实时推给对应的销售/客服由人在企微会话里继续聊。回复但还没成交的客户比任何沉默客户都有价值不能让他们在系统里躺着等下一个定时任务。这里要注意一个边界自动化适合做第一波唤醒不代表它能替代销售。客户回复后的人工衔接如果断档前面所有自动化工作都白费。可以在企微侧配置“客户联系”功能把会话记录同步给销售销售看到的是客户从哪条唤醒消息里点进来的对话开场自然很多。6.3 这套体系还能迁移到哪些场景定时任务加企微消息这套组合本质上是一个“根据业务状态主动触达”的通用骨架。跑通唤醒之后我把它延伸到了另外几个场景新客户欢迎注册后第三天自动发一条使用引导。流失预警核心客户 7 天未活跃自动通知客户成功经理。权益到期优惠券/积分到期前 3 天自动提醒。订单异常订单长时间卡在中间状态自动推送催办提醒。每个场景只是换了筛选条件、文案模板和回调处理逻辑调度、发消息、状态回收这些底座都没有变。这也是我为什么先把唤醒链路打磨稳的原因——它是所有主动触达业务的基石。我自己跑下来的体会是定时任务和企微消息 API 本身都不复杂真正复杂的是把“运营节奏”翻译成“调度策略”再把“发送成功”翻译成“客户真的回应了”。宁可少触达不要瞎触达宁可先跑 100 条测试名单也不要一上来就全量。最后再分享一个小习惯每次上线新的唤醒任务前我会先用测试成员列表跑一轮 mock 发送人工检查文案、链接、App 跳转是否正常再放开全量。这个动作已经帮我避免了好几次“上线两小时收到十几条投诉”的尴尬。
返回列表