mockValueOps;
+ private RedisTemplateSimpleDistributedLock lock;
+
+ @BeforeMethod
+ @SuppressWarnings("unchecked")
+ public void setUp() {
+ mockRedisTemplate = Mockito.mock(StringRedisTemplate.class);
+ mockValueOps = Mockito.mock(ValueOperations.class);
+ Mockito.when(mockRedisTemplate.opsForValue()).thenReturn(mockValueOps);
+ lock = new RedisTemplateSimpleDistributedLock(mockRedisTemplate, "test_interrupt_lock", 60000);
+ }
+
+ /**
+ * 测试 lock() 在 Thread.sleep 被中断时应恢复线程中断标志
+ *
+ * 修复前:InterruptedException 被忽略(// Ignore),线程中断标志丢失
+ * 修复后:调用 Thread.currentThread().interrupt() 恢复中断标志
+ *
+ */
+ @Test(description = "lock() 方法在中断时应恢复线程中断标志")
+ public void testLockRestoresInterruptedFlagAfterSleepInterruption() throws InterruptedException {
+ AtomicBoolean interruptedFlagAfterLock = new AtomicBoolean(false);
+
+ // 第一次 setIfAbsent 返回 false(模拟锁被占用),第二次返回 true(模拟锁释放)
+ Mockito.when(mockValueOps.setIfAbsent(Mockito.anyString(), Mockito.anyString(),
+ Mockito.anyLong(), Mockito.any(TimeUnit.class)))
+ .thenReturn(false)
+ .thenReturn(true);
+ // get() 返回不同的值,确保不走可重入路径
+ Mockito.when(mockValueOps.get(Mockito.anyString())).thenReturn("other-value");
+
+ Thread testThread = new Thread(() -> {
+ // 设置中断标志
+ Thread.currentThread().interrupt();
+ // 调用 lock(),第一次 tryLock 失败,sleep 会因中断标志立即抛出 InterruptedException
+ lock.lock();
+ interruptedFlagAfterLock.set(Thread.currentThread().isInterrupted());
+ });
+
+ testThread.start();
+ testThread.join(5000);
+
+ // 线程应该已经完成(不会永远阻塞)
+ Assert.assertFalse(testThread.isAlive(), "线程应该已完成");
+ // 关键验证:中断标志应被恢复(而非被忽略丢失)
+ Assert.assertTrue(interruptedFlagAfterLock.get(), "lock()执行后线程中断标志应被恢复");
+ }
+
+ /**
+ * 测试 tryLock() 在 Redis 正常响应时的基本行为
+ */
+ @Test(description = "tryLock() 成功获取锁时应返回 true")
+ public void testTryLockSuccessfully() {
+ Mockito.when(mockValueOps.setIfAbsent(Mockito.anyString(), Mockito.anyString(),
+ Mockito.anyLong(), Mockito.any(TimeUnit.class)))
+ .thenReturn(true);
+
+ boolean result = lock.tryLock();
+
+ Assert.assertTrue(result, "应成功获取锁");
+ Assert.assertNotNull(lock.getLockSecretValue(), "锁值不应为null");
+ }
+
+ /**
+ * 测试 tryLock() 在锁已被其他线程持有时应返回 false
+ */
+ @Test(description = "锁被占用时 tryLock() 应返回 false")
+ public void testTryLockWhenLockHeld() {
+ Mockito.when(mockValueOps.setIfAbsent(Mockito.anyString(), Mockito.anyString(),
+ Mockito.anyLong(), Mockito.any(TimeUnit.class)))
+ .thenReturn(false);
+ Mockito.when(mockValueOps.get(Mockito.anyString())).thenReturn("other-lock-value");
+
+ boolean result = lock.tryLock();
+
+ Assert.assertFalse(result, "锁被占用时应返回false");
+ }
+}
diff --git a/weixin-java-cp/INTELLIGENT_ROBOT.md b/weixin-java-cp/INTELLIGENT_ROBOT.md
index dcd90e1a1a..18dd0c677f 100644
--- a/weixin-java-cp/INTELLIGENT_ROBOT.md
+++ b/weixin-java-cp/INTELLIGENT_ROBOT.md
@@ -109,6 +109,16 @@ String fromUser = message.getFromUserName(); // 发送用户
// ...
```
+对于智能机器人 API 模式的 JSON 回调消息,可使用 `WxCpIntelligentRobotMessage` 解析:
+
+```java
+WxCpIntelligentRobotMessage callbackMessage =
+ robotService.parseCallbackMessage(jsonBody);
+String botId = callbackMessage.getAiBotId();
+String userId = callbackMessage.getFrom().getUserid();
+String msgType = callbackMessage.getMsgType();
+```
+
### 删除智能机器人
```java
@@ -146,4 +156,4 @@ robotService.deleteRobot(robotId);
1. 需要确保企业微信应用具有智能机器人相关权限
2. 智能机器人功能可能需要特定的企业微信版本支持
3. 会话ID可以用于保持对话的连续性,提升用户体验
-4. 机器人状态: 0表示停用,1表示启用
\ No newline at end of file
+4. 机器人状态: 0表示停用,1表示启用
diff --git a/weixin-java-cp/pom.xml b/weixin-java-cp/pom.xml
index 4d5d172ed2..e789f7a73d 100644
--- a/weixin-java-cp/pom.xml
+++ b/weixin-java-cp/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.8.2.B
+ 4.8.4.B
weixin-java-cp
@@ -125,6 +125,15 @@
src/test/resources/testng.xml
+
+ --add-opens java.base/java.lang=ALL-UNNAMED
+ --add-opens java.base/java.lang.reflect=ALL-UNNAMED
+ --add-opens java.base/java.io=ALL-UNNAMED
+ --add-opens java.base/java.security=ALL-UNNAMED
+ --add-opens java.base/java.util=ALL-UNNAMED
+ --add-opens java.management/javax.management=ALL-UNNAMED
+ --add-opens java.naming/javax.naming=ALL-UNNAMED
+
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpIntelligentRobotService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpIntelligentRobotService.java
index bc5f3f1915..58f4373ceb 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpIntelligentRobotService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpIntelligentRobotService.java
@@ -74,4 +74,12 @@ public interface WxCpIntelligentRobotService {
*/
WxCpIntelligentRobotSendMessageResponse sendMessage(WxCpIntelligentRobotSendMessageRequest request) throws WxErrorException;
-}
\ No newline at end of file
+ /**
+ * 解析智能机器人 API 模式回调消息.
+ *
+ * @param callbackMessageJson 回调消息JSON
+ * @return 解析后的回调消息对象
+ */
+ WxCpIntelligentRobotMessage parseCallbackMessage(String callbackMessageJson);
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java
index f66acc0252..269a69a475 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java
@@ -57,6 +57,19 @@ public interface WxCpService extends WxService {
*/
String getAccessToken(boolean forceRefresh) throws WxErrorException;
+ /**
+ *
+ * 获取通讯录同步access_token,本方法线程安全
+ * 通讯录同步相关接口仅支持通过"通讯录同步secret"调用,需要使用独立的access_token
+ * 详情请见: https://developer.work.weixin.qq.com/document/path/91579
+ *
+ *
+ * @param forceRefresh 强制刷新
+ * @return 通讯录同步专用的access token
+ * @throws WxErrorException the wx error exception
+ */
+ String getContactAccessToken(boolean forceRefresh) throws WxErrorException;
+
/**
*
* 获取会话存档access_token,本方法线程安全
@@ -220,6 +233,32 @@ public interface WxCpService extends WxService {
*/
String postForMsgAudit(String url, String postData) throws WxErrorException;
+ /**
+ *
+ * 使用通讯录同步access token发起get请求
+ * 通讯录同步相关API需要使用通讯录同步专用的secret获取独立的access token
+ *
+ *
+ * @param url 接口地址
+ * @param queryParam 请求参数
+ * @return the string
+ * @throws WxErrorException the wx error exception
+ */
+ String getForContact(String url, String queryParam) throws WxErrorException;
+
+ /**
+ *
+ * 使用通讯录同步access token发起post请求
+ * 通讯录同步相关API需要使用通讯录同步专用的secret获取独立的access token
+ *
+ *
+ * @param url 接口地址
+ * @param postData 请求body字符串
+ * @return the string
+ * @throws WxErrorException the wx error exception
+ */
+ String postForContact(String url, String postData) throws WxErrorException;
+
/**
*
* Service没有实现某个API的时候,可以用这个,
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
index 7c72cb9a8c..a3ec703ca4 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
@@ -313,6 +313,29 @@ public String postForMsgAudit(String url, String postData) throws WxErrorExcepti
return this.executeNormal(SimplePostRequestExecutor.create(this), urlWithToken, postData);
}
+ @Override
+ public String getForContact(String url, String queryParam) throws WxErrorException {
+ // 获取通讯录同步专用的access token
+ String contactAccessToken = getContactAccessToken(false);
+ // 拼接access_token参数
+ String urlWithToken = url + (url.contains("?") ? "&" : "?") + "access_token=" + contactAccessToken;
+ if (queryParam != null && !queryParam.isEmpty()) {
+ urlWithToken = urlWithToken + "&" + queryParam;
+ }
+ // 使用executeNormal方法,不自动添加token
+ return this.executeNormal(SimpleGetRequestExecutor.create(this), urlWithToken, null);
+ }
+
+ @Override
+ public String postForContact(String url, String postData) throws WxErrorException {
+ // 获取通讯录同步专用的access token
+ String contactAccessToken = getContactAccessToken(false);
+ // 拼接access_token参数
+ String urlWithToken = url + (url.contains("?") ? "&" : "?") + "access_token=" + contactAccessToken;
+ // 使用executeNormal方法,不自动添加token
+ return this.executeNormal(SimplePostRequestExecutor.create(this), urlWithToken, postData);
+ }
+
/**
* 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求.
*/
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImpl.java
index 8a12fa4ff4..aba1ee85c4 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImpl.java
@@ -67,4 +67,9 @@ public WxCpIntelligentRobotSendMessageResponse sendMessage(WxCpIntelligentRobotS
return WxCpIntelligentRobotSendMessageResponse.fromJson(responseText);
}
-}
\ No newline at end of file
+ @Override
+ public WxCpIntelligentRobotMessage parseCallbackMessage(String callbackMessageJson) {
+ return WxCpIntelligentRobotMessage.fromJson(callbackMessageJson);
+ }
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java
index ef78116e12..8285e59df2 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java
@@ -75,6 +75,51 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
return this.configStorage.getAccessToken();
}
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+
+ Lock lock = this.configStorage.getContactAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+ // 使用通讯录同步secret获取access_token
+ String contactSecret = this.configStorage.getContactSecret();
+ if (contactSecret == null || contactSecret.trim().isEmpty()) {
+ throw new WxErrorException("通讯录同步secret未配置");
+ }
+ String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+ this.configStorage.getCorpId(), contactSecret);
+
+ try {
+ HttpGet httpGet = new HttpGet(url);
+ if (this.httpProxy != null) {
+ RequestConfig config = RequestConfig.custom()
+ .setProxy(this.httpProxy).build();
+ httpGet.setConfig(config);
+ }
+ String resultContent = getRequestHttpClient().execute(httpGet, ApacheBasicResponseHandler.INSTANCE);
+ WxError error = WxError.fromJson(resultContent, WxType.CP);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+
+ WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+ this.configStorage.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+ } catch (IOException e) {
+ throw new WxRuntimeException(e);
+ }
+ } finally {
+ lock.unlock();
+ }
+ return this.configStorage.getContactAccessToken();
+ }
+
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java
index 3ca041e7ec..129823df5a 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java
@@ -76,6 +76,51 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
return this.configStorage.getAccessToken();
}
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+
+ Lock lock = this.configStorage.getContactAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+ // 使用通讯录同步secret获取access_token
+ String contactSecret = this.configStorage.getContactSecret();
+ if (contactSecret == null || contactSecret.trim().isEmpty()) {
+ throw new WxErrorException("通讯录同步secret未配置");
+ }
+ String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+ this.configStorage.getCorpId(), contactSecret);
+
+ try {
+ HttpGet httpGet = new HttpGet(url);
+ if (this.httpProxy != null) {
+ RequestConfig config = RequestConfig.custom()
+ .setProxy(this.httpProxy).build();
+ httpGet.setConfig(config);
+ }
+ String resultContent = getRequestHttpClient().execute(httpGet, BasicResponseHandler.INSTANCE);
+ WxError error = WxError.fromJson(resultContent, WxType.CP);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+
+ WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+ this.configStorage.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+ } catch (IOException e) {
+ throw new WxRuntimeException(e);
+ }
+ } finally {
+ lock.unlock();
+ }
+ return this.configStorage.getContactAccessToken();
+ }
+
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java
index 7b651cbc08..69cc074be9 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java
@@ -70,6 +70,49 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
return configStorage.getAccessToken();
}
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ final WxCpConfigStorage configStorage = getWxCpConfigStorage();
+ if (!configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return configStorage.getContactAccessToken();
+ }
+ Lock lock = configStorage.getContactAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return configStorage.getContactAccessToken();
+ }
+ // 使用通讯录同步secret获取access_token
+ String contactSecret = configStorage.getContactSecret();
+ if (contactSecret == null || contactSecret.trim().isEmpty()) {
+ throw new WxErrorException("通讯录同步secret未配置");
+ }
+ String url = String.format(configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+ this.configStorage.getCorpId(), contactSecret);
+ try {
+ HttpGet httpGet = new HttpGet(url);
+ if (getRequestHttpProxy() != null) {
+ RequestConfig config = RequestConfig.custom().setProxy(getRequestHttpProxy()).build();
+ httpGet.setConfig(config);
+ }
+ String resultContent = getRequestHttpClient().execute(httpGet, ApacheBasicResponseHandler.INSTANCE);
+ WxError error = WxError.fromJson(resultContent, WxType.CP);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+
+ WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+ configStorage.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+ } catch (IOException e) {
+ throw new WxRuntimeException(e);
+ }
+ } finally {
+ lock.unlock();
+ }
+ return configStorage.getContactAccessToken();
+ }
+
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
final WxCpConfigStorage configStorage = getWxCpConfigStorage();
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java
index eba9315649..ef6d86c665 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java
@@ -65,6 +65,45 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
return this.configStorage.getAccessToken();
}
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+
+ Lock lock = this.configStorage.getContactAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+ // 使用通讯录同步secret获取access_token
+ String contactSecret = this.configStorage.getContactSecret();
+ if (contactSecret == null || contactSecret.trim().isEmpty()) {
+ throw new WxErrorException("通讯录同步secret未配置");
+ }
+ HttpRequest request = HttpRequest.get(String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+ this.configStorage.getCorpId(), contactSecret));
+ if (this.httpProxy != null) {
+ httpClient.useProxy(this.httpProxy);
+ }
+ request.withConnectionProvider(httpClient);
+ HttpResponse response = request.send();
+
+ String resultContent = response.bodyText();
+ WxError error = WxError.fromJson(resultContent, WxType.CP);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+ WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+ this.configStorage.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+ } finally {
+ lock.unlock();
+ }
+ return this.configStorage.getContactAccessToken();
+ }
+
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java
index ce77b37805..76847f1f93 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java
@@ -56,12 +56,15 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
this.configStorage.getCorpSecret()))
.get()
.build();
- String resultContent = null;
- try {
- Response response = client.newCall(request).execute();
+ String resultContent;
+ try (Response response = client.newCall(request).execute()) {
+ if (response.body() == null) {
+ throw new WxErrorException("请求access token失败:响应内容为空");
+ }
resultContent = response.body().string();
} catch (IOException e) {
log.error(e.getMessage(), e);
+ throw new WxErrorException(e);
}
WxError error = WxError.fromJson(resultContent, WxType.CP);
@@ -75,6 +78,55 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
return this.configStorage.getAccessToken();
}
+ @Override
+ public String getContactAccessToken(boolean forceRefresh) throws WxErrorException {
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+
+ Lock lock = this.configStorage.getContactAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getContactAccessToken();
+ }
+ // 使用通讯录同步secret获取access_token
+ String contactSecret = this.configStorage.getContactSecret();
+ if (contactSecret == null || contactSecret.trim().isEmpty()) {
+ throw new WxErrorException("通讯录同步secret未配置");
+ }
+ //得到httpClient
+ OkHttpClient client = getRequestHttpClient();
+ //请求的request
+ Request request = new Request.Builder()
+ .url(String.format(this.configStorage.getApiUrl(GET_TOKEN), this.configStorage.getCorpId(),
+ contactSecret))
+ .get()
+ .build();
+ String resultContent;
+ try (Response response = client.newCall(request).execute()) {
+ if (response.body() == null) {
+ throw new WxErrorException("请求通讯录同步access token失败:响应内容为空");
+ }
+ resultContent = response.body().string();
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ throw new WxErrorException(e);
+ }
+ WxError error = WxError.fromJson(resultContent, WxType.CP);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+ WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+ this.configStorage.updateContactAccessToken(accessToken.getAccessToken(),
+ accessToken.getExpiresIn());
+ } finally {
+ lock.unlock();
+ }
+ return this.configStorage.getContactAccessToken();
+ }
+
@Override
public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
@@ -101,11 +153,15 @@ public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorExcepti
msgAuditSecret))
.get()
.build();
- String resultContent = null;
+ String resultContent;
try (Response response = client.newCall(request).execute()) {
+ if (response.body() == null) {
+ throw new WxErrorException("请求会话存档access token失败:响应内容为空");
+ }
resultContent = response.body().string();
} catch (IOException e) {
log.error(e.getMessage(), e);
+ throw new WxErrorException(e);
}
WxError error = WxError.fromJson(resultContent, WxType.CP);
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldData.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldData.java
index 971e5958d1..bb4d3a60ae 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldData.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldData.java
@@ -5,10 +5,9 @@
import lombok.NoArgsConstructor;
import java.io.Serializable;
-import java.util.List;
/**
- * 人事助手-员工档案数据(单个员工).
+ * 人事助手-员工档案数据(单个字段).
*
* @author leejoker created on 2024-01-01
*/
@@ -18,19 +17,98 @@ public class WxCpHrEmployeeFieldData implements Serializable {
private static final long serialVersionUID = 4593693598671765396L;
/**
- * 员工userid.
+ * 字段ID.
*/
+ @SerializedName("fieldid")
+ private Integer fieldId;
+
+ /**
+ * 子字段索引.
+ */
+ @SerializedName("sub_idx")
+ private Integer subIdx;
+
+ /**
+ * 结果状态,1表示成功.
+ */
+ @SerializedName("result")
+ private Integer result;
+
+ /**
+ * 值类型:1-字符串,2-uint64,3-uint32,4-int64,5-mobile.
+ */
+ @SerializedName("value_type")
+ private Integer valueType;
+
+ /**
+ * 字符串值(value_type=1时使用).
+ */
+ @SerializedName("value_string")
+ private String valueString;
+
+ /**
+ * 无符号32位整数值(value_type=3时使用).
+ */
+ @SerializedName("value_uint32")
+ private Long valueUint32;
+
+ /**
+ * 有符号64位整数值(value_type=4时使用).
+ */
+ @SerializedName("value_int64")
+ private Long valueInt64;
+
+ /**
+ * 无符号64位整数值(value_type=2时使用).
+ */
+ @SerializedName("value_uint64")
+ private Long valueUint64;
+
+ /**
+ * 手机号值(value_type=5时使用).
+ */
+ @SerializedName("value_mobile")
+ private MobileValue valueMobile;
+
+ /**
+ * 手机号值.
+ */
+ @Data
+ @NoArgsConstructor
+ public static class MobileValue implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 国家代码.
+ */
+ @SerializedName("value_country_code")
+ private String valueCountryCode;
+
+ /**
+ * 手机号.
+ */
+ @SerializedName("value_mobile")
+ private String valueMobile;
+ }
+
+ /**
+ * 员工userid(兼容旧版本,实际API不返回此字段).
+ * @deprecated 此字段在API响应中不存在
+ */
+ @Deprecated
@SerializedName("userid")
private String userid;
/**
- * 字段数据列表.
+ * 字段数据列表(兼容旧版本,实际API不返回此字段).
+ * @deprecated 此字段在API响应中不存在
*/
+ @Deprecated
@SerializedName("field_list")
- private List fieldList;
+ private java.util.List fieldList;
/**
- * 字段数据项.
+ * 字段数据项(用于更新员工档案).
*/
@Data
@NoArgsConstructor
@@ -38,15 +116,21 @@ public static class FieldItem implements Serializable {
private static final long serialVersionUID = 1L;
/**
- * 字段key.
+ * 字段ID.
*/
- @SerializedName("field_key")
- private String fieldKey;
+ @SerializedName("fieldid")
+ private Integer fieldId;
/**
- * 字段值.
+ * 字段值对象(推荐使用,支持多种类型).
*/
@SerializedName("field_value")
private WxCpHrEmployeeFieldValue fieldValue;
+
+ /**
+ * 字符串值(简化用法,适用于文本类型字段).
+ */
+ @SerializedName("value_string")
+ private String valueString;
}
}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldDataResp.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldDataResp.java
index 07e286c2ef..f8e763c293 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldDataResp.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldDataResp.java
@@ -21,10 +21,28 @@ public class WxCpHrEmployeeFieldDataResp extends WxCpBaseResp {
private static final long serialVersionUID = 6593693598671765396L;
/**
- * 员工档案数据列表.
+ * 字段数据列表(API实际返回field_info).
*/
- @SerializedName("employee_field_list")
- private List employeeFieldList;
+ @SerializedName("field_info")
+ private List fieldInfoList;
+
+ /**
+ * 员工档案数据列表(兼容旧版本方法名).
+ * @deprecated 请使用 getFieldInfoList()
+ */
+ @Deprecated
+ public List getEmployeeFieldList() {
+ return this.fieldInfoList;
+ }
+
+ /**
+ * 员工档案数据列表(兼容旧版本方法名).
+ * @deprecated 请使用 setFieldInfoList()
+ */
+ @Deprecated
+ public void setEmployeeFieldList(List employeeFieldList) {
+ this.fieldInfoList = employeeFieldList;
+ }
/**
* From json wx cp hr employee field data resp.
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldInfo.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldInfo.java
index e355d8cc6a..3db3f2057d 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldInfo.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldInfo.java
@@ -18,30 +18,43 @@ public class WxCpHrEmployeeFieldInfo implements Serializable {
private static final long serialVersionUID = 2593693598671765396L;
/**
- * 字段key.
+ * 字段ID.
*/
- @SerializedName("field_key")
- private String fieldKey;
-
- /**
- * 字段英文名称.
- */
- @SerializedName("field_en_name")
- private String fieldEnName;
+ @SerializedName("fieldid")
+ private Integer fieldId;
/**
- * 字段中文名称.
+ * 字段名称.
*/
- @SerializedName("field_zh_name")
- private String fieldZhName;
+ @SerializedName("field_name")
+ private String fieldName;
/**
* 字段类型.
- * 具体取值参见 {@link WxCpHrFieldType}
+ * 1: 文本
+ * 2: 单选/多选
+ * 3: 日期
*/
@SerializedName("field_type")
private Integer fieldType;
+ /**
+ * 是否必填.
+ */
+ @SerializedName("is_must")
+ private Boolean isMust;
+
+ /**
+ * 值类型.
+ * 1: 字符串
+ * 2: uint64
+ * 3: uint32
+ * 4: int64
+ * 5: mobile
+ */
+ @SerializedName("value_type")
+ private Integer valueType;
+
/**
* 获取字段类型枚举.
*
@@ -52,22 +65,79 @@ public WxCpHrFieldType getFieldTypeEnum() {
}
/**
- * 是否系统字段.
- * 0: 否
- * 1: 是
+ * 选项列表(单选/多选字段专用).
+ */
+ @SerializedName("option_list")
+ private List