diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..d7a5b96b34 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +## Review guidelines + +- 重点检查空指针、并发、资源释放、兼容性问题。 +- 不要只做代码风格建议,优先指出真实 bug 和回归风险。 +- 中文回复 review 结论。 diff --git a/README.md b/README.md index 94c52d7e07..026529a1c8 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,6 @@ ### 特别赞助
- - -
- - ccflow - -
@@ -120,6 +113,44 @@ ### Maven 引用方式 注意:最新版本(包括测试版)为 [![Maven Central](https://img.shields.io/maven-central/v/com.github.binarywang/wx-java.svg)](https://central.sonatype.com/artifact/com.github.binarywang/wx-java/versions),以下为最新正式版。 +#### 方式一:使用 BOM 统一管理版本(推荐) + +如果同时使用多个 WxJava 模块,推荐通过 BOM 统一管理版本,无需为每个模块单独指定版本号。 +`wx-java-bom` 从 **4.8.3.B** 版本开始提供,请使用该版本或更高版本: + +```xml + + 4.8.3.B + + + + + + com.github.binarywang + wx-java-bom + ${wx-java.version} + pom + import + + + +``` + +之后直接引入所需模块,无需指定版本: + +```xml + + com.github.binarywang + weixin-java-mp + + + com.github.binarywang + weixin-java-pay + +``` + +#### 方式二:直接引用单个模块 + ```xml com.github.binarywang diff --git a/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md b/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md index b64e4612b9..83d9c08158 100644 --- a/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md +++ b/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md @@ -196,19 +196,22 @@ msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, data -> { 1. **获取SDK时**:引用计数 +1 2. **使用完成后**:引用计数 -1 -3. **计数归零时**:SDK被自动释放 +3. **计数归零且SDK已过期时**:SDK被销毁并清理缓存 +4. **计数归零但SDK未过期时**:保留缓存,供后续调用直接复用 + +> **注意**:引用计数归零并不等同于立即销毁SDK。只有在 SDK 已超过有效期的情况下,框架才会调用 `Finance.DestroySdk()` 释放资源。这一机制避免了每次 API 调用后的频繁初始化/销毁循环。 ```java // 框架内部实现(简化版) public void downloadMediaFile(String sdkFileId, ...) { - long sdk = initSdk(); // 获取或初始化SDK + long sdk = initSdk(); // 获取或初始化SDK(有效期内直接复用缓存) configStorage.incrementMsgAuditSdkRefCount(sdk); // 引用计数 +1 - + try { // 执行实际操作 getMediaFile(sdk, sdkFileId, ...); } finally { - // 确保引用计数一定会减少 + // 确保引用计数一定会减少;仅在归零且过期时销毁 configStorage.decrementMsgAuditSdkRefCount(sdk); // 引用计数 -1 } } @@ -216,13 +219,13 @@ public void downloadMediaFile(String sdkFileId, ...) { ### SDK缓存机制 -SDK初始化后会缓存7200秒(企业微信官方文档规定),避免频繁初始化: +SDK初始化后会缓存7200秒,避免频繁初始化: - **首次调用**:初始化新的SDK -- **7200秒内**:复用缓存的SDK -- **超过7200秒**:重新初始化SDK +- **7200秒内**:复用缓存的SDK(即使引用计数曾归零也不重新初始化) +- **超过7200秒**:下次 `acquireMsgAuditSdk()` 返回0,触发重新初始化,旧SDK在重新初始化时被销毁 -新API的引用计数机制与缓存机制完美配合,确保SDK不会被提前销毁。 +新API的引用计数机制与缓存机制完美配合,确保SDK不会被提前销毁,也不会永久残留。 ## 迁移指南 diff --git a/docs/CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md b/docs/CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md index 072ceefd0c..1f073905f4 100644 --- a/docs/CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md +++ b/docs/CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md @@ -5,7 +5,7 @@ 当前实现(4.8.x)通过"共享SDK + 引用计数 + 7200秒过期"来管理会话存档SDK生命周期。 该方案存在以下核心问题: -1. **频繁初始化/销毁**:每次调用 `releaseSdk()` 后引用计数归零即销毁SDK。对于"拉取→解密→下载媒体"这类典型串行调用链,每步操作都会触发重新初始化。 +1. ~~**频繁初始化/销毁**:每次调用 `releaseSdk()` 后引用计数归零即销毁SDK。对于"拉取→解密→下载媒体"这类典型串行调用链,每步操作都会触发重新初始化。~~ ✅ 已在 4.8.3.B+ 修复:引用计数归零时,仅在 SDK 已过期的情况下才销毁,有效期内继续复用缓存。 2. **7200秒过期规则无依据**:官方文档FAQ明确说"不需要每次new/init sdk,可以在多次拉取中复用同一个sdk",无任何7200秒过期说明。 3. **线程安全问题**:企微技术人员建议"一个线程一个SDK实例",当前设计多线程共享同一SDK实例,存在并发安全隐患。 diff --git a/pom.xml b/pom.xml index f3be565062..ea7cf980e0 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.github.binarywang wx-java - 4.8.2.B + 4.8.4.B pom WxJava - Weixin/Wechat Java SDK 微信开发Java SDK @@ -124,9 +124,11 @@ weixin-java-miniapp weixin-java-open weixin-java-qidian + weixin-java-aispeech weixin-java-channel spring-boot-starters solon-plugins + wx-java-bom @@ -220,13 +222,14 @@ import - + joda-time joda-time 2.10.6 - test + + ch.qos.logback logback-classic diff --git a/solon-plugins/pom.xml b/solon-plugins/pom.xml index 9a375a60cc..620193dd4f 100644 --- a/solon-plugins/pom.xml +++ b/solon-plugins/pom.xml @@ -6,7 +6,7 @@ com.github.binarywang wx-java - 4.8.2.B + 4.8.4.B pom wx-java-solon-plugins diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml index 908e3957ee..682f1531ed 100644 --- a/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml @@ -5,7 +5,7 @@ wx-java-solon-plugins com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/solon-plugins/wx-java-channel-solon-plugin/pom.xml b/solon-plugins/wx-java-channel-solon-plugin/pom.xml index 1e3f457cfe..1e002c1e72 100644 --- a/solon-plugins/wx-java-channel-solon-plugin/pom.xml +++ b/solon-plugins/wx-java-channel-solon-plugin/pom.xml @@ -3,7 +3,7 @@ wx-java-solon-plugins com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/README.md b/solon-plugins/wx-java-cp-multi-solon-plugin/README.md index 97bcf0723f..8eb467f98f 100644 --- a/solon-plugins/wx-java-cp-multi-solon-plugin/README.md +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/README.md @@ -6,6 +6,25 @@ - 未实现 WxCpTpService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。 - 未实现 WxCpCgService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。 +## 关于 corp-secret 的说明 + +企业微信中不同功能模块对应不同的 `corp-secret`,每种 Secret 只对对应模块的接口具有调用权限: + +| Secret 类型 | 获取位置 | 可调用的接口 | 是否需要 agent-id | +|---|---|---|---| +| 自建应用 Secret | 应用管理 → 自建应用 → 选择应用 → 查看 Secret | 该应用有权限的接口 | **必填** | +| 通讯录同步 Secret | 管理工具 → 通讯录同步 → 查看 Secret | 部门/成员增删改查等通讯录接口 | **不填** | +| 客户联系 Secret | 客户联系 → API → Secret | 客户联系相关接口 | 不填 | + +> **常见问题**: +> - 使用自建应用 Secret + agent-id 可以获取部门列表,但**无法更新部门**(因为写接口需要通讯录同步权限) +> - 使用通讯录同步 Secret 可以同步部门,但**调用某些需要 agent-id 的应用接口会报错** + +如需同时使用多种权限范围,可在 `wx.cp.corps` 下配置多个条目,每个条目使用对应权限的 Secret,通过不同的 `tenantId` 区分后使用。 + +> **注意**: +> 当前插件实现会校验同一 `corp-id` 下的 `agent-id` **必须唯一**,并且 **只能有一个条目不填写 `agent-id`**。 +> 如果在同一 `corp-id` 下同时配置多个未填写 `agent-id` 的条目,会因 token/ticket 缓存 key 冲突而在启动时直接抛异常。 ## 快速开始 1. 引入依赖 @@ -18,25 +37,21 @@ ``` 2. 添加配置(app.properties) ```properties - # 应用 1 配置 - wx.cp.corps.tenantId1.corp-id = @corp-id - wx.cp.corps.tenantId1.corp-secret = @corp-secret + # 自建应用 1 配置(使用自建应用 Secret,需填写 agent-id) + wx.cp.corps.app1.corp-id = @corp-id + wx.cp.corps.app1.corp-secret = @自建应用的Secret(在"应用管理-自建应用"中查看) + wx.cp.corps.app1.agent-id = @自建应用的AgentId ## 选填 - wx.cp.corps.tenantId1.agent-id = @agent-id - wx.cp.corps.tenantId1.token = @token - wx.cp.corps.tenantId1.aes-key = @aes-key - wx.cp.corps.tenantId1.msg-audit-priKey = @msg-audit-priKey - wx.cp.corps.tenantId1.msg-audit-lib-path = @msg-audit-lib-path - - # 应用 2 配置 - wx.cp.corps.tenantId2.corp-id = @corp-id - wx.cp.corps.tenantId2.corp-secret = @corp-secret - ## 选填 - wx.cp.corps.tenantId2.agent-id = @agent-id - wx.cp.corps.tenantId2.token = @token - wx.cp.corps.tenantId2.aes-key = @aes-key - wx.cp.corps.tenantId2.msg-audit-priKey = @msg-audit-priKey - wx.cp.corps.tenantId2.msg-audit-lib-path = @msg-audit-lib-path + wx.cp.corps.app1.token = @token + wx.cp.corps.app1.aes-key = @aes-key + wx.cp.corps.app1.msg-audit-priKey = @msg-audit-priKey + wx.cp.corps.app1.msg-audit-lib-path = @msg-audit-lib-path + + # 通讯录同步配置(使用通讯录同步 Secret,不需要填写 agent-id) + # 此配置用于部门、成员的增删改查等通讯录管理操作 + wx.cp.corps.contact.corp-id = @corp-id + wx.cp.corps.contact.corp-secret = @通讯录同步的Secret(在"管理工具-通讯录同步"中查看) + ## agent-id 不填,通讯录同步不需要 agentId # 公共配置 ## ConfigStorage 配置(选填) @@ -59,8 +74,10 @@ ```java import com.binarywang.solon.wxjava.cp_multi.service.WxCpMultiServices; +import me.chanjar.weixin.cp.api.WxCpDepartmentService; import me.chanjar.weixin.cp.api.WxCpService; import me.chanjar.weixin.cp.api.WxCpUserService; +import me.chanjar.weixin.cp.bean.WxCpDepart; import org.noear.solon.annotation.Component; import org.noear.solon.annotation.Inject; @@ -70,27 +87,21 @@ public class DemoService { private WxCpMultiServices wxCpMultiServices; public void test() { - // 应用 1 的 WxCpService - WxCpService wxCpService1 = wxCpMultiServices.getWxCpService("tenantId1"); - WxCpUserService userService1 = wxCpService1.getUserService(); - userService1.getUserId("xxx"); - // todo ... - - // 应用 2 的 WxCpService - WxCpService wxCpService2 = wxCpMultiServices.getWxCpService("tenantId2"); - WxCpUserService userService2 = wxCpService2.getUserService(); - userService2.getUserId("xxx"); + // 使用自建应用的 WxCpService(对应 corp-secret 为自建应用 Secret) + WxCpService appService = wxCpMultiServices.getWxCpService("app1"); + WxCpUserService userService = appService.getUserService(); + userService.getUserId("xxx"); // todo ... - // 应用 3 的 WxCpService - WxCpService wxCpService3 = wxCpMultiServices.getWxCpService("tenantId3"); - // 判断是否为空 - if (wxCpService3 == null) { - // todo wxCpService3 为空,请先配置 tenantId3 企业微信应用参数 - return; - } - WxCpUserService userService3 = wxCpService3.getUserService(); - userService3.getUserId("xxx"); + // 使用通讯录同步的 WxCpService(对应 corp-secret 为通讯录同步 Secret) + // 通讯录同步 Secret 具有部门/成员增删改查等权限 + WxCpService contactService = wxCpMultiServices.getWxCpService("contact"); + WxCpDepartmentService departmentService = contactService.getDepartmentService(); + // 更新部门示例(WxCpDepart 包含 id、name、parentId 等字段) + WxCpDepart depart = new WxCpDepart(); + depart.setId(100L); + depart.setName("新部门名称"); + departmentService.update(depart); // todo ... } } diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml index c0d1dcc180..48b3fae27e 100644 --- a/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml @@ -4,7 +4,7 @@ wx-java-solon-plugins com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/AbstractWxCpConfiguration.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/AbstractWxCpConfiguration.java index ada4ac504c..25b4ab3747 100644 --- a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/AbstractWxCpConfiguration.java +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/AbstractWxCpConfiguration.java @@ -15,6 +15,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -37,6 +38,13 @@ protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiPrope /** * 校验同一个企业下,agentId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。 * + *

