简介基于Java的SpringBootLayUIMySQL手机号批量导入与微信开通检测系统面向需要批量核验手机号是否已注册微信的运营、数据及Java开发者。系统以SpringBoot提供RESTful服务搭配LayUI构建后台管理界面支持通过前端上传CSV/Excel文件后端解析手机号列表后逐条调用检测机制并可将命中与未命中的号码分类保存至MySQL数据库前端以表格形式清晰展示结果适用于会员拉新、精准营销、通讯录清洗等场景。压缩包共530个文件大小约110.33MB。其中包含38个Java源码与38个class文件、44个js、26个css、14个html页面以及165个xml配置另有4个properties环境配置和bath_phone.sql数据库初始化脚本同时附带SpringBoot的mvnw、jar、日志、字体图标等资源目录完整。已有1572人学习下载。通过学习源码可掌握SpringBoot与LayUI前后端联调、Apache POI解析Excel、批量数据校验与结果持久化、Redis缓存使用等实践技巧对独立开发类似批量管理工具有直接参考价值。1. 基于Java的手机批量导入微信手机号系统检测是否开通到底解决什么问题做过私域运营或客户回访的人都懂这个痛点手头有一批老客户手机号想知道哪些已经开通微信、哪些没有只有开通了的号码才能进社群、做私域触达。Excel里放了五六千个号码人工一个个搜索不现实而且判断标准不统一。基于Java的手机批量导入微信手机号系统做的就是把Excel里的号码批量化导入数据库按任务队列逐条检测微信是否开通最后把已开通、未开通、无效号码分别落库导出全程无人值守。它面向的是运营岗、CRM开发人员和想把这套流程产品化的开发者核心价值是把“人工翻手机”变成“可复现的工程流水线”。2. 数据链路与表结构设计一个手机号从导入到出结果经历了什么写第一行业务代码之前先把“数据怎么流动”定下来。批量检测系统大部分翻车都发生在半路导入完发现脏数据、检测完结果没落库、程序重启后任务丢了。这些问题的根源只有一个——状态和数据模型没在动手前设计好。本章把整条链路拆开先说状态机再给可落地的表结构最后讲批量写入的选型理由。2.1 一条手机号要经历几个状态任务状态机是先于代码画出来的一条手机号从进系统到出结果状态流转应该是这样的待导入0 → 待检测0 → 检测中5 → 已开通1 / 未开通2 / 无效号码3 / 通道异常4注意“待导入”和“待检测”在表里其实是一个状态不需要额外字段区分。导入阶段结束就是“已入库”没有别的中间态。真正要细心设计的是“通道异常”这个状态——它不等于失败而是“检测通道繁忙、结果不可信、需要重试”的意思。我在生产环境里会给“通道异常”加一个fail_count字段同一号码连续重试3次仍然异常才允许把最终状态落成“通道异常”否则重试期间不断跳转回“待检测”。这样做的好处是不会因为一次网络抖动就把有效号码误判成“未开通”也不会让一批任务在“检测中”卡死。状态值用TINYINT而不是VARCHAR原因有两个。第一是节省空间一千万行数据能差出接近200MB第二是枚举值可控代码里StatusEnum和数字一一对应不会出现“已开通”和“开通成功”这种脏状态文本。下面这条建表语句可以直接用于MySQL 5.7及以上版本注意索引要覆盖查询路径CREATE TABLE phone_check_task ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, batch_id VARCHAR(32) NOT NULL COMMENT 批次号一次导入生成一个, phone VARCHAR(15) NOT NULL COMMENT 手机号清洗后存储, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待检测 1已开通 2未开通 3无效号码 4通道异常 5检测中, fail_count TINYINT NOT NULL DEFAULT 0 COMMENT 异常重试次数, check_msg VARCHAR(255) DEFAULT NULL COMMENT 检测结果附加信息, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_batch_phone (batch_id, phone), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT手机号微信开通检测任务表;uk_batch_phone唯一索引是去重兜底Excel里同一个号码出现两次也只会入库一次idx_status是给重跑任务用的启动时SELECT ... WHERE status IN (0,4)扫出所有未完成或可重试的记录就能接着干活。这里没有把phone单独建唯一索引因为同一号码可能出现在两个不同批次里以“批次号手机号”做联合唯一更符合业务。2.2 批次表和任务表拆开为什么不能用一张表搞定单表也能跑但到几十万行以后问题就暴露了。如果批次信息文件名、导入时间、总数量直接塞在任务表每行里光是冗余就白费一大截存储而且查询批次统计要GROUP BY batch_id扫全表慢得没法看。常见的做法是拆两张表一张batch_import记录批次层面的元数据一张phone_check_task记录明细。要统计“这个批次完成多少”一条COUNT(*) ... WHERE batch_id ?就行。CREATE TABLE batch_import ( id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, batch_id VARCHAR(32) NOT NULL UNIQUE COMMENT 业务批次号, file_name VARCHAR(255) NOT NULL COMMENT 原始文件名, total_count INT NOT NULL DEFAULT 0 COMMENT 导入号码总数, processed_count INT NOT NULL DEFAULT 0 COMMENT 已检测完成数, status TINYINT NOT NULL DEFAULT 0 COMMENT 0导入中 1导入完成 2检测中 3检测完成 4异常终止, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT批量导入批次表;批次表里的processed_count是个冗余计数每完成一批更新一次不必实时同步。真正要严格一致的是“号码是否检测完”那是任务表的事。批次表的status只表示大阶段比如“导入中”和“检测中”用于前端展示进度。这个拆分有个额外好处程序中断后重启只需要看批次表status再结合任务表里status0/4的记录就知道哪些任务断了、从哪里续上。2.3 批量写入选型JDBC Batch还是MyBatis BatchExecutor引入框架之前先问一句整个链路里数据库写的瓶颈到底在哪。检测结果是分散返回的单个结果写一次肯定有性能问题但更大的瓶颈其实在导入那一关——一次性往库里塞几万行不允许每行一条INSERT。我自己试过两个方案按落地体验说结论小数据量单批5万以内用JDBC原生Batch就够了没必要为了这次导入专门搭一套MyBatis环境但如果你项目中本来就用了MyBatis Plus那用SqlSessionTemplate的ExecutorType.BATCH更省事不用改表结构。JDBC原生的批量插入代码核心只有三步关闭自动提交、攒批次、最后统一commit。这里给一个可以直接用的模板/** * 批量写入手机号任务记录 * param phoneList 清洗后的手机号列表 * param batchId 本次导入批次号 */ public void batchInsertTasks(ListString phoneList, String batchId) { String sql INSERT INTO phone_check_task (batch_id, phone, status) VALUES (?, ?, 0); try (Connection conn dataSource.getConnection()) { conn.setAutoCommit(false); // 手动管理事务 try (PreparedStatement ps conn.prepareStatement(sql)) { int count 0; for (String phone : phoneList) { ps.setString(1, batchId); ps.setString(2, phone); ps.addBatch(); if (count % 500 0) { // 每500条执行一次批次 ps.executeBatch(); ps.clearBatch(); } } ps.executeBatch(); // 收尾剩余批次 conn.commit(); } catch (SQLException e) { conn.rollback(); // 异常整体回滚 throw e; } } catch (SQLException e) { log.error(批量插入失败, e); } }500这个批次数不是拍脑袋定的。MySQL单条INSERT多个VALUES的耗时差不多但executeBatch攒得越大PreparedStatement内部的内存开销和网络往返延迟也线性上升。批量值太大会让驱动层的内存先顶不住太小又体现不出批处理优势。实战中500到1000是折中区间配合rewriteBatchedStatementstrue连接参数一次请求就能把500条VALUES全部拼进去比默认逐条提交快一个量级。3. 手机号批量导入的Java实现POI读取、清洗去重与高效入库导入环节是最表面、却最容易埋雷的一步。很多系统上线第一天就挂在“Excel里到底有什么”这件事上手机号带着全角字符、Excel单元格是文本型数字、行列里有隐藏的空格和换行符。本章按读取、清洗、入库三步走每一步都给出可抄的代码和参数不会只讲抽象概念。3.1 Excel读取的两种方式用户模式遇到大文件时怎么办Apache POI读xlsx有两种典型方式用户模式和事件模式。用户模式就是WorkbookFactory.create(InputStream)把整个工作簿一次性载入内存代码好写、逻辑直观适合两三千行的表格。但如果客户的Excel有五六万行、几十列再叠加样式和批注内存占用能冲到几百兆表现就是导入界面转圈半天然后直接OutOfMemoryError。事件模式是另一条路xlsx本质是zip包里面每个sheet对应一段XML文本POI的XSSFReader可以按Sheet读取XML流并进行流式解析。代价是代码量明显变大而且拿不到Excel的格式化样式适合“只取原始值、不管格式”的场景。我的判断标准是单文件超过2万行或导入频率很高的直接上事件模式偶尔导入一次小文件用户模式更快。import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.xssf.eventusermodel.XSSFReader; import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler; import org.apache.poi.xssf.eventusermodel.XSSFSheetXMLHandler.SheetContentsHandler; // 流式读取Excel单元格按行回调 try (OPCPackage pkg OPCPackage.open(file.getAbsolutePath())) { XSSFReader reader new XSSFReader(pkg); XSSFReader.SheetIterator sheets (XSSFReader.SheetIterator) reader.getSheetsData(); while (sheets.hasNext()) { try (InputStream sheetStream sheets.next()) { // 按行解析handler中拿到每行每个单元格的字符串值 XSSFSheetXMLHandler handler new XSSFSheetXMLHandler( reader.getStylesTable(), null, new SimpleSheetHandler(), false); InputSource source new InputSource(sheetStream); XMLHelper.newXMLReader().setContentHandler(handler); // 实际解析动作由XMLReader完成handler只接收回调 XMLHelper.newXMLReader().parse(source); } } }这个例子做了删减真实项目中handler里要处理“当前行号、单元格坐标、单元格值”三个回调。事件模式最大的坑是拿到的值是去格式化的原始字符串电话号码如果是1380 1234567这种带空格的文本会在这一层原样传出来——所以清洗逻辑必须接在handler里一边读一边清洗一边塞队列不要读完整张表再处理。3.2 手机号清洗规则全角数字、隐藏字符和号段校验一次做完写清洗代码的第一原则不要相信Excel里的任何字符串。我见过最离谱的“手机号”是 里面既有全角数字又有全角空格。清洗要做三件事统一转半角、剔除不可见字符、正则校验格式。private static final Pattern PHONE_PATTERN Pattern.compile(^1[3-9]\\d{9}$); /** 清洗并校验手机号非法返回null */ public static String cleanPhone(String raw) { if (raw null) return null; // 1. 全角数字(-)转半角ASCII差值0xFEE0 StringBuilder sb new StringBuilder(raw.length()); for (char c : raw.toCharArray()) { if (c 0xFF10 c 0xFF19) { c (char) (c - 0xFEE0); } sb.append(c); } // 2. 去掉空格、全角空格、零宽字符、BOM String s sb.toString() .replaceAll([\\s\\u3000], ) .replaceAll([\\u200B-\\u200D\\uFEFF\\u00A0], ); // 3. 正则校验 if (!PHONE_PATTERN.matcher(s).matches()) { return null; } return s; }正则^1[3-9]\d{9}$看起来宽松但它把“13000000000”到“19999999999”的范围全部放行符合当前号段分配现状。真正要防的是“8613800001234”这种带国家码的和“12345”这种短号前者在Excel里经常出现后者没意义。清洗完的结果存库前还可以做一次内存去重用HashSet承接几十万级别的量完全扛得住超过百万再考虑用Redis的SADD去重内存去重会撞上JVM堆上限。3.3 导入入库的线程模型与参数批大小、队列水位、事务边界读取、清洗、入库三个阶段如果串行跑瓶颈一定是数据库写入Excel解析在等写入空闲。常见做法是用生产者-消费者线程模型主线程读Excel清洗后的号码放进有界队列单独一条写线程消费队列做批量入库。下面这段是生产环境的简版骨架重点是队列水位和批大小配合Executors.newFixedThreadPool(1); // 单条写线程避免并发写锁竞争 BlockingQueueString queue new LinkedBlockingQueue(10000); // 有界队列防止内存暴涨 AtomicInteger totalInserted new AtomicInteger(0); // 消费线程持续从队列取数据并批量入库 Runnable dbWriter () - { ListString buffer new ArrayList(500); while (true) { try { String phone queue.poll(200, TimeUnit.MILLISECONDS); if (phone ! null) { buffer.add(phone); } if (buffer.size() 500 || (phone null !buffer.isEmpty())) { batchInsertTasks(buffer, batchId); // 复用2.3的批量插入方法 totalInserted.addAndGet(buffer.size()); buffer.clear(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } };队列容量设10000是因为缓冲太大没意义——消费端每批只提交500条队列里的数据超过500就是给消费线程兜底用的防止Excel解析线程短暂卡顿把队列打空。而poll(200ms)的超时时间是为了处理“解析结束但剩余数据还在队列里”的情况队列空了不立刻退出等200ms确认没有新数据再收尾。事务边界要和batchInsertTasks保持一致每500条是一个事务而不是“所有任务一个事务”——一个大事务跑半小时中途任何一条SQL异常都要全部回滚代价太大。4. 检测是否开通微信的核心逻辑通道抽象、并发控制与结果回写导入只是搬运工真正决定系统价值的是检测环节。怎么做到“几万个号码有序检测、结果准确落库”靠的不是某个神奇接口而是把通道、并发和回写三件事设计清楚。通道的选型会变、接口会换但事务边界和并发框架必须稳定。4.1 检测通道怎么抽象把检测逻辑从业务代码里剥离很多初版代码把检测逻辑直接写在Service层调一次接口写一个if/else。看似能用实际一换通道就得改业务代码还测不准。正确做法是先定义检测器的接口把“通道”做成可插拔实现业务只依赖接口/** 微信开通检测统一接口 */ public interface WechatRegisterDetector { /** * 检测手机号是否开通微信 * return code: 1已开通 2未开通 3无效号码 4通道异常 */ DetectResult check(String phone); } /** 检测结果封装 */ public class DetectResult { private final int code; private final String msg; // 构造函数和getter省略 }接口稳定实现随便换。有的通道适合大风量并发有的通道要求恒定低速业务层完全不感知。真正干活时我会把通道实现再包一层“路由”逻辑比如第一个通道连续失败3次自动切换到备用通道切换过程中在日志里记录通道健康度方便后续调参。这一层抽象同时也让代码可测——单元测试只需要mock一个WechatRegisterDetector返回预设结果不需要真的去请求外部服务。4.2 并发度控制线程池和信号量的关系通道大多有QPS上限并发太高触发拦截并发太低浪费资源。很多人上来就直接newFixedThreadPool(20)把20条线程全开出去结果通道限流一波全回来线程池满了后面的任务全部排队等待整体吞吐更低。正确的思路是把并发拆成“线程池大小”和“信号量许可数”两层来控制。// 核心参数线程池大小10信号量许可20每线程检测间隔150ms ExecutorService detectPool Executors.newFixedThreadPool(10); Semaphore semaphore new Semaphore(20); // 提交任务时同时申请信号量 for (PhoneTask task : taskList) { semaphore.acquire(); // 控制全局并发不超过20 detectPool.submit(() - { try { DetectResult result detector.check(task.getPhone()); // 处理结果并回写见4.3 } finally { semaphore.release(); } }); }线程池大小控制“同时占用多少CPU和DB连接”信号量控制“同时在途的检测请求数”。两个参数配合效果就是线程池再满活跃请求也不会超过信号量许可数。实际调参经验先找到通道能稳定接受的QPS比如通道限速每秒10次那就把信号量许可数设为10 × 平均响应时间平均响应时间0.5秒许可数取5再加余量到8线程池保持松散即可。这是工程估算上线后按日志里的通道超时率回调。4.3 结果回写状态更新别用大事务重试要单独走检测结果回来以后最容易出现的错误是“每检测一条就UPDATE一次”十万条产生十万次数据库往返。正确做法是在消费端攒批按结果分组一次性更新。SQL只更新状态和失败次数两个字段把check_msg放到不常读的扩展表避免大文本拖慢主表更新。// 按结果分组批量更新每组最多500条 String sql UPDATE phone_check_task SET status ?, fail_count ?, update_time NOW() WHERE id ?; try (Connection conn dataSource.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { for (DetectResultWithId item : batch) { ps.setInt(1, item.getCode()); // 直接映射为status ps.setInt(2, item.getFailCount()); ps.setLong(3, item.getTaskId()); ps.addBatch(); } ps.executeBatch(); conn.commit(); } }fail_count只在通道异常时加1已开通未开通保持不变。重试逻辑不能放在检测线程里否则一个异常号码会占住线程反复重试其他号码全在后面等。我会单独维护一个“重试队列”把status4的号码按fail_count从小到大捞出来重新丢回检测线程池。连续失败3次就放弃标记为通道异常交给人工排查。这个设计还有一个隐藏的好处程序崩了以后重启只需要找回所有status IN (0,4)的记录重新入队不会重复检测已出结果的号码。5. 避坑指南批量导入和检测环节的5个典型翻车现场批量检测系统看着简单跑起来全是细节。下面这几条都是实际项目里踩过的坑每条按现象、原因、解决三个层面说清楚希望能帮你少走弯路。5.1 现象POI解析xlsx报Zip bomb异常或ZipSecureFile相关错误导入一个8MB的Excel启动解析没几秒直接抛Zip bomb detected相关异常。原因是POI读取xlsx时要解zip而xlsx内部XML有大量冗余解压率超高POI默认防zip炸弹机制误判为恶意文件。处理方式是显式放宽限制ZipSecureFile.setMinInflateRatio(0.01)把最小压缩比从默认的0.01调得更低或setMaxEntrySize(1_000_000_000L)放大单个压缩入口上限。参数调整要按文件实际大小来不能无脑放开否则内存会被恶意构造的压缩包打爆。5.2 现象导入后号码库全是脏数据清洗规则没兜住一个客户发来的Excel里手机号单元格被自动显示成1.38001E10还有一批前面带了不可见的零宽字符\u200B。导入后正则校验全挂最终有效号码只剩三成。原因是Excel数字格式自动转科学计数法以及从网页复制时带入隐藏字符。解决分两层在Excel解析层把单元格显式按字符串读取避免POI把数字自动转double在清洗层对所有隐藏字符做白名单过滤只保留半角数字和号其余一律清除。清洗规则要打在入库前入库后补数据成本翻倍。5.3 现象检测线程池跑满数据库连接池也跟着耗尽检测任务提交时不做并发控制把线程池设成了50结果通道响应慢50个线程全在等待数据库连接池8个连接被打满结果回写全部排队最后系统假死。原因是线程池并发度和数据库连接数、通道QPS三者没有统一调度。解决方法是引入信号量做全局限流同时在回写处设一个独立的固定连接池和业务连接分离保证即使检测线程全卡住回写也不会被拖死。这两处参数要联动调不能只调一个。5.4 现象批量插入5000条就报锁等待超时MySQL主从延迟飙升导入时把批大小设成5000executeBatch一次提交几万行结果MySQL锁等待超时主从同步延迟冲到十几秒。原因是单事务太大行锁范围被撑大加上rewriteBatchedStatements拼接的SQL过长超过max_allowed_packet的安全值。解决方法是把事务内的批量控制在500条以内同时把连接参数的rewriteBatchedStatementstrue打开两者搭配效果最好。如果单表超过千万行考虑用定时任务分批导入而不是单次扛下全部数据。5.5 现象程序重启后刚导入的任务全丢了应用重启再查数据库发现一批号码状态仍是待检测但日志显示已经检测到一半了。原因是检测结果只在内存里攒批还没来得及批量回写进程就挂了数据自然丢。解决办法是把“已检测”状态及时持久化不要攒500条才写一次——攒到200条就提交一个小事务虽然写次数多了一些但重启后最多丢200条不会出现“半批消失”的情况。更稳妥的做法是给每个检测结果打上task_id回写失败时单独记录日志启动时读日志反查缺失项。6. 验证与交付让检测系统从“能跑”变成“可信”的测试方法系统写完不等于能用没有验证过准确率就大规模跑批次结果错了很难反查。我的习惯是上线前做三件事小批次冒烟、抽样比对、全量基线记录。先用一批结果已知的号码做冒烟测试比如20个号码里混入3个确定已注册微信的号跑完看这3个是否被标记为“已开通”。这一步验证的是通道配置和状态映射不是数量级。接下来把检测完成的号码随机抽100个导出到Excel手动抽查一遍状态是否和预期一致——重点看“通道异常”的号码有没有被误判成“未开通”这两种状态的处理逻辑完全不同。最后记录本次批次的基线数据总号码数、有效号码率、已开通占比、通道异常比例。下次跑新批次时拿基线对比如果“通道异常”比例从2%飙到15%说明检测通道出问题了或号码质量变差及时停下来排查而不是闷头跑完。数据库文件的交付也要顺手做好我给任务表建了idx_batch和idx_status两个索引数据库文件里已包含导入MySQL 8.0后直接执行表结构脚本即可。但要注意连接串里加上useUnicodetruecharacterEncodingutf8否则中文备注和号码里的特殊字符可能乱码。启动主流程前先改三个参数数据库连接数8调整为16检测并发场景需要更多连接、批量写入的批大小设为500、信号量许可数按通道QPS重算。改完这三个数再跑第一批基本不会翻车。这套批量检测方案的可复用价值在于状态机和并发框架而不是某个具体通道。做一次业务流程把状态、接口、重试机制沉淀成模板以后换任何检测源都能快速落地。我个人每次跑批次前的习惯是先跑一个20条的小批次做冒烟顺手把通道参数调准确认没问题再放全量。这个习惯帮我省掉了很多重新清洗数据的麻烦希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?