简介:这是一份基于 Java 的微信手机号批量导入与开通状态检测系统源码,后端采用 SpringBoot 框架,前端使用 LayUI 界面库,数据存储于 MySQL 关系型数据库,面向需要批量核验手机号是否注册微信的开发人员或中小型运营团队,解决手动逐个查询效率低的问题。整个压缩包以 RAR 格式封装,共 530 个文件,体积约 110.33MB,包含 38 个 Java 源码、38 个 class 编译文件、165 个 xml 配置、4 个 properties 环境配置、1 个 sql 数据库脚本;前端配套 44 个 js、26 个 css、14 个 html 及大量 gif 图片,项目层级清晰,可导入 IntelliJ IDEA 运行调试。已有 1570 人学习。源码覆盖控制层、业务层、文件上传解析、RESTful 接口封装等关键模块,并借助 Apache POI 实现表格文件批处理,结合 RedisUtils 等工具类完善业务扩展;数据库脚本包含基础表结构和测试数据,便于快速搭建环境,适合作为企业工具开发或毕业设计的参考。压缩包内还附有日志、文档、图标等辅助文件,方便对照排错。
1. 批量查微信开通状态:这套 Java 系统到底帮你解决了什么
销售和运营手里积压了一批手机号,想快速知道哪些人开通了微信,好决定后续用哪种方式触达。手动一个个在微信里搜索添加,几百条就够累,上万条基本不可能完成。这个项目要做的,就是把这件手工作业变成一套基于 Java 的批量任务系统:导入 Excel 手机号,清洗去重,落库,再交给检测通道逐条确认开通状态,最后把结果导出成带标签的名单。它解决的不是"能不能查"的问题,而是"批量、可追溯、可复用"的问题。适合正在做私域运营、客户筛选或销售线索清洗的团队,也适合想拿一个真实业务场景练手 Java 后端完整链路的开发者——从文件解析、数据清洗、异步任务调度到数据库设计,一条线全都能碰到。
2. 数据模型先行:批次表、号码表与检测日志表如何设计
很多人做这类工具会踩同一个坑:拿到需求就想先把导入和检测跑通,数据库表随便建两张,结果跑到一半发现没法知道"这批号码检测到哪了""某条号码检测了几次""失败原因是什么"。我一般会先定数据模型,再写业务代码。这套系统表不多,三张核心表加一张用户表就够,但每张表的字段都要能支撑起一个完整任务的生命周期,而不是临时凑一场。
2.1 为什么必须落库:任务可追溯与断点续跑
如果只是为了几千条号码跑一次,用内存加文件导出行不行?行,但不值得。批量检测的典型场景是万级以上的号码、耗时以小时计,中间任何一次服务器重启、网络抖动、通道触发风控暂停,都可能导致任务中断。如果不落库,中断后只能重新导一次,之前的检测全部浪费;落库之后,每次检测完成就把结果写回,任务恢复时只需要扫一遍状态为"待检测"的记录继续跑,这就是断点续跑。
这是把一次性脚本升级为"系统"的关键一步。源码里的设计原则是:号码表只存号码本身和清洗信息,检测结果单独放状态字段,每一次检测动作都记录一条检测日志。日志表的作用是审计——某条号码到底通过哪个通道、在什么时间、拿到什么返回结果,出了纠纷能翻旧账,而不是对着一条孤零零的状态码猜测。
2.2 三张表的建表 SQL 与索引设计
我用 MySQL 8.0 为例,字符集统一用 utf8mb4,排序规则用 utf8mb4_unicode_ci。手机号字段用 varchar(11) 而不是 bigint,原因是手机号前面可能带 +86 或 0086 前缀(由清洗层处理),且字符串更稳妥,避免数值精度问题。
CREATE TABLE import_batch ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '批次ID', batch_no VARCHAR(32) NOT NULL COMMENT '批次号,格式YYYYMMDDHHMMSS+随机4位', file_name VARCHAR(255) NOT NULL COMMENT '原始文件名', total_count INT NOT NULL DEFAULT 0 COMMENT '文件内号码总数', valid_count INT NOT NULL DEFAULT 0 COMMENT '清洗后有效号码数', detect_count INT NOT NULL DEFAULT 0 COMMENT '已检测号码数', status TINYINT NOT NULL DEFAULT 0 COMMENT '批次状态: 0待导入, 1导入中, 2待检测, 3检测中, 4已完成, 5失败', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, finish_time DATETIME DEFAULT NULL, UNIQUE KEY uk_batch_no (batch_no) ) ENGINE=InnoDB COMMENT '导入批次表'; CREATE TABLE phone_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, batch_id BIGINT NOT NULL COMMENT '所属批次', phone VARCHAR(11) NOT NULL COMMENT '清洗后的11位手机号', carrier VARCHAR(10) DEFAULT '' COMMENT '运营商归属: 移动/联通/电信/广电/虚拟', province VARCHAR(20) DEFAULT '' COMMENT '归属地(可选)', wx_status TINYINT NOT NULL DEFAULT 0 COMMENT '微信状态: 0未知, 1已开通, 2未开通, 3检测失败, 4风控跳过', detect_count INT NOT NULL DEFAULT 0 COMMENT '累计检测次数', last_detect_time DATETIME DEFAULT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_phone (phone), KEY idx_batch_status (batch_id, wx_status) ) ENGINE=InnoDB COMMENT '手机号记录表'; CREATE TABLE detect_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, batch_id BIGINT NOT NULL, phone_id BIGINT NOT NULL, phone VARCHAR(11) NOT NULL, channel VARCHAR(20) NOT NULL COMMENT '检测通道: android_contact/manual/sms', result TINYINT NOT NULL COMMENT '本次检测结果,取值同wx_status', cost_ms INT NOT NULL DEFAULT 0 COMMENT '通道耗时(毫秒)', error_msg VARCHAR(500) DEFAULT '' COMMENT '错误信息', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_phone (phone), KEY idx_batch_time (batch_id, create_time) ) ENGINE=InnoDB COMMENT '检测日志表';三张表的关系是这样:import_batch 是一批任务的汇总行,phone_record 是明细,detect_log 是明细上的操作流水。唯一索引 uk_phone 直接放在 phone_record 上,用来做天然去重,后面导入时会用到 INSERT ... ON DUPLICATE KEY UPDATE 完成幂等写入。
索引设计上,phone_record 的 (batch_id, wx_status) 联合索引很关键。当任务恢复续跑时,最常执行的 SQL 是"查某批次里状态为 0 的号码",这是等值加范围查询,复合索引能把扫描范围从全表缩小到一个批次的一个状态。detect_log 的 (batch_id, create_time) 索引是为了按批次导出审计记录,否则在百万级日志下全表扫描会把数据库拖垮。
2.3 批次状态机的定义与代码映射
批次状态我定义了六种:0 待导入、1 导入中、2 待检测、3 检测中、4 已完成、5 失败。为什么要卡这么细?因为导入和检测是两段独立的流程,中间可能隔很久。比如晚上导入完,第二天早上才开检测,如果没有"待检测"这个中间态,系统就没法区分"刚建批次还没导完"和"导完了可以开始检测"。
对应到 Java 代码里,我习惯用枚举而不是魔法数字。源码里定义一个 BatchStatusEnum:
public enum BatchStatusEnum { PENDING(0, "待导入"), IMPORTING(1, "导入中"), READY(2, "待检测"), DETECTING(3, "检测中"), FINISHED(4, "已完成"), FAILED(5, "失败"); private final int code; private final String desc; BatchStatusEnum(int code, String desc) { this.code = code; this.desc = desc; } public static BatchStatusEnum of(int code) { for (BatchStatusEnum value : values()) { if (value.code == code) { return value; } } throw new IllegalArgumentException("未知批次状态: " + code); } }状态流转规则写在一个服务方法里:导入接口创建批次时状态为 PENDING,解析 Excel 开始后置为 IMPORTING,解析完成并落库后置为 READY;检测调度器发现 READY 的批次,置为 DETECTING 并开始逐条检测;全部号码检测完且没有待重试的,置为 FINISHED;如果中途出现不可恢复的错误,置为 FAILED。用枚举的好处是,所有判断都走 of() 方法,状态码写错在开发期就暴露,而不是在线上跑出脏数据。
batch_no 的生成我用了时间戳加随机四位数字,理由是既保证可读性(从批次号能看出创建时间),又避免并发下生成重复主键。如果你要用批量导入框架,还可以在这个字段上存文件内容的 hash,重复上传时直接幂等返回,稍后在最后一章细说。
3. 用 POI 把 Excel 变干净数据:导入解析与清洗的完整链路
导入环节的输入是一个 .xlsx 或 .xls 文件,输出是一张干净的 phone_record 表。这个链路里最烦人的不是解析本身,而是用户给的 Excel 格式千奇百怪:手机号被设置成数值格式、前面带 +86、中间有空格、混入座机号和 400 电话。处理不好,检测环节就全是脏数据。
3.1 上传接口与文件校验(MultipartFile 的四个检查)
文件上传用 Spring MVC 的 MultipartFile 接收,接口做的事很少:校验、建批次、丢给异步线程池。很多人在这一步犯错——在 Controller 里同步解析,一个 5 万行的 Excel 加清洗可能要十几秒,HTTP 请求直接超时,前端拿到超时后又不会自动重试,最后用户以为没传上去,重复提交产生一堆脏批次。
@PostMapping("/api/batch/import") public Result<Long> importBatch(@RequestParam("file") MultipartFile file) { // 1. 扩展名校验:只允许 .xlsx 和 .xls String original = file.getOriginalFilename(); if (original == null || (!original.endsWith(".xlsx") && !original.endsWith(".xls"))) { return Result.error("仅支持 .xlsx 或 .xls 文件"); } // 2. 文件大小校验,超过20MB直接拒绝 if (file.getSize() > 20 * 1024 * 1024) { return Result.error("文件不能超过20MB"); } // 3. 创建批次,状态为 PENDING ImportBatch batch = batchService.createBatch(original); // 4. 异步解析,避免大文件阻塞HTTP线程 taskExecutor.execute(() -> importService.doImport(batch.getId(), file)); return Result.success(batch.getId()); }参数说明:文件大小限制放在 20MB,按一行号码加几个字段平均 30 字节算,大约能装 60 万行,已经超出大多数运营一次导入的量级。如果要支持更大的文件,不建议调大 limit,而是先让用户拆文件,或者改用流式解析。异步线程池的配置要单独给:核心线程 2、最大线程 4、队列容量 100,不要把导入任务和检测任务混在同一线程池里,否则大文件解析会把检测任务饿死。
3.2 POI 解析 .xlsx 与 .xls:彻底告别科学计数法
解析我分两层写:第一层用 WorkbookFactory.create 自动识别版本,第二层逐行读取并统一转字符串,核心是 DataFormatter。
public List<String> parsePhones(InputStream in, int maxRows) throws IOException { List<String> result = new ArrayList<>(); // WorkbookFactory 会根据文件头自动判断 xlsx/xls try (Workbook workbook = WorkbookFactory.create(in)) { Sheet sheet = workbook.getSheetAt(0); // 只取第一个 sheet DataFormatter formatter = new DataFormatter(); // 关键:按显示格式读单元格 for (Row row : sheet) { if (row.getRowNum() == 0) { continue; // 跳过表头 } Cell cell = row.getCell(0); // 默认手机号在第一列 if (cell == null) { continue; } String value = formatter.formatCellValue(cell).trim(); if (value.isEmpty()) { continue; } result.add(value); if (result.size() >= maxRows) { break; } } } return result; }这段代码里常见的误用是直接 cell.getStringCellValue()——遇到数字单元格会抛 IllegalStateException;另一种误用是 cell.getNumericCellValue() 再转 long,看起来没问题,但 11 位号码在 double 下只有约 15 位有效数字,只要原始 Excel 设置的是常规格式,拿到的就是 1.3812345678E10 这种科学计数法,还原不回去。DataFormatter 的职责是按单元格的显示格式格式化,文本格式读成字符串,数值格式读成它显示的样子,能避开八成科学计数法的坑。
maxRows 参数是安全阀。异步解析怕的是用户传一个 100 万行的文件直接 OOM。我习惯在解析方法里设置上限,超出就截断并记录告警,返回给用户"文件过大,已截断前 N 条"。生产环境里 maxRows 我设 50 万,如果业务真的需要百万级,建议换 EasyExcel 的流式监听器,或者把 XSSFWorkbook 换成 SAX 事件模型,后者处理 100 万行内存占用可以控制在 200MB 以内。
3.3 号码清洗规则与 MyBatis 批量落库
解析出来的原始字符串不能直接入库。第一步统一规整:去掉 +86、0086 前缀,去掉空格和横线;第二步格式校验:必须 1 开头、共 11 位、全是数字;第三步按号段判断运营商。虚拟运营商号段(170/171/167 等)也是真实可用的手机号,不做剔除,只做标注。
public class PhoneCleaner { private static final Pattern PHONE_PATTERN = Pattern.compile("^1[3-9]\\d{9}$"); public static CleanedPhone clean(String raw) { String s = raw.trim() .replaceAll("[\\s-]", "") .replaceAll("^(\\+86|0086)", ""); if (!PHONE_PATTERN.matcher(s).matches()) { return null; // 非法号码直接丢弃 } String carrier = judgeCarrier(s); return new CleanedPhone(s, carrier); } private static String judgeCarrier(String phone) { char second = phone.charAt(1); if ("45".indexOf(second) >= 0) return "移动"; if ("78".indexOf(second) >= 0) return "联通"; if (second == '3' || second == '9') return "电信"; // 简化判断 return "未知"; } }判断运营商这段我特意写了"简化判断",因为各运营商的号段不断更新,精确到每个号段的规则应该抽成配置表,定期同步运营商标识库。生产环境里可以把号段规则放到 dict_carrier 表,启动时加载到本地缓存,比硬编码在代码里好维护。运营商判断不追求 100% 准确,只用于结果统计,不影响检测正确性。
落库用 MyBatis 的批量插入。这里要说明为什么不用 foreach 拼一个超大 INSERT:5 万条拼成一条 SQL 会超过 MySQL max_allowed_packet,而且事务太大回滚代价高。正确姿势是分批:
<insert id="batchInsert" parameterType="list"> INSERT INTO phone_record (batch_id, phone, carrier) VALUES <foreach collection="list" item="item" separator=","> (#{item.batchId}, #{item.phone}, #{item.carrier}) </foreach> ON DUPLICATE KEY UPDATE carrier = VALUES(carrier) </insert>调用方每 500 条提交一次,用一个事务包住。批大小 500 是稳定区间,太小频繁提交慢,太大单条 SQL 太长解析开销高。压到每批 2000 条也行,但 JDBC URL 上必须加 rewriteBatchedStatements=true,否则 MyBatis 虽然发了批量 SQL,驱动还是会逐条执行,性能差 10 倍以上。这是整条导入链路里性价比最高的一个优化点。
4. 检测通道怎么选:Android 通讯录匹配方案与结果状态机
先说实话:微信没有提供"输入手机号查询是否注册"的开放接口,公众号和小程序里的 OAuth 只能拿到已授权用户的信息,拿陌生号码去问是不行的。所以这个系统的检测环节,采用行业里最常见的通讯录匹配思路——把待检测号码导入手机通讯录,微信客户端会在"新的朋友"列表里自动展示已开通微信的联系人。谁出现在列表里,谁就是已开通;没出现的,就是没开通或尚未匹配到。
4.1 检测通道的真实分工:后端调度 + 设备端执行
这套架构里,Java 后端负责任务调度、数据下发、结果回收和数据库落盘;真正执行"打开微信、读取匹配结果"动作的,是一台 Android 测试机上运行的辅助 App(基于无障碍服务)。后端和设备端通过 HTTP 接口通信:后端把一批号码下发到设备,设备端模拟人工操作完成匹配后逐条上报结果。
这个分工很重要。它把不可控的 UI 操作隔离在设备端,Java 后端保持稳定,换手机、换微信版本只影响设备端。而且后端只管拿到结果写库,设备端到底是怎么操作的,后端完全不用关心。如果以后要换检测方式,比如接入短信验证码辅助,只要设备端换一套执行逻辑,后端代码一行都不用动。
提示:这种检测方式只用于自有客户名单的运营核对,确保号码来源合法且在用户授权范围内使用。不要用自动化大规模骚扰用户,控制频率是对渠道和自己账号的保护。
4.2 DetectChannel 接口与通讯录匹配实现(含节流参数)
后端对检测动作做一个抽象通道接口,以后接入其他检测方式不用改业务代码:
public interface DetectChannel { String channelName(); DetectResult detect(PhoneRecord record, DetectContext context); } public class AndroidContactChannel implements DetectChannel { @Override public String channelName() { return "android_contact"; } @Override public DetectResult detect(PhoneRecord record, DetectContext context) { // 1. 向设备端下发号码(通过设备网关) DeviceClient device = context.getDeviceClient(); // 2. 设备端执行通讯录写入、微信匹配、结果读取 DeviceReport report = device.submitContactMatch(record.getPhone()); // 3. 把设备返回的原始证据转成统一结果 return DetectResult.fromDeviceReport(report); } }通道抽象的价值在于,检测核心业务(任务推进、状态流转、重试、统计)只依赖 DetectChannel 接口。你换成别的通道,只需要 new 一个实现注册进去。DetectResult 里除了开通状态,还带 evidence(设备端返回的截图路径、操作回执)、costMs 耗时、retryable 是否可重试三个字段,这几个字段在重试策略和人工核对里非常有用。
设备端的节流参数写在后端的配置里,这是控制风险的关键:
detect: channel: android_contact thread-pool-size: 3 per-phone-min-interval-ms: 3000 batch-request-size: 20 retry-times: 2 retry-interval-ms: 10000 risk-pause-seconds: 60参数按真实踩坑经验设定:thread-pool-size 是并发设备台数,一个设备一个线程,不是越多越好,3 台设备一天已经足够跑几十万条;per-phone-min-interval-ms 是同一设备相邻两条检测的最小间隔,防止高频触发风控;retry-times 和 retry-interval-ms 控制失败重试;risk-pause-seconds 是触发风控后整批暂停的时长,我一般设 60 秒,宁可慢也不去挑战风控。
4.3 结果状态机:PENDING、OPENED、NOT_OPENED 与 UNKNOWN
设备端返回的结果不是简单的"是/否",我设计了四种状态:已开通、未开通、检测失败、风控跳过。原因是设备端的匹配本身有不确定性——微信的通讯录匹配有延迟,刚导入的号码立即去读,可能还没被服务端同步,返回"未开通"实际上是假阴性;设备端弹验证码或操作超时,也不该硬判成"未开通"。
状态流转用一个服务类管理,重点是不允许从"已开通"回退到"未开通":
public class DetectStateMachine { public static boolean canTransit(int oldStatus, int newStatus) { // 已开通是终态,不允许回退 if (oldStatus == WxStatus.OPENED.getCode()) { return false; } // 未知状态不允许作为最终结果 if (newStatus == WxStatus.UNKNOWN.getCode()) { return false; } return true; } }这个状态机的业务来源是:通讯录匹配一次没匹配到,不代表对方没开微信,可能是对方关掉了"通过手机号搜索到我"的开关,或者微信服务端还没完成索引。所以"未开通"要区分两种情况——多次检测稳定未匹配,和单次检测未匹配但次数不足。我在 phone_record 里用 detect_count 字段做累计,只有当 detect_count >= 2 且从未出现过 OPENED 时,才把最终标记定为"未开通";否则保留为 UNKNOWN,交给运营判断。这是对结果准确性负责的做法,虽然多消耗一次检测配额,但避免了把潜在客户误判掉。
5. 导入检测全流程避坑:五个高频事故与我的解决记录
这套系统的坑主要集中在 Excel 解析、批量插入、重复数据和任务恢复四个环节。下面五条按"现象→原因→解决"记录,都是线上实际发生过的。
5.1 Excel 手机号变成 1.38E+10
现象:导入后导出的号码全是 1.38E+10 这种科学计数法,有些变成 13812345678.0,清洗后正则匹配不上,有效数只有几千。
原因:用户 Excel 里手机号列是"常规"格式,POI 读取数值单元格时,getStringCellValue 直接抛异常;用 getNumericCellValue 拿到 double,11 位号码在高位丢失精度,转字符串就成了科学计数法。
解决:统一用 DataFormatter 读取单元格,它在底层会按单元格格式转字符串。再在清洗正则里兼容纯数字字符串,先转 BigDecimal 再转 string 做兜底。治本的办法是给运营发一个手机号列预置为"文本"格式的导入模板,但不能指望所有人按模板来,代码里必须兜底。
5.2 500 条批量插入慢到像单条执行
现象:往 MySQL 插 5 万条号码,跑了三分钟都没结束,数据库 CPU 不高但连接数被打满。
原因:MyBatis 的 foreach insert 默认走 PreparedStatement 逐条 execute,驱动根本没有启用批量模式。看日志能看到 5 万条 INSERT 语句一条条刷。
解决:JDBC URL 加 rewriteBatchedStatements=true,这是 MySQL 驱动提供的批量重写开关。改完同一个批次从三分钟降到三秒。隐藏点:驱动版本低于 5.1.8 时该参数不生效,换成 8.x 驱动即可。排查时可以开 MyBatis 的慢 SQL 日志,确认批量是否真的合成了多 VALUES 的 INSERT。
5.3 重复号码重复检测,白白消耗配额
现象:同一个手机号在多个批次 Excel 里出现,系统又检测了一遍,设备端配额消耗翻倍,运营统计的检测量虚高。
原因:phone_record 的唯一索引没建,或者建了但导入代码用 INSERT IGNORE 而不是 ON DUPLICATE KEY UPDATE,重复数据虽然没插入,但一批的 total_count 还是按 Excel 行数算的,造成"检测了 5 万条、实际只有 3 万个独立号码"的假象。
解决:建 uk_phone 唯一索引,导入时用 ON DUPLICATE KEY UPDATE 更新载体归属,批次统计按实际插入量计算。更稳的做法是导入前先 SELECT 批量比对,把已存在的号码过滤掉,避免每次都走唯一索引冲突的异常路径。
5.4 任务中断后批次永久停在 DETECTING
现象:服务器半夜重启,第二天所有检测中的批次全部卡死在"检测中",调度器不再扫描,整个任务静默死亡。
原因:批次状态存在数据库里没恢复。DETECTING 状态被写进去后,应用重启没有代码把这些批次重置回 READY,而调度器只扫描 READY 状态,于是任务永远停在半路。
解决:在应用启动时加一个 ApplicationRunner,把状态为 DETECTING 且 finish_time 为空的批次全部更新回 READY,同时把该批次下状态为检测中的号码重置回未知。如果你用了消息队列做任务分发,还需要处理已投递未消费的消息,靠消息幂等键去重。
5.5 座机、400 电话混进手机号池
现象:Excel 里混着 010-12345678、400 电话、8 位直线号码,清洗正则没拦住,设备端下发时各种异常,甚至把非手机号也写进了通讯录。
原因:只做了"以 1 开头共 11 位"的粗校验,但座机号经过前面的替换处理后也可能变成 11 位数字,恰好绕过检查。
解决:正则收紧为 ^1[3-9]\d{9}$,第二位限定 3-9,天然排除座机区号和 400 号段。同时在清洗阶段统计每种丢弃原因的数量,生成错误报告告诉用户"这批文件里有 200 个座机号已被过滤",避免用户以为是系统把号码弄丢了。
6. 用 2000 条假数据压一遍:验证、调参与上线前的核对
6.1 用假号段生成器造测试数据
检测类功能不能拿真实客户数据直接测,先构造一批既符合号段规则又不会打扰真人的号码:
public static String fakePhone() { String[] prefixes = {"139", "138", "188", "177", "159"}; String prefix = prefixes[ThreadLocalRandom.current().nextInt(prefixes.length)]; return prefix + String.format("%08d", ThreadLocalRandom.current().nextInt(100000000)); }假号码生成后先查库去重,2 万条假数据足够验证导入吞吐量和清洗正确率。
6.2 小样本人工核对与三项指标
批量压测通过不等于检测结果可信。我会从检测结果里抽样 100 条,人工在真实微信里手动搜一遍,统计三项指标:准确率(判定为已开通里真正开通的比例)、漏报率、不可判定比例。如果准确率低于 95%,先查设备端的匹配等待时间,微信通讯录同步需要时间,等待太短会漏掉大量已开通用户。
6.3 主键冲突时的幂等处理技巧
最后说一个很顺手的小技巧:重复导入同一份文件,结果不翻倍。做法是批次号生成时带文件内容 hash,同一个文件重复上传直接返回已存在的批次号,而不是新建批次。这个技巧看似小,实际很省事——运营同事经常觉得"上次没传成功"再传一次,没有幂等,数据库里全是识别不清的脏批次。
我的习惯是每个批量工具上线前,先用脚本造一批假数据完整跑通,再看一眼抽样核对结果,确认没问题才敢把真实数据交给它。这类工具一次误判,可能让运营把一个没开微信的人当成潜在客户去触达,浪费一次珍贵的机会。希望这套思路帮到你,批量导入与检测的坑基本都集中在数据模型、清洗规则和状态机这三件事上,把它们定稳,剩下的都是体力活。
本文还有配套的精品资源,点击获取