
简介一套基于Java的企业微信开放接口设计源码面向需要对接通讯录、消息推送等能力的企业后端开发者。资源包共37个文件以32个Java源文件为主体清晰实现认证鉴权、请求封装与回调处理等核心逻辑另含两个XML配置文件、一个YAML配置、一个说明文档及一个Git忽略文件覆盖运行配置与版本管理需求。项目采用标准Maven工程结构源码按功能模块分包构建与资源目录分层清晰可直接导入IDE开展二次开发也可作为学习企业微信API调用流程的范例。压缩包仅39KB体量轻巧、上手成本低适合已有Java基础并希望快速落地企业微信集成的中级开发者。资源虽小但覆盖企业微信开放接口的核心链路代码结构可复用便于在此基础上扩展消息推送、通讯录同步等场景。目前已有480人浏览学习可帮助减少接口联调中的常见弯路提升开发效率。1. 基于 Java 的企业微信 OpenAPI 源码先搞清楚它能帮你省下什么做内部系统开发的 Java 工程师十有八九都接过“给企业微信发通知”这种需求。短信要花钱App 装了没人看而企业微信的 OpenAPI 接口允许自建应用把消息、通讯录、文件直接推到员工手机上员工在微信里就能收到。这套源码就是围绕 Java 对接企业微信 OpenAPI 的完整工程它把 token 管理、回调验签、消息发送、通讯录同步这些重复劳动封装成了可直接复用的模块你拿到手改一下 corpid 和 secret 就能跑通。适合正在维护办公系统、想快速接入企业微信接口的 Java 工程师也适合刚接触 OpenAPI、对签名和回调机制一头雾水的开发。它不解决业务问题解决的是“接入那一层最磨人的基建”。2. 接入前的三个准备token 获取、请求封装与回调验签2.1 先打通 access_token企业微信所有接口的通行证企业微信的接口设计里几乎每个请求都要带 access_token。它由 corpid 和 corpsecret 两个参数向 gettoken 接口换回来有效期 7200 秒。最省事的写法是每个请求前都调一次 gettoken但实际这么干的人都被限流过。企业微信对 gettoken 的调用频率限制很严格一个 secret 一分钟几十次就报错所以正确的做法是缓存起来快过期时再刷新。我一般会用一个 TokenManager 负责这件事核心逻辑就是“缓存优先过期刷新”Component public class AccessTokenManager { private String cachedToken; private long expireAt 0L; Value(${wechat.corpId}) private String corpId; Value(${wechat.corpSecret}) private String corpSecret; private final RestTemplate restTemplate new RestTemplate(); public synchronized String getToken() { long now System.currentTimeMillis(); // 预留 200 秒余量避免 token 刚好在请求途中过期 if (cachedToken ! null now expireAt - 200_000) { return cachedToken; } String url String.format( https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid%scorpsecret%s, corpId, corpSecret ); MapString, Object resp restTemplate.getForObject(url, Map.class); // 企业微信正常返回时 errcode 为 0 if (resp ! null Integer.valueOf(0).equals(resp.get(errcode))) { cachedToken (String) resp.get(access_token); expireAt now Integer.parseInt(resp.get(expires_in).toString()) * 1000L; } else { throw new RuntimeException(获取token失败: resp); } return cachedToken; } }这段代码做了两件关键事第一是给 token 留了 200 秒的提前量防止你用旧 token 发请求时正好碰上服务端刷新第二是加了 synchronized避免多线程同时刷新导致 gettoken 被重复调用。反正我第一次写的时候没加锁生产环境一压测就看到一堆 42001 超时错误。2.2 封装统一请求接口把签名参数和超时逻辑收口有了 token 之后剩下的请求基本都是 GET 或者 POST 到qyapi.weixin.qq.com。直接裸写 RestTemplate 会有一个问题每个接口都要拼 access_token、手动处理 errcode、管超时。我习惯做一个WeComClient把 GET 和 POST 统一收口业务代码只传 url 和 body。Component public class WeComClient { private static final String BASE_URL https://qyapi.weixin.qq.com/cgi-bin; private final RestTemplate restTemplate; public WeComClient() { SimpleClientHttpRequestFactory factory new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(5_000); factory.setReadTimeout(10_000); this.restTemplate new RestTemplate(factory); } public MapString, Object get(String path, String token) { String url BASE_URL path ?access_token token; return restTemplate.getForObject(url, Map.class); } public MapString, Object post(String path, String token, Object body) { String url BASE_URL path ?access_token token; return restTemplate.postForObject(url, body, Map.class); } public boolean isOk(MapString, Object resp) { return resp ! null Integer.valueOf(0).equals(resp.get(errcode)); } }这个封装本身没有技术含量但值得注意两个细节连接超时设 5 秒、读取超时设 10 秒。企业微信接口在高峰期偶尔会慢太短的超时会让整个业务跟着抖动太长又会拖垮你的线程池。至于把所有接口的返回都先当 Map 处理是因为 OpenAPI 不同接口的返回结构差异很大先用 Map 接住再按业务字段去取比定义几十个 DTO 更省事。2.3 回调 URL 的验证第一次握手最容易出错的地方企业微信的自建应用要接收消息和事件必须在管理后台配置回调 URL。配置时它会发一个验证请求带上 msg_signature、timestamp、nonce 和 echostr 四个参数你的服务需要把 echostr 解密后原样返回。这一步可以说是接入 OpenAPI 时最容易翻车的环节十个人里有八个卡在这里。验证的核心是两件事签名校验和 AES 解密。签名规则是把 token、timestamp、nonce、echostr 里的 encrypt 字段拼起来按字典序排序然后做 SHA-1结果和 msg_signature 比对。解密的加密模式是 AES-256-CBC密钥是回调设置的 EncodingAESKey 做 MD5 之后得到。RestController RequestMapping(/api/callback) public class CallbackController { GetMapping(/verify) public String verify( RequestParam(msg_signature) String signature, RequestParam(timestamp) Long timestamp, RequestParam(nonce) String nonce, RequestParam(echostr) String echostr) throws Exception { String[] items {Config.token, String.valueOf(timestamp), nonce, echostr}; Arrays.sort(items); String joined String.join(, items); String sha1 DigestUtils.sha1Hex(joined); if (!sha1.equals(signature)) { return invalid signature; } // 注意这里是先解密 echostr再返回给企业微信 String decryptStr WeComCrypto.decrypt(echostr); return decryptStr; } }代码里最容易写错的是排序那一步。企业微信文档要求的拼接顺序是“token、timestamp、nonce、echostr”但实际编码前必须先把这四项按字典序 sort 一遍。我见过有人直接把原文顺序拼起来去算签名结果永远对不上还怀疑是 AES 密钥的问题。记住sort 在前join 在后顺序不能想当然。3. 把最常用的三个业务接口跑通通讯录、消息与媒体文件3.1 通讯录同步部门、成员、标签的三级结构企业微信的通讯录不是扁平的它有部门、成员、标签三层结构。用得最多的接口是“获取部门列表”和“获取成员详情”。比如你要做一个“按部门推送消息”的功能第一步就得先把部门树拉下来再逐层拿成员。我这边一般用一个结构体把部门树装起来public class DeptNode { private String id; // 部门 id private String name; private int parentId; // 父部门 id根部门为 1 private ListDeptNode children new ArrayList(); } // 拉取全部部门列表 public ListDeptNode fetchDeptTree(String token) { MapString, Object resp weComClient.get(/department/list, token); if (!weComClient.isOk(resp)) { throw new RuntimeException(拉取部门失败: resp); } ListMapString, Object rawList (ListMapString, Object) resp.get(department); MapString, DeptNode nodeMap new HashMap(); // 第一遍先把所有节点建立出来 for (MapString, Object item : rawList) { DeptNode node new DeptNode(); node.setId(item.get(id).toString()); node.setName((String) item.get(name)); node.setParentId((int) item.get(parentid)); nodeMap.put(node.getId(), node); } // 第二遍按 parentId 挂到父节点下 ListDeptNode roots new ArrayList(); for (DeptNode node : nodeMap.values()) { if (node.getParentId() 1) { roots.add(node); } else { DeptNode parent nodeMap.get(String.valueOf(node.getParentId())); if (parent ! null) { parent.getChildren().add(node); } } } return roots; }/department/list这个接口一次能返回全量部门不需要做分页注意它返回的字段有 id、name、parentid、order。真正让新人困惑的是部门 id 是字符串父部门 id 是 int两者的类型在 JSON 解析时经常不一致直接用 Jackson 的 Map 接就很稳不会因为类型转换报错。成员接口user/list则是按部门 id 拉一次最多拉 100 个成员实际业务里要写个循环去翻页。3.2 发应用消息文本、Markdown 与文件卡片消息推送是这套源码里最常用的模块。自建应用消息接口的路径是/message/send请求体是 JSON核心字段是 touser、msgtype、agentid 和具体消息体。写个最简单的文本消息public boolean sendTextMessage(String token, String toUser, String content, int agentId) { MapString, Object body new HashMap(); body.put(touser, toUser); body.put(msgtype, text); body.put(agentid, agentId); MapString, Object text new HashMap(); text.put(content, content); body.put(text, text); MapString, Object resp weComClient.post(/message/send, token, body); if (!weComClient.isOk(resp)) { log.error(消息发送失败: {}, resp); return false; } return true; }这里有个关键点touser 是字符串不是数组。企业微信的接口把它设计成用竖线分隔的字符串比如zhangsan|lisi|wangwu一次最多 1000 人。我之前照搬微信公众平台的数组写法结果直接报 40058 参数错误。Markdown 消息的 body 结构也类似把 text 换成 markdown 对象就行但它只支持企业微信客户端内展示微信里看不全格式。另外消息推送接口对文本长度限制是 2048 字节超出会被截断。实际场景里我见过有人拿这个接口做内部工单通知也有团队把 DeepSeek 这类服务的自动回复结果推到企业微信员工直接在聊天框里看到机器人的答复。这套消息能力可以说是整个 OpenAPI 里性价比最高的部分一通百通。3.3 上传临时素材图片与文件怎么发给用户消息接口只支持文本和 Markdown想发图片、文件或语音就得先走一遍“上传临时素材”接口拿到 media_id再在消息体里引用它。临时素材接口的路径是/media/upload用 POST 表单提交参数是 type 和 media。public String uploadMedia(String token, String type, String filePath) throws IOException { String url https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token token type type; HttpHeaders headers new HttpHeaders(); headers.setContentType(MediaType.MULTIPART_FORM_DATA); // 使用 Spring 的 Resource 包装文件 FileSystemResource fileResource new FileSystemResource(new File(filePath)); MultiValueMapString, Object form new LinkedMultiValueMap(); form.add(media, fileResource); HttpEntityMultiValueMapString, Object requestEntity new HttpEntity(form, headers); ResponseEntityMap response restTemplate.postForEntity(url, requestEntity, Map.class); MapString, Object resp response.getBody(); if (resp ! null Integer.valueOf(0).equals(resp.get(errcode))) { return (String) resp.get(media_id); } throw new RuntimeException(上传素材失败: resp); }然后发送图片消息时body 里的 image 对象长这样{ touser: zhangsan, msgtype: image, agentid: 1000002, image: { media_id: MEDIA_ID } }注意临时素材的有效期是 3 天且只能使用一次发完就失效。如果你要长期给用户发同一张图片得把它上传到“永久素材”接口路径是/media/add不过那个接口对文件大小有更严格的限制图片不能超过 2MB。4. 企业微信接口避坑指南我踩过的六个典型问题问题一gettoken 频繁报 45009 或直接拿不到 token现象服务一启动就疯狂打日志几百条 gettoken 失败提示“调用超过每日限额”。原因代码里每次请求都重新调 gettoken没有做缓存。解决按 2.1 节的方式做本地缓存缓存 key 用 corpsecret 区分不同应用刷新时加同步锁。问题二消息发送返回 60011 没有权限现象调/message/send返回“agentid 或 touser 没有权限”。原因corpid 和 corpsecret 匹配到的是另一个应用或者 touser 里的成员 ID 不在该应用可见范围内。解决去管理后台确认自建应用的“可见范围”配置把对应部门和成员加进去。这条最常见也说不上 bug就是配置和代码对不上。问题三回调 URL 验证永远失败现象后台点击保存一直提示“URL 不通过”日志显示 msg_signature 比对不相等。原因拼接待签名字符串前没有做 sort或者解密 echostr 时用了错误的 AES Key。解决把校验流程拆成两步先只打印签名计算过程确认 SHA-1 结果和服务端的一致再处理解密。我当时的血泪经验是不要一上来就写完整实现先写个只校验签名的临时接口能通过再往下走。问题四发送消息报 40058 参数不合法现象请求体看起来没问题但接口返回“参数不完整或参数值类型不正确”。原因touser 传成了 JSON 数组或者文本消息的 content 超过 2048 字节。解决touser 必须用zhangsan|lisi这种竖线字符串消息体严格按文档字段名拼不能多不能少。问题五上传的图片在客户端打不开现象发送图片消息成功但员工点开是黑屏或提示文件已过期。原因media_id 来自临时素材接口而 3 天有效期已过或者被其他消息重复消耗。解决对 media_id 做持久化映射记录上传时间和使用次数如果业务上要长期重复发送改用/media/add永久素材接口。问题六内网环境请求企业微信接口超时无响应现象接口偶发超时重跑一次又成功日志里全是 connect timeout。原因服务器出网链路不稳定且没有设置合理的读取超时时间。解决按照 2.2 节设置连接超时 5 秒、读取超时 10 秒超时后最多重试一次重试时换一个不同的 token 缓存键避免重试还是打到同一个网络节点。5. 进阶玩法把回调加解密做成切面一处接入全局可用回调接口写好后你会发现一个问题每个回调入口都要做签名校验、AES 解密、时间戳防重这些逻辑如果散落在每个 Controller 里代码会非常臃肿。我更习惯用 Spring AOP 把这个过程收敛起来做一个注解WeComCallback被它标记的方法只需要关注解密后的业务数据其他事全交给切面。Aspect Component public class WeComCallbackAspect { Around(annotation(com.example.wecom.annotation.WeComCallback)) public Object handleCallback(ProceedingJoinPoint pjp) throws Throwable { // 1. 从请求中取出参数 HttpServletRequest request getRequest(); String signature request.getParameter(msg_signature); String timestamp request.getParameter(timestamp); String nonce request.getParameter(nonce); // 2. 读取 body 里的密文 encrypt 字段 String body StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8); MapString, Object bodyMap objectMapper.readValue(body, Map.class); String encrypt (String) bodyMap.get(encrypt); // 3. 校验签名 String[] items {Config.token, timestamp, nonce, encrypt}; Arrays.sort(items); String sha1 DigestUtils.sha1Hex(String.join(, items)); if (!sha1.equals(signature)) { return invalid signature; } // 4. 解密得到明文 XML String decryptXml WeComCrypto.decrypt(encrypt); MapString, Object bizData XmlUtils.parseXmlToMap(decryptXml); // 5. 把解析后的业务数据放在 request attribute 里供业务方法使用 request.setAttribute(wecomBizData, bizData); return pjp.proceed(); } }这个切面的好处是新接一个回调事件时你只需要定义一个新方法加一行注解业务逻辑里直接从 request 取值就行。而且签名校验的代码只维护一份不会出现不同回调类里面写的排序方式不一致的荒诞情况。至于验证方法我习惯用企业微信自带的回调调试工具它会主动发起一个带 encrypt 参数的请求你只需要在日志里观察切面打印的 decryptXml 是否拿到中文业务数据。从那以后我每次对接新回调 URL 都强制走一遍这个流程先看签名是否通过再看解密日志里有没有明文确认没问题才把管理后台的保存按钮点下去。这套习惯帮我避过了大部分回调相关的线上事故希望帮到你。本文还有配套的精品资源点击获取