同一企业(corpId 相同)下可配置多个条目以使用不同的权限 Secret,例如:

+ *
    + *
  • 自建应用条目:填写应用对应的 corpSecret 和 agentId
  • + *
  • 通讯录同步条目:填写通讯录同步 Secret,agentId 可不填(null)
  • + *
+ *

但同一 corpId 下不允许出现重复的 agentId(包括多个 null)。

+ * * 查看 {@link me.chanjar.weixin.cp.config.impl.AbstractWxCpInRedisConfigImpl#setAgentId(Integer)} */ Collection corpList = corps.values(); @@ -49,8 +57,8 @@ protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiPrope String corpId = entry.getKey(); // 校验每个企业下,agentId 是否唯一 boolean multi = entry.getValue().stream() - // 通讯录没有 agentId,如果不判断是否为空,这里会报 NPE 异常 - .collect(Collectors.groupingBy(c -> c.getAgentId() == null ? 0 : c.getAgentId(), Collectors.counting())) + // 通讯录没有 agentId,使用字符串转换避免 null 与 agentId=0 冲突 + .collect(Collectors.groupingBy(c -> Objects.toString(c.getAgentId(), "null"), Collectors.counting())) .entrySet().stream().anyMatch(e -> e.getValue() > 1); if (multi) { throw new RuntimeException("请确保企业微信配置唯一性[" + corpId + "]"); diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpSingleProperties.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpSingleProperties.java index e761a09062..6f7f633c3f 100644 --- a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpSingleProperties.java +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpSingleProperties.java @@ -8,6 +8,16 @@ /** * 企业微信企业相关配置属性 * + *

企业微信中不同的 corpSecret 对应不同的权限范围,常见的有:

+ *
    + *
  • 自建应用 Secret:在"应用管理 - 自建应用"中查看,只能调用该应用有权限的接口
  • + *
  • 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,用于管理部门和成员(增删改查)
  • + *
  • 客户联系 Secret:在"客户联系"中查看,用于客户联系相关接口
  • + *
+ *

如需同时使用多种权限范围(例如:既要操作通讯录,又要调用自建应用接口), + * 可在 {@code wx.cp.corps} 下配置多个条目,每个条目使用对应权限的 {@code corpSecret}, + * 其中通讯录同步的条目无需填写 {@code agentId}。

+ * * @author yl * created on 2023/10/16 */ @@ -20,7 +30,16 @@ public class WxCpSingleProperties implements Serializable { */ private String corpId; /** - * 微信企业号 corpSecret + * 微信企业号 corpSecret(权限密钥) + * + *

企业微信针对不同的功能模块提供了不同的 Secret,每种 Secret 只对对应模块的接口有调用权限:

+ *
    + *
  • 自建应用 Secret:在"应用管理 - 自建应用"中找到对应应用,查看其 Secret, + * 使用时需同时配置对应的 {@code agentId}
  • + *
  • 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看, + * 使用此 Secret 可管理部门、成员,无需配置 {@code agentId}
  • + *
  • 其他 Secret(客户联系等):根据需要在企业微信后台查看对应 Secret
  • + *
*/ private String corpSecret; /** @@ -28,7 +47,10 @@ public class WxCpSingleProperties implements Serializable { */ private String token; /** - * 微信企业号应用 ID + * 微信企业号应用 ID(AgentId) + * + *

使用自建应用 Secret 时,需要填写对应应用的 AgentId。

+ *

使用通讯录同步 Secret 时,无需填写此字段。

*/ private Integer agentId; /** diff --git a/solon-plugins/wx-java-cp-solon-plugin/pom.xml b/solon-plugins/wx-java-cp-solon-plugin/pom.xml index b8d2c43351..4682d570cd 100644 --- a/solon-plugins/wx-java-cp-solon-plugin/pom.xml +++ b/solon-plugins/wx-java-cp-solon-plugin/pom.xml @@ -4,7 +4,7 @@ wx-java-solon-plugins com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml index 6ca319ad7f..7cfdbcbaf2 100644 --- a/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml @@ -5,7 +5,7 @@ wx-java-solon-plugins com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml b/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml index 28f80f00b1..a540df9492 100644 --- a/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml +++ b/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml @@ -4,7 +4,7 @@ wx-java-solon-plugins com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml index f78f9b5d59..359fffe823 100644 --- a/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml @@ -5,7 +5,7 @@ wx-java-solon-plugins com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/solon-plugins/wx-java-mp-solon-plugin/pom.xml b/solon-plugins/wx-java-mp-solon-plugin/pom.xml index 6ca5283c18..8678e82a10 100644 --- a/solon-plugins/wx-java-mp-solon-plugin/pom.xml +++ b/solon-plugins/wx-java-mp-solon-plugin/pom.xml @@ -5,7 +5,7 @@ wx-java-solon-plugins com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java index c1f5ebf0f3..7121f2d700 100644 --- a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java +++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java @@ -36,9 +36,9 @@ public WxMpConfigStorage wxMaConfig() { } private WxMpRedissonConfigImpl getWxMpInRedissonConfigStorage() { - RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + String configuredHost = applicationContext.cfg().get(WxMpProperties.PREFIX + ".config-storage.redis.host"); RedissonClient redissonClient; - if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + if (StringUtils.isNotEmpty(configuredHost)) { redissonClient = applicationContext.getBean("wxMpRedissonClient"); } else { redissonClient = applicationContext.getBean(RedissonClient.class); diff --git a/solon-plugins/wx-java-open-solon-plugin/pom.xml b/solon-plugins/wx-java-open-solon-plugin/pom.xml index dcd856dc26..b4d9bc904c 100644 --- a/solon-plugins/wx-java-open-solon-plugin/pom.xml +++ b/solon-plugins/wx-java-open-solon-plugin/pom.xml @@ -5,7 +5,7 @@ wx-java-solon-plugins com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/solon-plugins/wx-java-pay-solon-plugin/README.md b/solon-plugins/wx-java-pay-solon-plugin/README.md index b0e212593b..8ff3416293 100644 --- a/solon-plugins/wx-java-pay-solon-plugin/README.md +++ b/solon-plugins/wx-java-pay-solon-plugin/README.md @@ -23,6 +23,8 @@ wx: pay: appId: xxxxxxxxxxx mchId: 15xxxxxxxxx #商户id + apiHostUrl: http://10.0.0.1:3128 # 可选:代理主机 + apiHostUrlPath: /api-weixin # 可选:代理入口前缀 apiV3Key: Dc1DBwSc094jACxxxxxxxxxxxxxxx #V3密钥 certSerialNo: 62C6CEAA360BCxxxxxxxxxxxxxxx privateKeyPath: classpath:cert/apiclient_key.pem #apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径 diff --git a/solon-plugins/wx-java-pay-solon-plugin/pom.xml b/solon-plugins/wx-java-pay-solon-plugin/pom.xml index 26e0b7faca..e158b9f1a6 100644 --- a/solon-plugins/wx-java-pay-solon-plugin/pom.xml +++ b/solon-plugins/wx-java-pay-solon-plugin/pom.xml @@ -5,7 +5,7 @@ wx-java-solon-plugins com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/config/WxPayAutoConfiguration.java b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/config/WxPayAutoConfiguration.java index 3ef7456daa..c311a099a2 100644 --- a/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/config/WxPayAutoConfiguration.java +++ b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/config/WxPayAutoConfiguration.java @@ -55,10 +55,11 @@ public WxPayService wxPayService() { payConfig.setPrivateKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath())); payConfig.setPrivateCertPath(StringUtils.trimToNull(this.properties.getPrivateCertPath())); payConfig.setCertSerialNo(StringUtils.trimToNull(this.properties.getCertSerialNo())); - payConfig.setApiV3Key(StringUtils.trimToNull(this.properties.getApiv3Key())); + payConfig.setApiV3Key(StringUtils.trimToNull(this.properties.getApiV3Key())); payConfig.setPublicKeyId(StringUtils.trimToNull(this.properties.getPublicKeyId())); payConfig.setPublicKeyPath(StringUtils.trimToNull(this.properties.getPublicKeyPath())); payConfig.setApiHostUrl(StringUtils.trimToNull(this.properties.getApiHostUrl())); + payConfig.setApiHostUrlPath(StringUtils.trimToNull(this.properties.getApiHostUrlPath())); payConfig.setStrictlyNeedWechatPaySerial(this.properties.isStrictlyNeedWechatPaySerial()); payConfig.setFullPublicKeyModel(this.properties.isFullPublicKeyModel()); diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/properties/WxPayProperties.java b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/properties/WxPayProperties.java index d394fefbd1..fe024f59f1 100644 --- a/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/properties/WxPayProperties.java +++ b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/properties/WxPayProperties.java @@ -59,7 +59,7 @@ public class WxPayProperties { /** * apiV3秘钥 */ - private String apiv3Key; + private String apiV3Key; /** * 微信支付分回调地址 @@ -114,13 +114,19 @@ public class WxPayProperties { private String apiHostUrl; /** - * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认不添加 + * 自定义API主机路径前缀(用于代理入口前缀) + * 例如:/api-weixin */ - private boolean strictlyNeedWechatPaySerial = false; + private String apiHostUrlPath; /** - * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认不使用 + * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加 */ - private boolean fullPublicKeyModel = false; + private boolean strictlyNeedWechatPaySerial = true; + + /** + * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认使用 + */ + private boolean fullPublicKeyModel = true; } diff --git a/solon-plugins/wx-java-qidian-solon-plugin/pom.xml b/solon-plugins/wx-java-qidian-solon-plugin/pom.xml index cb0caaa1e4..b03fa0e22f 100644 --- a/solon-plugins/wx-java-qidian-solon-plugin/pom.xml +++ b/solon-plugins/wx-java-qidian-solon-plugin/pom.xml @@ -3,7 +3,7 @@ wx-java-solon-plugins com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/spring-boot-starters/pom.xml b/spring-boot-starters/pom.xml index ff2ce88236..6d3c7c7e00 100644 --- a/spring-boot-starters/pom.xml +++ b/spring-boot-starters/pom.xml @@ -6,7 +6,7 @@ com.github.binarywang wx-java - 4.8.2.B + 4.8.4.B pom wx-java-spring-boot-starters diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml index de7a389532..d3cf55aac1 100644 --- a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml index 9f22f79503..d36164d58a 100644 --- a/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml @@ -3,7 +3,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/README.md index e3ea7bf0f8..0f0b74695e 100644 --- a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/README.md +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/README.md @@ -6,6 +6,29 @@ - 未实现 WxCpTpService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。 - 未实现 WxCpCgService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。 +## 关于 corp-secret 的说明 + +企业微信中不同功能模块对应不同的 `corp-secret`,每种 Secret 只对对应模块的接口具有调用权限: + +| Secret 类型 | 获取位置 | 可调用的接口 | 是否需要 agent-id | +|---|---|---|---| +| 自建应用 Secret | 应用管理 → 自建应用 → 选择应用 → 查看 Secret | 该应用有权限的接口 | **必填** | +| 通讯录同步 Secret | 管理工具 → 通讯录同步 → 查看 Secret | 部门/成员增删改查等通讯录接口 | **不填** | +| 客户联系 Secret | 客户联系 → API → Secret | 客户联系相关接口 | 不填 | + +> **常见问题**: +> - 使用自建应用 Secret + agent-id 可以获取部门列表,但**无法更新部门**(因为写接口需要通讯录同步权限) +> - 使用通讯录同步 Secret 可以同步部门,但**调用某些需要 agent-id 的应用接口会报错** + +如需同时使用多种权限范围,可在 `wx.cp.corps` 下配置多个条目,每个条目使用对应权限的 Secret,通过不同的 `tenantId` 区分后使用。 + +> **配置限制说明**: +> - 当前 starter 实现会校验:同一 `corp-id` 下,`agent-id` **必须唯一** +> - 同一 `corp-id` 下,**只能有一个条目不填 `agent-id`** +> - 否则会因为 token/ticket 缓存 key 冲突而在启动时直接抛异常 +> +> 因此,像"通讯录同步 Secret""客户联系 Secret"这类通常不填写 `agent-id` 的配置,**不能**在同一个 `corp-id` 下同时配置多个 `agent-id` 均为空的条目;如确有多个条目,请确保其中最多只有一个未填写 `agent-id`。 + ## 快速开始 1. 引入依赖 @@ -18,25 +41,21 @@ ``` 2. 添加配置(application.properties) ```properties - # 应用 1 配置 - wx.cp.corps.tenantId1.corp-id = @corp-id - wx.cp.corps.tenantId1.corp-secret = @corp-secret - ## 选填 - wx.cp.corps.tenantId1.agent-id = @agent-id - wx.cp.corps.tenantId1.token = @token - wx.cp.corps.tenantId1.aes-key = @aes-key - wx.cp.corps.tenantId1.msg-audit-priKey = @msg-audit-priKey - wx.cp.corps.tenantId1.msg-audit-lib-path = @msg-audit-lib-path - - # 应用 2 配置 - wx.cp.corps.tenantId2.corp-id = @corp-id - wx.cp.corps.tenantId2.corp-secret = @corp-secret + # 自建应用 1 配置(使用自建应用 Secret,需填写 agent-id) + wx.cp.corps.app1.corp-id = @corp-id + wx.cp.corps.app1.corp-secret = @自建应用的Secret(在"应用管理-自建应用"中查看) + wx.cp.corps.app1.agent-id = @自建应用的AgentId ## 选填 - wx.cp.corps.tenantId2.agent-id = @agent-id - wx.cp.corps.tenantId2.token = @token - wx.cp.corps.tenantId2.aes-key = @aes-key - wx.cp.corps.tenantId2.msg-audit-priKey = @msg-audit-priKey - wx.cp.corps.tenantId2.msg-audit-lib-path = @msg-audit-lib-path + wx.cp.corps.app1.token = @token + wx.cp.corps.app1.aes-key = @aes-key + wx.cp.corps.app1.msg-audit-priKey = @msg-audit-priKey + wx.cp.corps.app1.msg-audit-lib-path = @msg-audit-lib-path + + # 通讯录同步配置(使用通讯录同步 Secret,不需要填写 agent-id) + # 此配置用于部门、成员的增删改查等通讯录管理操作 + wx.cp.corps.contact.corp-id = @corp-id + wx.cp.corps.contact.corp-secret = @通讯录同步的Secret(在"管理工具-通讯录同步"中查看) + ## agent-id 不填,通讯录同步不需要 agentId # 公共配置 ## ConfigStorage 配置(选填) @@ -59,8 +78,10 @@ ```java import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServices; +import me.chanjar.weixin.cp.api.WxCpDepartmentService; import me.chanjar.weixin.cp.api.WxCpService; import me.chanjar.weixin.cp.api.WxCpUserService; +import me.chanjar.weixin.cp.bean.WxCpDepart; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -70,27 +91,21 @@ public class DemoService { private WxCpMultiServices wxCpMultiServices; public void test() { - // 应用 1 的 WxCpService - WxCpService wxCpService1 = wxCpMultiServices.getWxCpService("tenantId1"); - WxCpUserService userService1 = wxCpService1.getUserService(); - userService1.getUserId("xxx"); - // todo ... - - // 应用 2 的 WxCpService - WxCpService wxCpService2 = wxCpMultiServices.getWxCpService("tenantId2"); - WxCpUserService userService2 = wxCpService2.getUserService(); - userService2.getUserId("xxx"); + // 使用自建应用的 WxCpService(对应 corp-secret 为自建应用 Secret) + WxCpService appService = wxCpMultiServices.getWxCpService("app1"); + WxCpUserService userService = appService.getUserService(); + userService.getUserId("xxx"); // todo ... - // 应用 3 的 WxCpService - WxCpService wxCpService3 = wxCpMultiServices.getWxCpService("tenantId3"); - // 判断是否为空 - if (wxCpService3 == null) { - // todo wxCpService3 为空,请先配置 tenantId3 企业微信应用参数 - return; - } - WxCpUserService userService3 = wxCpService3.getUserService(); - userService3.getUserId("xxx"); + // 使用通讯录同步的 WxCpService(对应 corp-secret 为通讯录同步 Secret) + // 通讯录同步 Secret 具有部门/成员增删改查等权限 + WxCpService contactService = wxCpMultiServices.getWxCpService("contact"); + WxCpDepartmentService departmentService = contactService.getDepartmentService(); + // 更新部门示例(WxCpDepart 包含 id、name、parentId 等字段) + WxCpDepart depart = new WxCpDepart(); + depart.setId(100L); + depart.setName("新部门名称"); + departmentService.update(depart); // todo ... } } diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml index 514a67b3ec..e4c3b1ba8d 100644 --- a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml @@ -4,7 +4,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpConfiguration.java index 9b959222e0..a10bdf9bed 100644 --- a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpConfiguration.java +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpConfiguration.java @@ -18,6 +18,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -40,6 +41,13 @@ protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiPrope /** * 校验同一个企业下,agentId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。 * + *

同一企业(corpId 相同)下可配置多个条目以使用不同的权限 Secret,例如:

+ *
    + *
  • 自建应用条目:填写应用对应的 corpSecret 和 agentId
  • + *
  • 通讯录同步条目:填写通讯录同步 Secret,agentId 可不填(null)
  • + *
+ *

但同一 corpId 下不允许出现重复的 agentId(包括多个 null)。

+ * * 查看 {@link me.chanjar.weixin.cp.config.impl.AbstractWxCpInRedisConfigImpl#setAgentId(Integer)} */ Collection corpList = corps.values(); @@ -52,8 +60,8 @@ protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiPrope String corpId = entry.getKey(); // 校验每个企业下,agentId 是否唯一 boolean multi = entry.getValue().stream() - // 通讯录没有 agentId,如果不判断是否为空,这里会报 NPE 异常 - .collect(Collectors.groupingBy(c -> c.getAgentId() == null ? 0 : c.getAgentId(), Collectors.counting())) + // 通讯录没有 agentId,使用字符串转换避免 null 与 agentId=0 冲突 + .collect(Collectors.groupingBy(c -> Objects.toString(c.getAgentId(), "null"), Collectors.counting())) .entrySet().stream().anyMatch(e -> e.getValue() > 1); if (multi) { throw new RuntimeException("请确保企业微信配置唯一性[" + corpId + "]"); diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpSingleProperties.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpSingleProperties.java index 8ad7149fe6..fcfa654a15 100644 --- a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpSingleProperties.java +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpSingleProperties.java @@ -8,6 +8,16 @@ /** * 企业微信企业相关配置属性 * + *

企业微信中不同的 corpSecret 对应不同的权限范围,常见的有:

+ *
    + *
  • 自建应用 Secret:在"应用管理 - 自建应用"中查看,只能调用该应用有权限的接口
  • + *
  • 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,用于管理部门和成员(增删改查)
  • + *
  • 客户联系 Secret:在"客户联系"中查看,用于客户联系相关接口
  • + *
+ *

如需同时使用多种权限范围(例如:既要操作通讯录,又要调用自建应用接口), + * 可在 {@code wx.cp.corps} 下配置多个条目,每个条目使用对应权限的 {@code corpSecret}, + * 其中通讯录同步的条目无需填写 {@code agentId}。

+ * * @author yl * created on 2023/10/16 */ @@ -20,7 +30,16 @@ public class WxCpSingleProperties implements Serializable { */ private String corpId; /** - * 微信企业号 corpSecret + * 微信企业号 corpSecret(权限密钥) + * + *

企业微信针对不同的功能模块提供了不同的 Secret,每种 Secret 只对对应模块的接口有调用权限:

+ *
    + *
  • 自建应用 Secret:在"应用管理 - 自建应用"中找到对应应用,查看其 Secret, + * 使用时需同时配置对应的 {@code agentId}
  • + *
  • 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看, + * 使用此 Secret 可管理部门、成员,无需配置 {@code agentId}
  • + *
  • 其他 Secret(客户联系等):根据需要在企业微信后台查看对应 Secret
  • + *
*/ private String corpSecret; /** @@ -28,7 +47,10 @@ public class WxCpSingleProperties implements Serializable { */ private String token; /** - * 微信企业号应用 ID + * 微信企业号应用 ID(AgentId) + * + *

使用自建应用 Secret 时,需要填写对应应用的 AgentId。

+ *

使用通讯录同步 Secret 时,无需填写此字段。

*/ private Integer agentId; /** diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml index df23601c73..f2b6d12f8e 100644 --- a/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml @@ -4,7 +4,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml index fa0b98aabf..fc3b490242 100644 --- a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml @@ -4,7 +4,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml index 05f595ac26..ebb6deba33 100644 --- a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml index 25d5f66758..9ebb1ee540 100644 --- a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml @@ -4,7 +4,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml index 88b11099a3..37bc9faedb 100644 --- a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml index 9e95574bc2..6e3d6da48c 100644 --- a/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml index c5cf07e799..e1a4509c6f 100644 --- a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml index 72c856f27c..e0d5779c6b 100644 --- a/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/README.md index d8d41b7de8..1ae4ac6299 100644 --- a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/README.md +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/README.md @@ -104,7 +104,7 @@ wx: # 公众号1配置 wx.pay.configs.wx1234567890abcdef.app-id=wx1234567890abcdef wx.pay.configs.wx1234567890abcdef.mch-id=1234567890 -wx.pay.configs.wx1234567890abcdef.apiv3-key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +wx.pay.configs.wx1234567890abcdef.api-v3-key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx wx.pay.configs.wx1234567890abcdef.cert-serial-no=62C6CEAA360BCxxxxxxxxxxxxxxx wx.pay.configs.wx1234567890abcdef.private-key-path=classpath:cert/app1/apiclient_key.pem wx.pay.configs.wx1234567890abcdef.private-cert-path=classpath:cert/app1/apiclient_cert.pem @@ -113,7 +113,7 @@ wx.pay.configs.wx1234567890abcdef.notify-url=https://example.com/pay/notify # 公众号2配置 wx.pay.configs.wx9876543210fedcba.app-id=wx9876543210fedcba wx.pay.configs.wx9876543210fedcba.mch-id=9876543210 -wx.pay.configs.wx9876543210fedcba.apiv3-key=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy +wx.pay.configs.wx9876543210fedcba.api-v3-key=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy wx.pay.configs.wx9876543210fedcba.cert-serial-no=73D7DFBB471CDxxxxxxxxxxxxxxx wx.pay.configs.wx9876543210fedcba.private-key-path=classpath:cert/app2/apiclient_key.pem wx.pay.configs.wx9876543210fedcba.private-cert-path=classpath:cert/app2/apiclient_cert.pem @@ -255,8 +255,9 @@ public class PayService { | payScorePermissionNotifyUrl | 支付分授权回调地址 | 无 | | useSandboxEnv | 是否使用沙箱环境 | false | | apiHostUrl | 自定义API主机地址 | https://api.mch.weixin.qq.com | -| strictlyNeedWechatPaySerial | 是否所有V3请求都添加序列号头 | false | -| fullPublicKeyModel | 是否完全使用公钥模式 | false | +| apiHostUrlPath | 自定义API主机路径前缀(代理入口前缀) | 空 | +| strictlyNeedWechatPaySerial | 是否所有V3请求都添加序列号头 | true | +| fullPublicKeyModel | 是否完全使用公钥模式 | true | | publicKeyId | 公钥ID | 无 | | publicKeyPath | 公钥文件路径 | 无 | @@ -312,5 +313,5 @@ wx: ## 更多信息 - [WxJava 项目首页](https://github.com/Wechat-Group/WxJava) -- [微信支付官方文档](https://pay.weixin.qq.com/wiki/doc/api/) -- [微信支付V3接口文档](https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml) +- [微信支付V2文档](https://pay.weixin.qq.com/doc/v2) +- [微信支付V3接口文档](https://pay.weixin.qq.com/doc/v3/merchant/4012062524) diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml index 1964bcbbfe..a2320eca2a 100644 --- a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPaySingleProperties.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPaySingleProperties.java index a5cda55fb0..ef936fc234 100644 --- a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPaySingleProperties.java +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPaySingleProperties.java @@ -58,7 +58,7 @@ public class WxPaySingleProperties implements Serializable { /** * apiV3秘钥. */ - private String apiv3Key; + private String apiV3Key; /** * 微信支付异步回调地址,通知url必须为直接可访问的url,不能携带参数. @@ -113,12 +113,18 @@ public class WxPaySingleProperties implements Serializable { private String apiHostUrl; /** - * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认不添加. + * 自定义API主机路径前缀(用于代理入口前缀). + * 例如:/api-weixin */ - private boolean strictlyNeedWechatPaySerial = false; + private String apiHostUrlPath; /** - * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认不使用. + * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加. */ - private boolean fullPublicKeyModel = false; + private boolean strictlyNeedWechatPaySerial = true; + + /** + * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认使用. + */ + private boolean fullPublicKeyModel = true; } diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java index 459fe3b6c0..7cbcceabb4 100644 --- a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java @@ -79,10 +79,11 @@ private WxPayService buildWxPayService(WxPaySingleProperties properties) { payConfig.setPrivateKeyPath(StringUtils.trimToNull(properties.getPrivateKeyPath())); payConfig.setPrivateCertPath(StringUtils.trimToNull(properties.getPrivateCertPath())); payConfig.setCertSerialNo(StringUtils.trimToNull(properties.getCertSerialNo())); - payConfig.setApiV3Key(StringUtils.trimToNull(properties.getApiv3Key())); + payConfig.setApiV3Key(StringUtils.trimToNull(properties.getApiV3Key())); payConfig.setPublicKeyId(StringUtils.trimToNull(properties.getPublicKeyId())); payConfig.setPublicKeyPath(StringUtils.trimToNull(properties.getPublicKeyPath())); payConfig.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl())); + payConfig.setApiHostUrlPath(StringUtils.trimToNull(properties.getApiHostUrlPath())); payConfig.setStrictlyNeedWechatPaySerial(properties.isStrictlyNeedWechatPaySerial()); payConfig.setFullPublicKeyModel(properties.isFullPublicKeyModel()); diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java index 25a091da02..87132fdcf3 100644 --- a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java @@ -26,6 +26,8 @@ "wx.pay.configs.app1.notify-url=https://example.com/pay/notify", "wx.pay.configs.app2.app-id=wx2222222222222222", "wx.pay.configs.app2.mch-id=2222222222", + "wx.pay.configs.app2.api-host-url=http://10.0.0.1:3128", + "wx.pay.configs.app2.api-host-url-path=/api-weixin", "wx.pay.configs.app2.apiv3-key=22222222222222222222222222222222", "wx.pay.configs.app2.cert-serial-no=2222222222222222", "wx.pay.configs.app2.private-key-path=classpath:cert/apiclient_key.pem", @@ -57,7 +59,9 @@ public void testConfiguration() { assertNotNull(app2Config, "app2 configuration should exist"); assertEquals("wx2222222222222222", app2Config.getAppId()); assertEquals("2222222222", app2Config.getMchId()); - assertEquals("22222222222222222222222222222222", app2Config.getApiv3Key()); + assertEquals("http://10.0.0.1:3128", app2Config.getApiHostUrl()); + assertEquals("/api-weixin", app2Config.getApiHostUrlPath()); + assertEquals("22222222222222222222222222222222", app2Config.getApiV3Key()); } @Test @@ -71,6 +75,7 @@ public void testGetWxPayService() { assertNotNull(app2Service, "Should get WxPayService for app2"); assertEquals("wx2222222222222222", app2Service.getConfig().getAppId()); assertEquals("2222222222", app2Service.getConfig().getMchId()); + assertEquals("/api-weixin", app2Service.getConfig().getApiHostUrlPath()); // 测试相同key返回相同实例 WxPayService app1ServiceAgain = wxPayMultiServices.getWxPayService("app1"); diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md b/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md index d87a38fb9c..bed890d5e8 100644 --- a/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md +++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md @@ -23,6 +23,8 @@ wx: pay: appId: xxxxxxxxxxx mchId: 15xxxxxxxxx #商户id + apiHostUrl: http://10.0.0.1:3128 # 可选:代理主机 + apiHostUrlPath: /api-weixin # 可选:代理入口前缀 apiV3Key: Dc1DBwSc094jACxxxxxxxxxxxxxxx #V3密钥 certSerialNo: 62C6CEAA360BCxxxxxxxxxxxxxxx privateKeyPath: classpath:cert/apiclient_key.pem #apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径 diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml index ecdb925730..67a26da606 100644 --- a/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java index 758fd929a1..7e748ba1a3 100644 --- a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java +++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java @@ -59,10 +59,11 @@ public WxPayService wxPayService() { payConfig.setPrivateKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath())); payConfig.setPrivateCertPath(StringUtils.trimToNull(this.properties.getPrivateCertPath())); payConfig.setCertSerialNo(StringUtils.trimToNull(this.properties.getCertSerialNo())); - payConfig.setApiV3Key(StringUtils.trimToNull(this.properties.getApiv3Key())); + payConfig.setApiV3Key(StringUtils.trimToNull(this.properties.getApiV3Key())); payConfig.setPublicKeyId(StringUtils.trimToNull(this.properties.getPublicKeyId())); payConfig.setPublicKeyPath(StringUtils.trimToNull(this.properties.getPublicKeyPath())); payConfig.setApiHostUrl(StringUtils.trimToNull(this.properties.getApiHostUrl())); + payConfig.setApiHostUrlPath(StringUtils.trimToNull(this.properties.getApiHostUrlPath())); payConfig.setStrictlyNeedWechatPaySerial(this.properties.isStrictlyNeedWechatPaySerial()); payConfig.setFullPublicKeyModel(this.properties.isFullPublicKeyModel()); diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java index 25f7d7c02e..49045c4ee0 100644 --- a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java +++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java @@ -57,7 +57,7 @@ public class WxPayProperties { /** * apiV3秘钥 */ - private String apiv3Key; + private String apiV3Key; /** * 微信支付异步回调地址,通知url必须为直接可访问的url,不能携带参数 @@ -112,13 +112,19 @@ public class WxPayProperties { private String apiHostUrl; /** - * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认不添加 + * 自定义API主机路径前缀(用于代理入口前缀) + * 例如:/api-weixin */ - private boolean strictlyNeedWechatPaySerial = false; + private String apiHostUrlPath; /** - * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认不使用 + * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加 */ - private boolean fullPublicKeyModel = false; + private boolean strictlyNeedWechatPaySerial = true; + + /** + * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认使用 + */ + private boolean fullPublicKeyModel = true; } diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml index 7e314df780..b07dc4276e 100644 --- a/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml @@ -3,7 +3,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.8.2.B + 4.8.4.B 4.0.0 diff --git a/weixin-graal/pom.xml b/weixin-graal/pom.xml index a55cc19226..eb12b9a728 100644 --- a/weixin-graal/pom.xml +++ b/weixin-graal/pom.xml @@ -6,7 +6,7 @@ com.github.binarywang wx-java - 4.8.2.B + 4.8.4.B weixin-graal diff --git a/weixin-java-aispeech/pom.xml b/weixin-java-aispeech/pom.xml new file mode 100644 index 0000000000..40c6557065 --- /dev/null +++ b/weixin-java-aispeech/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + com.github.binarywang + wx-java + 4.8.4.B + + + weixin-java-aispeech + WxJava - Aispeech Java SDK + 微信智能对话 Java SDK + + + + com.github.binarywang + weixin-java-common + ${project.version} + + + + org.apache.httpcomponents.client5 + httpclient5 + + + + org.testng + testng + test + + + org.projectlombok + lombok + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + src/test/resources/testng.xml + + + + + + + + + native-image + + false + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + com.github.binarywang.wx.graal.GraalProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor,lombok.launch.AnnotationProcessorHider$ClaimingProcessor + + + + com.github.binarywang + weixin-graal + ${project.version} + + + + + + + + + diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java new file mode 100644 index 0000000000..51d46562cb --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java @@ -0,0 +1,23 @@ +package me.chanjar.weixin.aispeech.api; + +import java.util.List; +import me.chanjar.weixin.aispeech.bean.dialog.AsyncTaskResult; +import me.chanjar.weixin.aispeech.bean.dialog.BotIntent; +import me.chanjar.weixin.aispeech.bean.dialog.DialogQueryRequest; +import me.chanjar.weixin.aispeech.bean.dialog.DialogResult; +import me.chanjar.weixin.aispeech.bean.dialog.PublishProgress; +import me.chanjar.weixin.common.error.WxErrorException; + +public interface WxAispeechDialogService { + String getAccessToken(String appid, String account) throws WxErrorException; + + String importBotJson(int mode, List data) throws WxErrorException; + + String publishBot() throws WxErrorException; + + PublishProgress getPublishProgress(String env) throws WxErrorException; + + AsyncTaskResult queryAsyncTask(String taskId) throws WxErrorException; + + DialogResult query(DialogQueryRequest request) throws WxErrorException; +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java new file mode 100644 index 0000000000..fa27d48235 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java @@ -0,0 +1,51 @@ +package me.chanjar.weixin.aispeech.api; + +import java.io.File; +import java.util.List; +import java.util.Map; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeInfo; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeManualCreateRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveProgress; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeTagRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUpdateRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUrlCreateRequest; +import me.chanjar.weixin.common.error.WxErrorException; + +public interface WxAispeechKnowledgeService { + KnowledgeInfo createKnowledgeByFile(String knowledgeBaseId, File file, String title, String description, String metadata) + throws WxErrorException; + + KnowledgeInfo createKnowledgeByUrl(String knowledgeBaseId, KnowledgeUrlCreateRequest request) throws WxErrorException; + + KnowledgeInfo createKnowledgeByManual(String knowledgeBaseId, KnowledgeManualCreateRequest request) throws WxErrorException; + + List listKnowledge(String knowledgeBaseId, Integer page, Integer pageSize) throws WxErrorException; + + List listKnowledgeByIds(List knowledgeIds) throws WxErrorException; + + KnowledgeInfo getKnowledge(String knowledgeId) throws WxErrorException; + + KnowledgeInfo updateKnowledge(String knowledgeId, KnowledgeUpdateRequest request) throws WxErrorException; + + KnowledgeInfo updateManualKnowledge(String knowledgeId, KnowledgeManualCreateRequest request) throws WxErrorException; + + boolean deleteKnowledge(String knowledgeId) throws WxErrorException; + + boolean updateKnowledgeTags(List knowledgeIds, Long tagId) throws WxErrorException; + + List searchKnowledge(String keyword, String knowledgeBaseId, Integer page, Integer pageSize) + throws WxErrorException; + + String moveKnowledge(KnowledgeMoveRequest request) throws WxErrorException; + + KnowledgeMoveProgress getMoveProgress(String taskId) throws WxErrorException; + + boolean createKnowledgeBaseTag(String knowledgeBaseId, KnowledgeTagRequest request) throws WxErrorException; + + boolean updateKnowledgeBaseTag(String knowledgeBaseId, String tagId, KnowledgeTagRequest request) throws WxErrorException; + + String postRaw(String path, Object requestBody) throws WxErrorException; + + String getRaw(String path, Map queryParams) throws WxErrorException; +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java new file mode 100644 index 0000000000..08ccf837e4 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java @@ -0,0 +1,13 @@ +package me.chanjar.weixin.aispeech.api; + +import me.chanjar.weixin.aispeech.config.WxAispeechConfigStorage; + +public interface WxAispeechService { + WxAispeechDialogService getDialogService(); + + WxAispeechKnowledgeService getKnowledgeService(); + + WxAispeechConfigStorage getConfigStorage(); + + void setConfigStorage(WxAispeechConfigStorage configStorage); +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java new file mode 100644 index 0000000000..9bd53b454e --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java @@ -0,0 +1,129 @@ +package me.chanjar.weixin.aispeech.api.impl; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import me.chanjar.weixin.aispeech.api.WxAispeechDialogService; +import me.chanjar.weixin.aispeech.bean.dialog.AispeechApiResponse; +import me.chanjar.weixin.aispeech.bean.dialog.AsyncTaskResult; +import me.chanjar.weixin.aispeech.bean.dialog.BotIntent; +import me.chanjar.weixin.aispeech.bean.dialog.DialogQueryRequest; +import me.chanjar.weixin.aispeech.bean.dialog.DialogResult; +import me.chanjar.weixin.aispeech.bean.dialog.PublishProgress; +import me.chanjar.weixin.aispeech.util.WxAispeechSignUtil; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; +import org.apache.commons.lang3.StringUtils; + +public class WxAispeechDialogServiceImpl implements WxAispeechDialogService { + private final WxAispeechServiceImpl service; + + public WxAispeechDialogServiceImpl(WxAispeechServiceImpl service) { + this.service = service; + } + + @Override + public String getAccessToken(String appid, String account) throws WxErrorException { + Map request = new HashMap<>(); + if (StringUtils.isNotBlank(account)) { + request.put("account", account); + } + + String response = service.executeDialogPost("/v2/token", request, false, appid); + Type type = new TypeToken>() { } .getType(); + AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type); + ensureSuccess(result); + String token = result.getData().get("access_token").getAsString(); + service.getConfigStorage().setOpenAiToken(token); + return token; + } + + @Override + public String importBotJson(int mode, List data) throws WxErrorException { + Map request = new HashMap<>(); + request.put("mode", mode); + request.put("data", data); + + String response = service.executeDialogPost("/v2/bot/import/json", request, true, null); + Type type = new TypeToken>() { } .getType(); + AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type); + ensureSuccess(result); + return result.getData().get("task_id").getAsString(); + } + + @Override + public String publishBot() throws WxErrorException { + String response = service.executeDialogPost("/v2/bot/publish", "{}", true, null); + Type type = new TypeToken>() { } .getType(); + AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type); + ensureSuccess(result); + return result.getRequestId(); + } + + @Override + public PublishProgress getPublishProgress(String env) throws WxErrorException { + Map request = new HashMap<>(); + request.put("env", env); + + String response = service.executeDialogPost("/v2/bot/effective_progress", request, true, null); + Type type = new TypeToken>() { } .getType(); + AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type); + ensureSuccess(result); + return result.getData(); + } + + @Override + public AsyncTaskResult queryAsyncTask(String taskId) throws WxErrorException { + Map request = new HashMap<>(); + request.put("task_id", taskId); + + String response = service.executeDialogPost("/v2/async/fetch", request, true, null); + Type type = new TypeToken>() { } .getType(); + AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type); + ensureSuccess(result); + return result.getData(); + } + + @Override + public DialogResult query(DialogQueryRequest request) throws WxErrorException { + String json = WxGsonBuilder.create().toJson(request); + String encrypted = WxAispeechSignUtil.encryptAesCbcToBase64(json, service.getConfigStorage().getAesKey()); + String response = service.executeDialogPost("/v2/bot/query", encrypted, true, null); + + String responseJson = response; + if (!looksLikeJson(response)) { + responseJson = WxAispeechSignUtil.decryptAesCbcFromBase64(response, service.getConfigStorage().getAesKey()); + } + + Type type = new TypeToken>() { } .getType(); + AispeechApiResponse result = WxGsonBuilder.create().fromJson(responseJson, type); + ensureSuccess(result); + + DialogResult dialogResult = result.getData(); + if (dialogResult != null && looksLikeJson(dialogResult.getAnswer())) { + dialogResult.setRawAnswer(WxGsonBuilder.create().fromJson(dialogResult.getAnswer(), JsonElement.class)); + } + return dialogResult; + } + + private boolean looksLikeJson(String value) { + return StringUtils.isNotBlank(value) && (value.startsWith("{") || value.startsWith("[")); + } + + private void ensureSuccess(AispeechApiResponse response) throws WxErrorException { + if (response == null) { + throw new WxErrorException("响应为空"); + } + if (response.getCode() == null || response.getCode() != 0) { + throw new WxErrorException(WxError.builder() + .errorCode(response.getCode() == null ? -1 : response.getCode()) + .errorMsg(response.getMsg()) + .build()); + } + } +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java new file mode 100644 index 0000000000..708f12890d --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java @@ -0,0 +1,184 @@ +package me.chanjar.weixin.aispeech.api.impl; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import java.io.File; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; +import me.chanjar.weixin.aispeech.api.WxAispeechKnowledgeService; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeInfo; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeListResult; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeManualCreateRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveProgress; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeTagRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUpdateRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUrlCreateRequest; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; +import org.apache.commons.lang3.StringUtils; + +public class WxAispeechKnowledgeServiceImpl implements WxAispeechKnowledgeService { + private final WxAispeechServiceImpl service; + + public WxAispeechKnowledgeServiceImpl(WxAispeechServiceImpl service) { + this.service = service; + } + + @Override + public KnowledgeInfo createKnowledgeByFile(String knowledgeBaseId, File file, String title, String description, String metadata) + throws WxErrorException { + String response = service.executeKnowledgeMultipartPost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/file", + file, title, description, metadata); + return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class); + } + + @Override + public KnowledgeInfo createKnowledgeByUrl(String knowledgeBaseId, KnowledgeUrlCreateRequest request) + throws WxErrorException { + String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/url", request); + return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class); + } + + @Override + public KnowledgeInfo createKnowledgeByManual(String knowledgeBaseId, KnowledgeManualCreateRequest request) + throws WxErrorException { + String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/manual", request); + return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class); + } + + @Override + public List listKnowledge(String knowledgeBaseId, Integer page, Integer pageSize) + throws WxErrorException { + Map query = new HashMap<>(); + query.put("page", page == null ? null : String.valueOf(page)); + query.put("page_size", pageSize == null ? null : String.valueOf(pageSize)); + String response = service.executeKnowledgeGet("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge", query); + KnowledgeListResult result = WxGsonBuilder.create().fromJson(response, KnowledgeListResult.class); + return result == null ? null : result.getData(); + } + + @Override + public List listKnowledgeByIds(List knowledgeIds) throws WxErrorException { + if (knowledgeIds == null || knowledgeIds.isEmpty()) { + return null; + } + StringJoiner joiner = new StringJoiner(","); + for (String knowledgeId : knowledgeIds) { + if (StringUtils.isNotBlank(knowledgeId)) { + joiner.add(knowledgeId); + } + } + if (joiner.length() == 0) { + return null; + } + + Map query = new HashMap<>(); + query.put("ids", joiner.toString()); + String response = service.executeKnowledgeGet("/api/v1/knowledge/batch", query); + return parseKnowledgeInfoList(response); + } + + @Override + public KnowledgeInfo getKnowledge(String knowledgeId) throws WxErrorException { + String response = service.executeKnowledgeGet("/api/v1/knowledge/" + knowledgeId, null); + return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class); + } + + @Override + public KnowledgeInfo updateKnowledge(String knowledgeId, KnowledgeUpdateRequest request) throws WxErrorException { + String response = service.executeKnowledgePut("/api/v1/knowledge/" + knowledgeId, request); + return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class); + } + + @Override + public KnowledgeInfo updateManualKnowledge(String knowledgeId, KnowledgeManualCreateRequest request) throws WxErrorException { + String response = service.executeKnowledgePut("/api/v1/knowledge/manual/" + knowledgeId, request); + return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class); + } + + @Override + public boolean deleteKnowledge(String knowledgeId) throws WxErrorException { + service.executeKnowledgeDelete("/api/v1/knowledge/" + knowledgeId); + return true; + } + + @Override + public boolean updateKnowledgeTags(List knowledgeIds, Long tagId) throws WxErrorException { + if (knowledgeIds == null || knowledgeIds.isEmpty() || tagId == null) { + return false; + } + + Map request = new HashMap<>(); + request.put("knowledge_ids", knowledgeIds); + request.put("tag_id", tagId); + String response = service.executeKnowledgePut("/api/v1/knowledge/tags", request); + return StringUtils.isNotBlank(response); + } + + @Override + public List searchKnowledge(String keyword, String knowledgeBaseId, Integer page, Integer pageSize) + throws WxErrorException { + Map query = new HashMap<>(); + query.put("keyword", keyword); + query.put("knowledge_base_id", knowledgeBaseId); + query.put("page", page == null ? null : String.valueOf(page)); + query.put("page_size", pageSize == null ? null : String.valueOf(pageSize)); + String response = service.executeKnowledgeGet("/api/v1/knowledge/search", query); + return parseKnowledgeInfoList(response); + } + + @Override + public String moveKnowledge(KnowledgeMoveRequest request) throws WxErrorException { + return service.executeKnowledgePost("/api/v1/knowledge/move", request); + } + + @Override + public KnowledgeMoveProgress getMoveProgress(String taskId) throws WxErrorException { + String response = service.executeKnowledgeGet("/api/v1/knowledge/move/progress/" + taskId, null); + return WxGsonBuilder.create().fromJson(response, KnowledgeMoveProgress.class); + } + + @Override + public boolean createKnowledgeBaseTag(String knowledgeBaseId, KnowledgeTagRequest request) throws WxErrorException { + String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/tags", request); + return StringUtils.isNotBlank(response); + } + + @Override + public boolean updateKnowledgeBaseTag(String knowledgeBaseId, String tagId, KnowledgeTagRequest request) + throws WxErrorException { + String response = service.executeKnowledgePut("/api/v1/knowledge-bases/" + knowledgeBaseId + "/tags/" + tagId, request); + return StringUtils.isNotBlank(response); + } + + @Override + public String postRaw(String path, Object requestBody) throws WxErrorException { + return service.executeKnowledgePost(path, requestBody); + } + + @Override + public String getRaw(String path, Map queryParams) throws WxErrorException { + return service.executeKnowledgeGet(path, queryParams); + } + + private List parseKnowledgeInfoList(String response) { + if (StringUtils.isBlank(response)) { + return null; + } + + JsonElement element = WxGsonBuilder.create().fromJson(response, JsonElement.class); + Type listType = new TypeToken>() { } .getType(); + if (element != null && element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + if (object.has("data")) { + return WxGsonBuilder.create().fromJson(object.get("data"), listType); + } + } + return WxGsonBuilder.create().fromJson(element, listType); + } +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java new file mode 100644 index 0000000000..e37d60e352 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java @@ -0,0 +1,4 @@ +package me.chanjar.weixin.aispeech.api.impl; + +public class WxAispeechServiceHttpClientImpl extends WxAispeechServiceHttpComponentsImpl { +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java new file mode 100644 index 0000000000..ac91d98938 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java @@ -0,0 +1,4 @@ +package me.chanjar.weixin.aispeech.api.impl; + +public class WxAispeechServiceHttpComponentsImpl extends WxAispeechServiceImpl { +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java new file mode 100644 index 0000000000..37a657cef2 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java @@ -0,0 +1,250 @@ +package me.chanjar.weixin.aispeech.api.impl; + +import com.google.gson.Gson; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.UUID; +import me.chanjar.weixin.aispeech.api.WxAispeechDialogService; +import me.chanjar.weixin.aispeech.api.WxAispeechKnowledgeService; +import me.chanjar.weixin.aispeech.api.WxAispeechService; +import me.chanjar.weixin.aispeech.config.WxAispeechConfigStorage; +import me.chanjar.weixin.aispeech.util.WxAispeechSignUtil; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.hc.DefaultHttpComponentsClientBuilder; +import me.chanjar.weixin.common.util.http.hc.HttpComponentsClientBuilder; +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.classic.methods.HttpPut; +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.net.URIBuilder; +import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; + +public class WxAispeechServiceImpl implements WxAispeechService { + private static final Gson GSON = new Gson(); + + private final WxAispeechDialogService dialogService = new WxAispeechDialogServiceImpl(this); + private final WxAispeechKnowledgeService knowledgeService = new WxAispeechKnowledgeServiceImpl(this); + + private WxAispeechConfigStorage configStorage; + private CloseableHttpClient httpClient; + private HttpHost proxy; + + @Override + public WxAispeechDialogService getDialogService() { + return dialogService; + } + + @Override + public WxAispeechKnowledgeService getKnowledgeService() { + return knowledgeService; + } + + @Override + public WxAispeechConfigStorage getConfigStorage() { + return configStorage; + } + + @Override + public void setConfigStorage(WxAispeechConfigStorage configStorage) { + this.configStorage = configStorage; + this.initHttp(); + } + + protected void initHttp() { + HttpComponentsClientBuilder builder = configStorage.getHttpComponentsClientBuilder(); + if (builder == null) { + builder = DefaultHttpComponentsClientBuilder.get(); + } + + builder.httpProxyHost(configStorage.getHttpProxyHost()) + .httpProxyPort(configStorage.getHttpProxyPort()) + .httpProxyUsername(configStorage.getHttpProxyUsername()) + .httpProxyPassword(configStorage.getHttpProxyPassword() == null ? null : + configStorage.getHttpProxyPassword().toCharArray()); + + if (configStorage.getHttpProxyHost() != null && configStorage.getHttpProxyPort() > 0) { + this.proxy = new HttpHost(configStorage.getHttpProxyHost(), configStorage.getHttpProxyPort()); + } else { + this.proxy = null; + } + + this.httpClient = builder.build(); + } + + protected String executeDialogPost(String path, Object requestBody, boolean withOpenToken, String appid) + throws WxErrorException { + String body = toBody(requestBody); + String requestId = UUID.randomUUID().toString(); + long timestamp = System.currentTimeMillis() / 1000; + String nonce = randomNonce(); + String sign = WxAispeechSignUtil.calcDialogSign(configStorage.getToken(), timestamp, nonce, body); + String resolvedAppid = StringUtils.defaultIfBlank(appid, configStorage.getAppid()); + + HttpPost request = new HttpPost(configStorage.getDialogApiBaseUrl() + path); + request.setHeader("request_id", requestId); + request.setHeader("timestamp", String.valueOf(timestamp)); + request.setHeader("nonce", nonce); + request.setHeader("sign", sign); + request.setHeader("Content-Type", ContentType.APPLICATION_JSON.getMimeType()); + if (withOpenToken) { + if (StringUtils.isBlank(configStorage.getOpenAiToken())) { + throw new WxErrorException("X-OPENAI-TOKEN不能为空,请先调用getAccessToken或手动设置"); + } + request.setHeader("X-OPENAI-TOKEN", configStorage.getOpenAiToken()); + } else { + if (StringUtils.isBlank(resolvedAppid)) { + throw new WxErrorException("X-APPID不能为空"); + } + request.setHeader("X-APPID", resolvedAppid); + } + request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON)); + return executeRequest(request); + } + + protected String executeKnowledgeGet(String path, Map queryParams) throws WxErrorException { + try { + URIBuilder builder = new URIBuilder(configStorage.getKnowledgeApiBaseUrl() + path); + if (queryParams != null) { + for (Map.Entry entry : queryParams.entrySet()) { + if (entry.getValue() != null) { + builder.addParameter(entry.getKey(), entry.getValue()); + } + } + } + HttpGet request = new HttpGet(builder.build()); + enrichKnowledgeHeaders(request, ""); + return executeRequest(request); + } catch (Exception e) { + throw toWxErrorException(e); + } + } + + protected String executeKnowledgePost(String path, Object requestBody) throws WxErrorException { + String body = toBody(requestBody); + HttpPost request = new HttpPost(configStorage.getKnowledgeApiBaseUrl() + path); + request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON)); + enrichKnowledgeHeaders(request, body); + return executeRequest(request); + } + + protected String executeKnowledgePut(String path, Object requestBody) throws WxErrorException { + String body = toBody(requestBody); + HttpPut request = new HttpPut(configStorage.getKnowledgeApiBaseUrl() + path); + request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON)); + enrichKnowledgeHeaders(request, body); + return executeRequest(request); + } + + protected String executeKnowledgeMultipartPost(String path, File file, String title, String description, String metadata) + throws WxErrorException { + HttpPost request = new HttpPost(configStorage.getKnowledgeApiBaseUrl() + path); + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.addBinaryBody("file", file, ContentType.DEFAULT_BINARY, file.getName()); + if (StringUtils.isNotBlank(title)) { + builder.addTextBody("title", title, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)); + } + if (StringUtils.isNotBlank(description)) { + builder.addTextBody("description", description, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)); + } + if (StringUtils.isNotBlank(metadata)) { + builder.addTextBody("metadata", metadata, ContentType.APPLICATION_JSON); + } + HttpEntity entity = builder.build(); + request.setEntity(entity); + if (entity.getContentType() != null) { + request.setHeader("Content-Type", entity.getContentType()); + } + enrichKnowledgeHeaders(request, ""); + return executeRequest(request); + } + + protected String executeKnowledgeDelete(String path) throws WxErrorException { + HttpUriRequestBase request = new HttpUriRequestBase("DELETE", URI.create(configStorage.getKnowledgeApiBaseUrl() + path)); + enrichKnowledgeHeaders(request, ""); + return executeRequest(request); + } + + private void enrichKnowledgeHeaders(HttpUriRequestBase request, String body) throws WxErrorException { + if (StringUtils.isBlank(configStorage.getAppid())) { + throw new WxErrorException("知识助理请求需要配置appid"); + } + if (StringUtils.isBlank(configStorage.getSecretKey())) { + throw new WxErrorException("知识助理请求需要配置secretKey"); + } + + String requestId = UUID.randomUUID().toString(); + long timestamp = System.currentTimeMillis() / 1000; + String nonce = randomNonce(); + String signature = WxAispeechSignUtil.calcKnowledgeSignature(configStorage.getSecretKey(), timestamp, nonce, + requestId, body); + + request.setHeader("X-APPID", configStorage.getAppid()); + request.setHeader("X-Request-ID", requestId); + request.setHeader("X-Timestamp", String.valueOf(timestamp)); + request.setHeader("X-Nonce", nonce); + request.setHeader("X-Signature", signature); + if (!request.containsHeader("Content-Type")) { + request.setHeader("Content-Type", ContentType.APPLICATION_JSON.getMimeType()); + } + } + + private String executeRequest(HttpUriRequestBase request) throws WxErrorException { + if (this.proxy != null) { + RequestConfig requestConfig = RequestConfig.custom().setProxy(this.proxy).build(); + request.setConfig(requestConfig); + } + + try (CloseableHttpResponse response = httpClient.execute(request)) { + int statusCode = response.getCode(); + HttpEntity entity = response.getEntity(); + String body = entity == null ? "" : EntityUtils.toString(entity, StandardCharsets.UTF_8); + if (statusCode >= 200 && statusCode < 300) { + return body; + } + + throw new WxErrorException(WxError.builder().errorCode(statusCode).errorMsg(body).build()); + } catch (IOException | ParseException e) { + throw toWxErrorException(e); + } + } + + protected T fromJson(String json, Class clazz) { + return GSON.fromJson(json, clazz); + } + + private String toBody(Object requestBody) { + if (requestBody == null) { + return "{}"; + } + if (requestBody instanceof String) { + return (String) requestBody; + } + return GSON.toJson(requestBody); + } + + private WxErrorException toWxErrorException(Exception e) { + if (e instanceof WxErrorException) { + return (WxErrorException) e; + } + return new WxErrorException(e); + } + + private String randomNonce() { + return UUID.randomUUID().toString().replace("-", "").substring(0, 16); + } +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java new file mode 100644 index 0000000000..24595b8b46 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java @@ -0,0 +1,29 @@ +package me.chanjar.weixin.aispeech.bean.dialog; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +@Data +public class AispeechApiResponse { + private Integer code; + private String msg; + @SerializedName("request_id") + private String requestId; + private T data; + + public Integer getCode() { + return code; + } + + public String getMsg() { + return msg; + } + + public String getRequestId() { + return requestId; + } + + public T getData() { + return data; + } +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java new file mode 100644 index 0000000000..a806fb368a --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java @@ -0,0 +1,33 @@ +package me.chanjar.weixin.aispeech.bean.dialog; + +import com.google.gson.JsonElement; +import java.util.List; +import lombok.Data; + +@Data +public class AsyncTaskResult { + private Integer state; + private String msg; + private Integer progress; + private Long start; + private Long end; + private String url; + private Integer totalCount; + private Integer successCount; + private Integer failCount; + private JsonElement successSkillInfo; + private List successSkillInfoList; + + @Data + public static class SkillInfo { + private Long id; + private String name; + private List intents; + } + + @Data + public static class IntentInfo { + private Long id; + private String name; + } +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java new file mode 100644 index 0000000000..3927461fc8 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java @@ -0,0 +1,13 @@ +package me.chanjar.weixin.aispeech.bean.dialog; + +import java.util.List; +import lombok.Data; + +@Data +public class BotIntent { + private String skill; + private String intent; + private Boolean disable; + private List questions; + private List answers; +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java new file mode 100644 index 0000000000..dd748957ff --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java @@ -0,0 +1,19 @@ +package me.chanjar.weixin.aispeech.bean.dialog; + +import com.google.gson.annotations.SerializedName; +import java.util.List; +import lombok.Data; + +@Data +public class DialogQueryRequest { + private String query; + private String env; + @SerializedName("first_priority_skills") + private List firstPrioritySkills; + @SerializedName("second_priority_skills") + private List secondPrioritySkills; + @SerializedName("user_name") + private String userName; + private String avatar; + private String userid; +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java new file mode 100644 index 0000000000..575628dc10 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java @@ -0,0 +1,47 @@ +package me.chanjar.weixin.aispeech.bean.dialog; + +import com.google.gson.JsonElement; +import com.google.gson.annotations.SerializedName; +import java.util.List; +import lombok.Data; + +@Data +public class DialogResult { + private String answer; + @SerializedName("answer_type") + private String answerType; + @SerializedName("skill_name") + private String skillName; + @SerializedName("intent_name") + private String intentName; + @SerializedName("msg_id") + private String msgId; + private List
    * 获取会话存档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