简介一套基于Java的手机批量导入与微信开通状态检测系统源码采用SpringBoot轻量级框架、LayUI前端组件库与MySQL数据库组合开发核心解决大量手机号的批量导入、解析及微信注册状态校验问题适用于需要批量验证号码可用性的运营人员或开发者参考。系统整体处理流程清晰前端上传CSV或Excel文件后端解析入库后逐条执行检测最终将开通结果实时反馈至界面展示。资源为rar压缩包整体110.33MB共530个文件涵盖38个Java源码与38个class编译文件、165个xml配置、44个js与26个css前端资源、14个html页面以及bath_phone.sql数据库脚本还包含演示gif和常用前端字体图标文件目录结构完整便于检索。项目内置application.properties等配置文件学习时可结合IntelliJ IDEA直接运行调试。目前已有1572人学习下载适合希望深入掌握SpringBoot整合LayUI、批量数据读写与第三方检测接口调用技术的开发者具备较高的实战参考价值。1. 这个 Java 检测工具能干什么一批手机号返回谁开通了微信做 CRM 清洗和私域运营的人大概率都遇到过这样的场景手里拿着几千行手机号 Excel想筛出哪部分已经开通微信好去做后续添加和维护。人工一个个去微信里搜索效率极低搜索动作频繁还会触发客户端限制。这个基于 Java 的「手机批量导入微信手机号检测系统」解决的正是这个问题把手机号文件批量导入程序自动去查询每个号码是否开通微信再把结果写进数据库并导出。源码里包含了完整的 Java 工程和数据库文件适合有 Java 基础、需要做客户资源清洗或定期核对手机号微信状态的开发者和运营人员。2. 检测原理没有官方接口就抓包它的搜索查询逻辑很多第一次接触这个资源的人都会问同一个问题Java 代码到底凭什么判断一个手机号开通了微信微信官方并没有提供「输入手机号返回是否注册微信」这种公开接口所以这类工具走的大多是抓包链路也就是模拟微信客户端自身的查询行为。2.1 四种检测方案对比为什么最终选抓包接口在我实际见过的检测方案里能落地的差不多有四种各自的门槛和稳定性完全不一样方案门槛稳定性适用场景微信网页版搜索接口伪造 UA低抓包一次即可会随版本变动个人内部工具微信客户端抓包内部查询接口中需要抓包工具相对稳定但需维护本资源采用的路线第三方聚合 API付费低但按条计费稳定商业项目、对 SLA 有要求官方开放平台接口需要企业资质和用户授权最稳但不适合批量检测合规业务场景本资源里的源码走的是第二条路线通过抓取微信客户端在「手机号搜索用户」场景下发出的请求拿到接口地址和请求头然后在 Java 程序里模拟这个请求。这个方案的优势是不依赖第三方付费服务跑一次的成本几乎为零代价是接口地址和参数可能随着微信版本升级而失效属于需要定期维护的工具。我自己的经验是这类接口通常能稳定用上较长周期失效后重新抓一次包、把新地址替换进去就行完全在可控范围内。2.2 伪造微信浏览器请求头关键 Header 与请求封装这类检测接口有一个共同特征对请求来源识别得很严。直接在浏览器里敲接口地址返回的都是风控页面或错误码但把请求头换成微信客户端内置浏览器的标识再带上正常的 Referer 和 Accept接口就会按正常客户端逻辑处理。下面是一段可用的 HTTP 请求封装代码import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.Duration; /** * 检测请求封装模拟微信内置浏览器的请求头 */ public class WechatCheckClient { // 占位地址实际地址需要从抓包里获取不同版本接口路径不同 private static final String CHECK_URL https://your-wechat-api.example.com/search; // 微信内置浏览器 UA关键是不能被识别成普通 PC 浏览器 private static final String USER_AGENT Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.27(0x18001b2b) NetType/WIFI Language/zh_CN; public static String check(String phone) throws Exception { HttpClient client HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(); HttpRequest request HttpRequest.newBuilder() .uri(URI.create(CHECK_URL ?phone phone)) .header(User-Agent, USER_AGENT) .header(Referer, https://weixin.qq.com/) .header(Accept, application/json, text/plain, */*) .timeout(Duration.ofSeconds(8)) .GET() .build(); HttpResponseString response client.send(request, HttpResponse.BodyHandlers.ofString()); // 原始响应必须留日志接口升级后靠它定位变化 return response.body(); } }这段代码里connectTimeout管的是建立连接阶段设 5 秒足够.timeout(8)管的是整个请求周期接口慢的时候可以适当放大但不要超过 10 秒否则批量检测的节奏会被拖垮。请求参数里只带了phone这是最基础的查询形态实际抓包时可能还要带上ticket、scene这类动态参数那些参数属于会过期的密钥类信息处理方式在避坑章节里细说。2.3 响应解析怎么从返回 JSON 判定「已开通」接口返回的 JSON 结构在微信不同版本里略有差异但核心逻辑是一致的能解析出用户对象含昵称、头像等公开信息就说明该号码已开通微信返回空用户或明确错误码则判定未开通。需要特别注意的是错误码里有一类代表「参数失效」或「被风控」这类结果不能被当成未开通处理。解析代码可以这样写import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; /** * 解析接口响应输出检测结论 */ public class ResultParser { public static boolean isWechatUser(String responseBody) { if (responseBody null || responseBody.isBlank()) { return false; } JSONObject obj JSON.parseObject(responseBody); int retCode obj.getIntValue(ret); if (retCode ! 0) { // 非 0 通常代表风控或参数异常抛专用异常让上层处理 throw new BlockedException(接口返回非0原始响应 responseBody); } JSONObject userInfo obj.getJSONObject(user); // 能取到非空昵称就认为该手机号开通了微信 return userInfo ! null userInfo.getString(nickname) ! null !userInfo.getString(nickname).isBlank(); } }ret 0是接口正常响应的标志在这个前提下再去拿user对象。之所以把非 0 情况单独抛异常而不是返回 false是防止风控状态下的大批量「未开通」污染最终统计结果这类数据应该标记为待重试而不是直接算作无效号码。提示接口属于抓包产物生产使用前务必先拿几个已知号码做验证并保留原始响应日志接口升级后第一时间对比差异。3. Java 批量导入文件读取、号码校验与线程池限速原理清楚了剩下的就是工程落地。整套源码的职责划分很明确入口类负责调度工具类负责读文件和解析服务类负责调接口数据层负责结果落库。新手拿到源码后先不急着改逻辑把每个类对应到这条链路上后面调参就知道该动哪里。3.1 项目结构与职责划分源码包里的工程结构大致如下类名职责关键点BatchImportMain程序入口串联整个流程参数解析、批次初始化FileImportUtil读取 txt/csv校验手机号格式支持 UTF-8 和 GBKPhoneValidator手机号合法性校验正则 号段过滤WechatCheckClient发起检测请求模拟微信 UAResultParser解析响应判定开通状态区分风控与真实未开通ResultWriter结果导出与写库支持 CSV 落盘PhoneCheckDaoJDBC 持久化操作批量插入、幂等更新对第一次接触这类工具的人来说WechatCheckClient和ResultParser是核心其余都是常规的 IO 和数据库操作。熟练的开发者则会重点关注PhoneCheckDao里的批量写入实现因为号码量一旦上万单条插入性能和批量插入能差出一个量级。3.2 文件读取与手机号校验从 txt/csv 导入导入文件最常见的坑是编码问题Windows 下导出的 txt 经常是 GBK。本资源的读取工具类同时兼容了 UTF-8 和 GBK并且把「手机号、姓名」两种格式都处理了不管文件里是纯手机号还是带姓名带逗号分隔都能正确取到号码字段import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; public class FileImportUtil { // 国内手机号1 开头第二位 3-9后 9 位数字 private static final Pattern PHONE_PATTERN Pattern.compile(^1[3-9]\\d{9}$); public static SetString readAndValidate(String filePath, String charsetName) throws Exception { Charset charset charsetName null ? StandardCharsets.UTF_8 : Charset.forName(charsetName); ListString lines Files.readAllLines(Path.of(filePath), charset); SetString validPhones new LinkedHashSet(); for (String line : lines) { String trim line.trim(); if (trim.isEmpty()) { continue; } // 兼容 手机号 和 手机号,姓名 两种格式 String phone trim.split([,\t])[0].trim(); if (PHONE_PATTERN.matcher(phone).matches()) { validPhones.add(phone); } else { // 非法号码单独打印不影响主流程 System.out.println([跳过] 非法手机号: trim); } } return validPhones; } }这里用LinkedHashSet而不是HashSet是因为它能在去重的同时保持文件里的原始顺序后面导出结果时手机号顺序不会乱跳。readAllLines一次性读入内存对几万行文件完全够用如果跑到百万级再换成BufferedReader逐行处理。正则里[3-9]覆盖了目前主流的号段虚拟运营商号码如 170、171不在这条正则内这类号码会被跳过并打印提示如果你手里有大量虚拟号段的用户可以把正则改成^1(?:[3-9]|70|71)\\d{9}$。3.3 线程池与令牌桶批量检测的并发控制批量检测最忌一个号码一个号码地串行跑几千个号码能把人等到怀疑人生。源码里用了「线程池 信号量限速」的组合线程池决定同时有几个请求在飞信号量控制每秒钟最多放行多少个请求。这两个参数分开调互不干扰import java.util.concurrent.*; public class BatchChecker { private final ExecutorService executor; private final Semaphore limiter; // 令牌桶每秒放行额度 private final ScheduledExecutorService scheduler; public BatchChecker(int threads, int permitsPerSecond) { this.executor Executors.newFixedThreadPool(threads); this.limiter new Semaphore(permitsPerSecond); // 每秒补充一次令牌控制全局 QPS this.scheduler Executors.newSingleThreadScheduledExecutor(); this.scheduler.scheduleAtFixedRate(() - limiter.release(permitsPerSecond), 1, 1, TimeUnit.SECONDS); } public void checkOne(String phone) { executor.submit(() - { try { limiter.acquire(); // 拿不到令牌就阻塞等待 String body WechatCheckClient.check(phone); boolean opened ResultParser.isWechatUser(body); ResultWriter.save(phone, opened, body); } catch (BlockedException e) { // 风控类异常标记待重试不写入未开通 ResultWriter.save(phone, 2, e.getMessage()); } catch (Exception e) { ResultWriter.save(phone, 2, e.getMessage()); } }); } public void shutdown() { scheduler.shutdown(); executor.shutdown(); } }线程数这个参数IO 密集型的检测任务可以调大一点一般设CPU 核数 * 2到核数 * 4之间。但真正决定请求节奏的是permitsPerSecond这个数才是需要谨慎对待的个人 IP 环境下我一般控制在每秒 2 到 3 个请求跑五分钟观察一下返回的错误码没有异常再往上加。令牌桶的设计保证了即使线程池里有 20 个线程实际打到接口的 QPS 依然被限制在设定值附近。3.4 结果写库与日志落盘检测结果最终要落库这里必须用 JDBC 的批量插入否则几万条数据单条insert会慢到难以接受。每次都把PreparedStatement的addBatch()攒到 200 条再一次性提交性能最好超过这个阈值边际收益就开始下降import java.sql.*; import java.util.List; public class PhoneCheckDao { public void batchInsert(ListPhoneCheckRecord records) throws SQLException { String sql INSERT INTO phone_check (batch_id, phone, name, status, response_raw) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE status VALUES(status); try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { conn.setAutoCommit(false); int count 0; for (PhoneCheckRecord r : records) { ps.setLong(1, r.getBatchId()); ps.setString(2, r.getPhone()); ps.setString(3, r.getName()); ps.setInt(4, r.getStatus()); ps.setString(5, r.getResponseRaw()); ps.addBatch(); if (count % 200 0) { ps.executeBatch(); conn.commit(); } } ps.executeBatch(); conn.commit(); } } }ON DUPLICATE KEY UPDATE配合明细表上的唯一索引batch_id, phone实现的是幂等写入同一批次里重复提交的号码不会产生脏数据只会更新状态。这行处理在重跑时特别有用——上次跑到一半中断了这次直接从断点继续跑历史结果会被正确覆盖。响应原文response_raw字段要保留全量内容接口出问题时它是唯一的排错依据。4. 数据库与 SQL批次表、明细表加一条开通率统计资源附带的数据库文件不是随便建两张表就完事它的设计里有两个细节值得抄一是把「批次」和「明细」拆成两张表二是给明细表加组合唯一索引。这两个决定不复杂但直接影响这个工具能不能安全地反复使用。4.1 表结构设计为什么拆批次表和明细表第一次跑导入工具时我图省事只建了一张检测表结果第二次导入新号码时旧数据和新数据混在一起想按次统计开通率只能靠时间字段模糊切分非常难受。拆成批次表和明细表之后每次导入先插一条批次记录拿到batch_id明细表里每一行检测结果都归属于某个批次想算哪次的开通率就查哪次的互不干扰。明细表上的UNIQUE KEY uk_batch_phone (batch_id, phone)是第二道保险。批量导入场景最容易翻车的地方就是同一个号码在文件里出现多次如果不加唯一索引检测结果会被写入多条统计出来的开通率水分很大。加了唯一索引之后即使导入工具没做去重数据库层面也会强制只保留一条。4.2 建表 SQL 与字段说明数据库文件里自带的建表 SQL 大致如下我加了注释方便对照理解CREATE DATABASE IF NOT EXISTS wechat_check DEFAULT CHARACTER SET utf8mb4; USE wechat_check; -- 批次表一次导入对应一条批次记录 CREATE TABLE batch_info ( id BIGINT AUTO_INCREMENT PRIMARY KEY, batch_name VARCHAR(64) NOT NULL COMMENT 批次名称如 20240517_会员清洗, total_count INT NOT NULL DEFAULT 0 COMMENT 导入号码总数, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT检测批次表; -- 明细表每个手机号的检测结果对应一行 CREATE TABLE phone_check ( id BIGINT AUTO_INCREMENT PRIMARY KEY, batch_id BIGINT NOT NULL COMMENT 所属批次ID, phone VARCHAR(20) NOT NULL COMMENT 手机号, name VARCHAR(64) DEFAULT NULL COMMENT 姓名文件里有就带过来, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未开通 1已开通 2待重试, response_raw TEXT COMMENT 接口原始返回排错用, check_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_batch_phone (batch_id, phone), KEY idx_phone (phone), CONSTRAINT fk_batch FOREIGN KEY (batch_id) REFERENCES batch_info(id) ) ENGINEInnoDB COMMENT检测明细表;字段选择上有几个点需要说明字段为什么这么设计status用 TINYINT 而不是 VARCHAR状态值固定三个数字存得快、索引体积小response_raw用 TEXT接口返回可能超长TEXT 足够存完整响应phone用 VARCHAR(20)手机号不是数值不该用 BIGINT前面加 0 的风险也要避开外键fk_batch保证明细不会落到不存在的批次上防止脏数据多提一句手机号字段千万不要设计成数值类型。手机号没有任何数学运算需求超过 10 位以后用 BIGINT 虽然不会溢出但一旦未来要接国际号码就会非常被动。字符串类型是最稳妥的选择。4.3 常用统计与导出 SQL数据落库之后最常用的就是开通率统计和结果导出。开通率直接决定这批号码的清洗价值导出则用来把已开通号码交给运营同事做后续添加-- 当前批次的总数、已开通数、开通率 SELECT COUNT(*) AS total, SUM(status 1) AS opened_count, ROUND(SUM(status 1) / COUNT(*) * 100, 2) AS open_rate FROM phone_check WHERE batch_id 1; -- 导出已开通号码供后续微信添加 SELECT phone, name, check_time FROM phone_check WHERE batch_id 1 AND status 1 INTO OUTFILE /tmp/opened_phones.csv FIELDS TERMINATED BY , OPTIONALLY ENCLOSED BY LINES TERMINATED BY \n;SUM(status 1)这种写法把条件判断放进聚合函数里一条 SQL 就能同时拿到总数和已开通数省掉了子查询。开通率算出来之后顺便看一眼status 2的待重试数量——如果占比超过 5%说明接口已经有相当概率被风控这批结果最好重跑一遍再投入使用。5. 避坑指南六个让批量检测翻车的细节这个工具我前后改过三轮翻车现场基本都集中在接口、编码和并发控制这三块。下面按「现象 → 原因 → 解决」的格式整理成六条每一条都是真实踩过之后沉淀下来的。5.1 读出来全是乱码源文件编码和代码不一致现象导入后控制台打印的中文姓名全是问号写入数据库的姓名字段也是乱码。原因Windows 下用记事本导出的 txt 默认是 GBK 编码而代码里Files.readAllLines默认按 UTF-8 读取GBK 字节被强行按 UTF-8 解码就产生了乱码。这是文件类工具最典型的编码事故。解决读取时显式指定字符集。资源里的FileImportUtil已经兼容了这个场景调用时传入GBK即可FileImportUtil.readAndValidate(phones.txt, GBK)。如果文件来源不确定先看最前面几个字节有没有 UTF-8 的 BOM 标记有 BOM 按 UTF-8没有就试 GBK。5.2 跑了几十个之后全线「未开通」被风控拦截了现象前面几十个号码检测结果完全正常之后突然所有号码都返回未开通且响应时间异常短日志里接口返回的错误码是统一的值。原因请求频率超过了接口的容忍阈值IP 被临时限流接口返回的是风控数据而不是真实查询结果。这跟玄学一样阈值没有公开标准而且不同 IP 段的表现差异很大。解决立刻停止任务把permitsPerSecond降到 1等 10 到 15 分钟再继续。如果仍然被拦就需要更换出口 IP个人环境下可以切换 4G 网络让运营商重分配 IP也可以换一个网络环境再跑。程序的status 2设计就是为了应对这种场景被风控的号码标记为待重试解封后重新跑这些数据即可。5.3 已开通号码被判漏号段覆盖不全现象拿 5 个已知开通微信的号码验证能识别出 4 个其中 1 个 170 号段的手机号被判定为未开通。原因虚拟运营商号段170、171和部分携号转网号码在接口返回的数据结构里和其他号段不一样可能缺少标准的nickname字段导致解析逻辑把它误判成未开通。解决两步走。第一步在解析逻辑里放开判断条件只要有user对象且包含头像地址就算开通第二步在正则校验阶段把这些号段放进来改成^1(?:[3-9]|70|71)\\d{9}$。改完之后重新跑一次验证集确认命中率。5.4 第二天接口突然全部超时接口地址或参数变了现象同一个程序昨晚跑得好好第二天再启动全部超时一个号码都查不出来。原因接口路径、参数名或签名规则被更新了旧地址已经失效。抓包类接口的通病微信客户端一升级对应的内部接口就可能调整。解决把status 2的待重试号码保留住不要删除。重新用抓包工具抓一次新版客户端的搜索请求对比新旧请求头差异更新代码里的CHECK_URL和相关参数。这个维护频率不算高我自己的经验是一个季度到半年一次但每次更新完都要先跑小批量验证再全量执行。5.5 重复号码重复计数导入前没去重现象导入 5000 行文件库里却出现了 6000 条记录翻看发现有大量相同手机号。原因Excel 导出时行与行之间有重复导入工具没做去重或者去重时只去了完全相同的行没去掉仅有姓名不同的重复号码。后果是接口被白白调用了一遍又一遍统计结果也被拉偏。解决FileImportUtil里按手机号字段单独去重而不是整行去重。同时依靠数据库的唯一索引兜底万一哪次漏了去重ON DUPLICATE KEY UPDATE会把重复插入变成更新不会产生真正的重复记录。5.6 线程开太大反而更慢资源全耗在等待上现象线程数从 8 调到 20检测总体耗时没有变短反而偶尔出现大批量超时。原因检测请求是 IO 密集型没错但线程数不是越多越好。线程太多时大量线程都在Semaphore.acquire()上排队加上每个请求还要等 8 秒超时线程资源全被阻塞请求占住了GC 压力也上来整体吞吐反而下降。解决把线程数和 QPS 分开看。线程数保持CPU 核数 * 2即可决定吞吐量的是permitsPerSecond。先用每秒 2 个跑 5 分钟看平均响应时间和错误码稳定再逐步上调到 3 或 4。6. 进阶技巧优雅停机、指数退避重试与结果导出工具能跑通只是第一步真正好用的检测程序还得能停得干净、错得可控。这部分我把自己在生产环境里加的三个能力直接给出来都是小改动但收益很大。6.1 优雅停机与断点续跑几万个号码的检测通常要跑半个小时以上中间难免有人误关终端或重启机器。直接在窗口上按CtrlC会让线程池里的任务瞬间中断已经发出但还没返回的请求全部丢失。注册一个关闭钩子让程序退出前等一等正在执行的请求再把进度记录下来Runtime.getRuntime().addShutdownHook(new Thread(() - { System.out.println(收到退出信号等待已提交任务结束...); executor.shutdown(); try { executor.awaitTermination(10, TimeUnit.SECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 记录当前已处理数量下次从断点继续 saveProgress(processedCount.get()); }));配合明细表里的status 2待重试标记中断后重新启动时只需查SELECT phone FROM phone_check WHERE batch_id ? AND status 2就能精确拿到未完成名单不用整个批次重跑。6.2 失败重试指数退避请求超时和网络抖动在批量场景里是常态直接放弃会把大量正常号码误标为待重试。重试要用指数退避第一次等 300 毫秒、第二次 600 毫秒、第三次 1.2 秒给接口留出恢复时间private static final int MAX_RETRY 3; public boolean checkWithRetry(String phone) { for (int attempt 1; attempt MAX_RETRY; attempt) { try { String body WechatCheckClient.check(phone); return ResultParser.isWechatUser(body); } catch (BlockedException e) { // 风控类错误不要重试直接标记待人工处理 ResultWriter.save(phone, 2, e.getMessage()); return false; } catch (Exception e) { if (attempt MAX_RETRY) { ResultWriter.save(phone, 2, e.getMessage()); return false; } // 指数退避300ms - 600ms - 1200ms Thread.sleep(300L * (1L (attempt - 1))); } } return false; }这里把BlockedException和普通超时区分开是关键。普通超时重试有意义风控类错误重试只会让 IP 被限制得更久。遇到风控就直接落库标记等解封后再跑。6.3 结果导出与人工复核最后一步是把已开通的号码导出给运营同事使用。除了用INTO OUTFILE导出也可以在 Java 里直接生成 CSV加一个 UTF-8 BOM 头Excel 打开就不会乱码。导出后我习惯抽 5% 的号码人工在微信里搜一遍确认接口状态没有异常再放心把名单交给业务方。这个工具我前后迭代过几版本质是把重复劳动交给程序但接口的脆弱性决定了它需要敬畏心。最开始那版我没有做去重和断点续跑5 万条数据的批次里有 2 万条重复白白消耗了一下午的接口配额还因为频率太高被限流了半天。从那以后我每次导入前都强制先跑一遍正则校验和唯一索引检查断点续跑和指数退避也成了标配。希望这篇拆解能帮你把这个源码用起来少走几步我走过的弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?