formFields;
+
+ /**
+ * 为保持向后兼容保留的 2 参数构造函数。
+ *
+ * 仅设置文件参数名和上传数据,额外表单字段将为 {@code null}。
+ *
+ * @param name 参数名,如:media
+ * @param data 上传数据
+ * @deprecated 请使用包含 formFields 参数的构造函数或静态工厂方法 {@link #fromFile(String, File)}、{@link #fromBytes(String, String, byte[])}
+ */
+ @Deprecated
+ public CommonUploadParam(@NotNull String name, @NotNull CommonUploadData data) {
+ this(name, data, null);
+ }
+
/**
* 从文件构造
*
@@ -43,7 +66,7 @@ public class CommonUploadParam implements Serializable {
*/
@SneakyThrows
public static CommonUploadParam fromFile(String name, File file) {
- return new CommonUploadParam(name, CommonUploadData.fromFile(file));
+ return new CommonUploadParam(name, CommonUploadData.fromFile(file), null);
}
/**
@@ -55,11 +78,32 @@ public static CommonUploadParam fromFile(String name, File file) {
*/
@SneakyThrows
public static CommonUploadParam fromBytes(String name, @Nullable String fileName, byte[] bytes) {
- return new CommonUploadParam(name, new CommonUploadData(fileName, new ByteArrayInputStream(bytes), bytes.length));
+ return new CommonUploadParam(name, new CommonUploadData(fileName, new ByteArrayInputStream(bytes), bytes.length), null);
+ }
+
+ /**
+ * 添加额外的表单字段
+ *
+ * @param fieldName 表单字段名
+ * @param fieldValue 表单字段值
+ * @return 当前对象,支持链式调用
+ */
+ public CommonUploadParam addFormField(String fieldName, String fieldValue) {
+ if (fieldName == null || fieldName.trim().isEmpty()) {
+ throw new IllegalArgumentException("表单字段名不能为空");
+ }
+ if (fieldValue == null) {
+ throw new IllegalArgumentException("表单字段值不能为null");
+ }
+ if (this.formFields == null) {
+ this.formFields = new HashMap<>();
+ }
+ this.formFields.put(fieldName, fieldValue);
+ return this;
}
@Override
public String toString() {
- return String.format("{name:%s, data:%s}", name, data);
+ return String.format("{name:%s, data:%s, formFields:%s}", name, data, formFields);
}
}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java
index ffe9b5e3ea..3f380543b0 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java
@@ -838,6 +838,34 @@ public enum WxMaErrorMsgEnum {
*/
CODE_89424(89424, "授权次数到达上限"),
+ /**
+ * 微信小程序虚拟支付错误码
+ *
+ * @see 虚拟支付 API 文档
+ */
+ CODE_268490001(268490001, "openid错误"),
+ CODE_268490002(268490002, "请求参数字段错误,具体看errmsg"),
+ CODE_268490003(268490003, "签名错误"),
+ CODE_268490004(268490004, "重复操作(赠送和代币支付和充值广告金相关接口会返回,表示之前的操作已经成功)"),
+ CODE_268490005(268490005, "订单已经通过cancel_currency_pay接口退款,不支持再退款"),
+ CODE_268490006(268490006, "代币的退款/支付操作金额不足"),
+ CODE_268490007(268490007, "图片或文字存在敏感内容,禁止使用"),
+ CODE_268490008(268490008, "代币未发布,不允许进行代币操作"),
+ CODE_268490009(268490009, "用户session_key不存在或已过期,请重新登录"),
+ CODE_268490011(268490011, "数据生成中,请稍后调用本接口获取"),
+ CODE_268490012(268490012, "批量任务运行中,请等待完成后才能再次运行"),
+ CODE_268490013(268490013, "禁止对核销状态的单进行退款"),
+ CODE_268490014(268490014, "退款操作进行中,稍后可以使用相同参数重试"),
+ CODE_268490015(268490015, "频率限制"),
+ CODE_268490016(268490016, "退款的left_fee字段与实际不符,请通过query_order接口查询确认"),
+ CODE_268490018(268490018, "广告金充值账户行业id不匹配"),
+ CODE_268490019(268490019, "广告金充值账户id已绑定其他appid"),
+ CODE_268490020(268490020, "广告金充值账户主体名称错误"),
+ CODE_268490021(268490021, "账户未完成进件"),
+ CODE_268490022(268490022, "广告金充值账户无效"),
+ CODE_268490023(268490023, "广告金余额不足"),
+ CODE_268490024(268490024, "广告金充值金额必须大于0"),
+
;
private final int code;
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorApacheImpl.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorApacheImpl.java
index 7f19241cdb..dba92e27da 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorApacheImpl.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorApacheImpl.java
@@ -44,11 +44,19 @@ public String execute(String uri, CommonUploadParam param, WxType wxType) throws
if (param != null) {
CommonUploadData data = param.getData();
InnerStreamBody part = new InnerStreamBody(data.getInputStream(), ContentType.DEFAULT_BINARY, data.getFileName(), data.getLength());
- HttpEntity entity = MultipartEntityBuilder
+ MultipartEntityBuilder entityBuilder = MultipartEntityBuilder
.create()
.addPart(param.getName(), part)
- .setMode(HttpMultipartMode.RFC6532)
- .build();
+ .setMode(HttpMultipartMode.RFC6532);
+
+ // 添加额外的表单字段
+ if (param.getFormFields() != null && !param.getFormFields().isEmpty()) {
+ for (java.util.Map.Entry entry : param.getFormFields().entrySet()) {
+ entityBuilder.addTextBody(entry.getKey(), entry.getValue(), ContentType.TEXT_PLAIN.withCharset("UTF-8"));
+ }
+ }
+
+ HttpEntity entity = entityBuilder.build();
httpPost.setEntity(entity);
}
String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE);
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorHttpComponentsImpl.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorHttpComponentsImpl.java
index f79eaa49b8..f79e4cd96f 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorHttpComponentsImpl.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorHttpComponentsImpl.java
@@ -41,11 +41,19 @@ public String execute(String uri, CommonUploadParam param, WxType wxType) throws
if (param != null) {
CommonUploadData data = param.getData();
InnerStreamBody part = new InnerStreamBody(data.getInputStream(), ContentType.DEFAULT_BINARY, data.getFileName(), data.getLength());
- HttpEntity entity = MultipartEntityBuilder
+ MultipartEntityBuilder entityBuilder = MultipartEntityBuilder
.create()
.addPart(param.getName(), part)
- .setMode(HttpMultipartMode.EXTENDED)
- .build();
+ .setMode(HttpMultipartMode.EXTENDED);
+
+ // 添加额外的表单字段
+ if (param.getFormFields() != null && !param.getFormFields().isEmpty()) {
+ for (java.util.Map.Entry entry : param.getFormFields().entrySet()) {
+ entityBuilder.addTextBody(entry.getKey(), entry.getValue(), ContentType.TEXT_PLAIN.withCharset("UTF-8"));
+ }
+ }
+
+ HttpEntity entity = entityBuilder.build();
httpPost.setEntity(entity);
}
String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE);
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorJoddHttpImpl.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorJoddHttpImpl.java
index 36e8660f77..182820d076 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorJoddHttpImpl.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorJoddHttpImpl.java
@@ -39,6 +39,14 @@ public String execute(String uri, CommonUploadParam param, WxType wxType) throws
}
request.withConnectionProvider(requestHttp.getRequestHttpClient());
request.form(param.getName(), new CommonUploadParamToUploadableAdapter(param.getData()));
+
+ // 添加额外的表单字段
+ if (param.getFormFields() != null && !param.getFormFields().isEmpty()) {
+ for (java.util.Map.Entry entry : param.getFormFields().entrySet()) {
+ request.form(entry.getKey(), entry.getValue());
+ }
+ }
+
HttpResponse response = request.send();
response.charset(StandardCharsets.UTF_8.name());
String responseContent = response.bodyText();
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorOkHttpImpl.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorOkHttpImpl.java
index 40a4622b89..6a0343980f 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorOkHttpImpl.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorOkHttpImpl.java
@@ -31,10 +31,18 @@ public CommonUploadRequestExecutorOkHttpImpl(RequestHttp entry : param.getFormFields().entrySet()) {
+ bodyBuilder.addFormDataPart(entry.getKey(), entry.getValue());
+ }
+ }
+
+ RequestBody body = bodyBuilder.build();
Request request = new Request.Builder().url(uri).post(body).build();
try (Response response = requestHttp.getRequestHttpClient().newCall(request).execute()) {
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamCDataListConverter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamCDataListConverter.java
new file mode 100644
index 0000000000..0b55a9c037
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamCDataListConverter.java
@@ -0,0 +1,54 @@
+package me.chanjar.weixin.common.util.xml;
+
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+
+/**
+ * 兼容两种格式的字符串列表转换器:
+ *
+ * - 旧格式(4.8.0之前):<MemChangeList><![CDATA[id1,id2]]></MemChangeList>
+ * - 新格式(4.8.0起):<MemChangeList><Item><![CDATA[id1]]></Item></MemChangeList>
+ *
+ * 解析结果统一为逗号分隔的字符串。
+ */
+public class XStreamCDataListConverter implements Converter {
+
+ @Override
+ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
+ if (source != null) {
+ writer.setValue("");
+ }
+ }
+
+ @Override
+ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+ if (reader.hasMoreChildren()) {
+ // 新格式:含有 - 子元素
+ StringBuilder sb = new StringBuilder();
+ while (reader.hasMoreChildren()) {
+ reader.moveDown();
+ String value = reader.getValue();
+ if (value != null && !value.isEmpty()) {
+ if (sb.length() > 0) {
+ sb.append(",");
+ }
+ sb.append(value);
+ }
+ reader.moveUp();
+ }
+ return sb.length() > 0 ? sb.toString() : null;
+ } else {
+ // 旧格式:直接 CDATA 文本
+ String value = reader.getValue();
+ return (value != null && !value.isEmpty()) ? value : null;
+ }
+ }
+
+ @Override
+ public boolean canConvert(Class type) {
+ return type == String.class;
+ }
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/CommonUploadParamTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/CommonUploadParamTest.java
new file mode 100644
index 0000000000..05c8b379d3
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/CommonUploadParamTest.java
@@ -0,0 +1,119 @@
+package me.chanjar.weixin.common.bean;
+
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * CommonUploadParam 单元测试
+ *
+ * @author Binary Wang
+ */
+@Test
+public class CommonUploadParamTest {
+
+ @Test
+ public void testFromFile() {
+ File file = new File("test.txt");
+ CommonUploadParam param = CommonUploadParam.fromFile("media", file);
+
+ Assert.assertNotNull(param);
+ Assert.assertEquals(param.getName(), "media");
+ Assert.assertNotNull(param.getData());
+ Assert.assertNull(param.getFormFields());
+ }
+
+ @Test
+ public void testFromBytes() {
+ byte[] bytes = "test content".getBytes();
+ CommonUploadParam param = CommonUploadParam.fromBytes("media", "test.txt", bytes);
+
+ Assert.assertNotNull(param);
+ Assert.assertEquals(param.getName(), "media");
+ Assert.assertNotNull(param.getData());
+ Assert.assertEquals(param.getData().getFileName(), "test.txt");
+ Assert.assertNull(param.getFormFields());
+ }
+
+ @Test
+ public void testAddFormField() {
+ File file = new File("test.txt");
+ CommonUploadParam param = CommonUploadParam.fromFile("media", file);
+
+ // 添加单个表单字段
+ param.addFormField("title", "测试标题");
+
+ Assert.assertNotNull(param.getFormFields());
+ Assert.assertEquals(param.getFormFields().size(), 1);
+ Assert.assertEquals(param.getFormFields().get("title"), "测试标题");
+
+ // 添加多个表单字段
+ param.addFormField("introduction", "测试介绍");
+
+ Assert.assertEquals(param.getFormFields().size(), 2);
+ Assert.assertEquals(param.getFormFields().get("introduction"), "测试介绍");
+ }
+
+ @Test
+ public void testAddFormFieldChaining() {
+ File file = new File("test.txt");
+ CommonUploadParam param = CommonUploadParam.fromFile("media", file)
+ .addFormField("title", "测试标题")
+ .addFormField("introduction", "测试介绍");
+
+ Assert.assertNotNull(param.getFormFields());
+ Assert.assertEquals(param.getFormFields().size(), 2);
+ Assert.assertEquals(param.getFormFields().get("title"), "测试标题");
+ Assert.assertEquals(param.getFormFields().get("introduction"), "测试介绍");
+ }
+
+ @Test
+ public void testConstructorWithFormFields() {
+ CommonUploadData data = new CommonUploadData("test.txt", null, 0);
+ Map formFields = new HashMap<>();
+ formFields.put("title", "测试标题");
+ formFields.put("introduction", "测试介绍");
+
+ CommonUploadParam param = new CommonUploadParam("media", data, formFields);
+
+ Assert.assertNotNull(param.getFormFields());
+ Assert.assertEquals(param.getFormFields().size(), 2);
+ Assert.assertEquals(param.getFormFields().get("title"), "测试标题");
+ Assert.assertEquals(param.getFormFields().get("introduction"), "测试介绍");
+ }
+
+ @Test
+ public void testToString() {
+ File file = new File("test.txt");
+ CommonUploadParam param = CommonUploadParam.fromFile("media", file)
+ .addFormField("title", "测试标题");
+
+ String str = param.toString();
+ Assert.assertTrue(str.contains("name:media"));
+ Assert.assertTrue(str.contains("formFields:"));
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void testAddFormFieldWithNullFieldName() {
+ File file = new File("test.txt");
+ CommonUploadParam param = CommonUploadParam.fromFile("media", file);
+ param.addFormField(null, "value");
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void testAddFormFieldWithEmptyFieldName() {
+ File file = new File("test.txt");
+ CommonUploadParam param = CommonUploadParam.fromFile("media", file);
+ param.addFormField("", "value");
+ }
+
+ @Test(expectedExceptions = IllegalArgumentException.class)
+ public void testAddFormFieldWithNullFieldValue() {
+ File file = new File("test.txt");
+ CommonUploadParam param = CommonUploadParam.fromFile("media", file);
+ param.addFormField("fieldName", null);
+ }
+}
diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnumTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnumTest.java
new file mode 100644
index 0000000000..66147bb7ec
--- /dev/null
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnumTest.java
@@ -0,0 +1,62 @@
+package me.chanjar.weixin.common.error;
+
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNull;
+
+/**
+ * 微信小程序错误码枚举测试
+ *
+ * @author GitHub Copilot
+ */
+@Test
+public class WxMaErrorMsgEnumTest {
+
+ public void testFindMsgByCodeForExistingCode() {
+ String msg = WxMaErrorMsgEnum.findMsgByCode(40001);
+ assertNotNull(msg);
+ }
+
+ public void testFindMsgByCodeForNonExistingCode() {
+ String msg = WxMaErrorMsgEnum.findMsgByCode(999999);
+ assertNull(msg);
+ }
+
+ /**
+ * 验证微信小程序虚拟支付错误码
+ */
+ public void testVirtualPaymentErrorCodes() {
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490001), "openid错误");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490002), "请求参数字段错误,具体看errmsg");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490003), "签名错误");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490004), "重复操作(赠送和代币支付和充值广告金相关接口会返回,表示之前的操作已经成功)");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490005), "订单已经通过cancel_currency_pay接口退款,不支持再退款");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490006), "代币的退款/支付操作金额不足");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490007), "图片或文字存在敏感内容,禁止使用");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490008), "代币未发布,不允许进行代币操作");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490009), "用户session_key不存在或已过期,请重新登录");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490011), "数据生成中,请稍后调用本接口获取");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490012), "批量任务运行中,请等待完成后才能再次运行");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490013), "禁止对核销状态的单进行退款");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490014), "退款操作进行中,稍后可以使用相同参数重试");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490015), "频率限制");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490016), "退款的left_fee字段与实际不符,请通过query_order接口查询确认");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490018), "广告金充值账户行业id不匹配");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490019), "广告金充值账户id已绑定其他appid");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490020), "广告金充值账户主体名称错误");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490021), "账户未完成进件");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490022), "广告金充值账户无效");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490023), "广告金余额不足");
+ assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490024), "广告金充值金额必须大于0");
+ }
+
+ /**
+ * 验证虚拟支付错误码中不存在的编号(如268490010、268490017)返回null
+ */
+ public void testVirtualPaymentMissingCodes() {
+ assertNull(WxMaErrorMsgEnum.findMsgByCode(268490010));
+ assertNull(WxMaErrorMsgEnum.findMsgByCode(268490017));
+ }
+}
diff --git a/weixin-java-cp/pom.xml b/weixin-java-cp/pom.xml
index 922d4f6b84..4d5d172ed2 100644
--- a/weixin-java-cp/pom.xml
+++ b/weixin-java-cp/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.8.1.B
+ 4.8.2.B
weixin-java-cp
@@ -96,7 +96,6 @@
org.bouncycastle
bcprov-jdk18on
- 1.80
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpHrService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpHrService.java
new file mode 100644
index 0000000000..d9d6ed0129
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpHrService.java
@@ -0,0 +1,68 @@
+package me.chanjar.weixin.cp.api;
+
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldData;
+import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldDataResp;
+import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldInfoResp;
+
+import java.util.List;
+
+/**
+ * 人事助手相关接口.
+ * 官方文档:...
+ *
+ * @author copilot
+ */
+public interface WxCpHrService {
+
+ /**
+ * 获取员工档案字段信息.
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址:...
+ * 权限说明:
+ * 需要配置人事助手的secret,调用接口前需给对应成员赋予人事小助手应用的权限。
+ *
+ * @param fields 指定字段key列表,不填则返回全部字段
+ * @return 字段信息响应 wx cp hr employee field info resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpHrEmployeeFieldInfoResp getFieldInfo(List fields) throws WxErrorException;
+
+ /**
+ * 获取员工档案数据.
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址:...
+ * 权限说明:
+ * 需要配置人事助手的secret,调用接口前需给对应成员赋予人事小助手应用的权限。
+ *
+ * @param userid 员工userid
+ * @param fields 指定字段key列表
+ * @return 员工档案数据响应 wx cp hr employee field data resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpHrEmployeeFieldDataResp getEmployeeFieldInfo(String userid, List fields) throws WxErrorException;
+
+ /**
+ * 获取员工档案数据.
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址:...
+ */
+ WxCpHrEmployeeFieldDataResp getEmployeeFieldInfo(String userid, boolean getAll, List fields) throws WxErrorException;
+
+ /**
+ * 更新员工档案数据.
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址:...
+ * 权限说明:
+ * 需要配置人事助手的secret,调用接口前需给对应成员赋予人事小助手应用的权限。
+ *
+ * @param userid 员工userid
+ * @param fieldList 字段数据列表
+ * @throws WxErrorException the wx error exception
+ */
+ void updateEmployeeFieldInfo(String userid, List fieldList) throws WxErrorException;
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java
index b754e32b7e..5e8811953f 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java
@@ -215,4 +215,20 @@ void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @
*/
WxCpAgreeInfo checkSingleAgree(@NonNull WxCpCheckAgreeRequest checkAgreeRequest) throws WxErrorException;
+ /**
+ * 关闭当前线程持有的SDK,释放本地资源。
+ *
+ * 在线程池场景下,任务结束后必须在 finally 块中调用此方法,防止SDK实例随线程复用而泄漏。
+ * 独立线程或一次性任务也建议调用,以主动释放原生资源。
+ */
+ void closeThreadLocalSdk();
+
+ /**
+ * 关闭所有会话存档SDK实例,释放全部原生资源。
+ *
+ * 适用于应用关闭阶段(如 Spring Bean 销毁阶段 {@code @PreDestroy} 或 Shutdown Hook)。
+ * 调用后,所有线程的SDK均不可再使用。
+ */
+ void closeAllSdks();
+
}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaWeDocService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaWeDocService.java
index d63d32694a..712bc2a89c 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaWeDocService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaWeDocService.java
@@ -5,6 +5,10 @@
import me.chanjar.weixin.cp.bean.WxCpBaseResp;
import me.chanjar.weixin.cp.bean.oa.doc.*;
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+
/**
* 企业微信文档相关接口.
* 文档
@@ -79,6 +83,89 @@ public interface WxCpOaWeDocService {
*/
WxCpDocShare docShare(@NonNull String docId) throws WxErrorException;
+ /**
+ * 分享文档/收集表
+ * 该接口用于获取文档或收集表的分享链接。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/doc_share?access_token=ACCESS_TOKEN
+ *
+ * @param request 分享请求,docid/formid 二选一
+ * @return url 文档分享链接
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocShare docShare(@NonNull WxCpDocShareRequest request) throws WxErrorException;
+
+ /**
+ * 获取文档权限信息
+ * 该接口用于获取文档、表格、智能表格的查看规则、文档通知范围及权限、安全设置信息。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/doc_get_auth?access_token=ACCESS_TOKEN
+ *
+ * @param docId 文档docid
+ * @return 文档权限信息
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocAuthInfo docGetAuth(@NonNull String docId) throws WxErrorException;
+
+ /**
+ * 修改文档查看规则
+ * 该接口用于修改文档、表格、智能表格查看规则。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/mod_doc_join_rule?access_token=ACCESS_TOKEN
+ *
+ * @param request 修改文档查看规则请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp docModifyJoinRule(@NonNull WxCpDocModifyJoinRuleRequest request) throws WxErrorException;
+
+ /**
+ * 修改文档通知范围及权限
+ * 该接口用于修改文档、表格、智能表格通知范围列表。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/mod_doc_member?access_token=ACCESS_TOKEN
+ *
+ * @param request 修改文档通知范围及权限请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp docModifyMember(@NonNull WxCpDocModifyMemberRequest request) throws WxErrorException;
+
+ /**
+ * 修改文档安全设置
+ * 该接口用于修改文档、表格、智能表格的安全设置。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/mod_doc_safty_setting?access_token=ACCESS_TOKEN
+ *
+ * @param request 修改文档安全设置请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp docModifySafetySetting(
+ @NonNull WxCpDocModifySafetySettingRequest request
+ ) throws WxErrorException;
+
+ /**
+ * @deprecated Use {@link #docModifySafetySetting(WxCpDocModifySafetySettingRequest)} instead.
+ */
+ @Deprecated
+ default WxCpBaseResp docModifySaftySetting(
+ @NonNull WxCpDocModifySaftySettingRequest request
+ ) throws WxErrorException {
+ WxCpDocModifySafetySettingRequest newReq =
+ WxCpDocModifySafetySettingRequest.builder()
+ .docId(request.getDocId())
+ .enableReadonlyCopy(request.getEnableReadonlyCopy())
+ .watermark(request.getWatermark())
+ .build();
+ return docModifySafetySetting(newReq);
+ }
+
/**
* 编辑表格内容
* 该接口可以对一个在线表格批量执行多个更新操作
@@ -127,4 +214,330 @@ public interface WxCpOaWeDocService {
*/
WxCpDocSheetData getSheetRangeData(@NonNull WxCpDocSheetGetDataRequest request) throws WxErrorException;
+ /**
+ * 获取文档数据
+ * 该接口用于获取在线文档内容数据。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_doc_data?access_token=ACCESS_TOKEN
+ *
+ * @param request 获取文档数据请求参数
+ * @return 文档内容数据
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocData docGetData(@NonNull WxCpDocGetDataRequest request) throws WxErrorException;
+
+ /**
+ * 编辑文档内容
+ * 该接口用于编辑在线文档内容。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/mod_doc?access_token=ACCESS_TOKEN
+ *
+ * @param request 编辑文档内容请求参数
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp docModify(@NonNull WxCpDocModifyRequest request) throws WxErrorException;
+
+ /**
+ * 上传文档图片
+ * 该接口用于上传在线文档编辑时使用的图片资源。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/upload_doc_image?access_token=ACCESS_TOKEN
+ *
+ * @param file 图片文件
+ * @return 上传结果
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocImageUploadResult docUploadImage(@NonNull File file) throws WxErrorException;
+
+ /**
+ * 添加文档高级功能账号
+ * 该接口用于为在线文档添加高级功能账号。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/add_admin?access_token=ACCESS_TOKEN
+ *
+ * @param request 文档高级功能账号请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp docAddAdmin(@NonNull WxCpDocAdminRequest request) throws WxErrorException;
+
+ /**
+ * 删除文档高级功能账号
+ * 该接口用于删除在线文档的高级功能账号。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/del_admin?access_token=ACCESS_TOKEN
+ *
+ * @param request 文档高级功能账号请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp docDeleteAdmin(@NonNull WxCpDocAdminRequest request) throws WxErrorException;
+
+ /**
+ * 获取文档高级功能账号列表
+ * 该接口用于获取在线文档的高级功能账号列表。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_admin_list?access_token=ACCESS_TOKEN
+ *
+ * @param docId 文档 docid
+ * @return 文档高级功能账号列表
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocAdminListResult docGetAdminList(@NonNull String docId) throws WxErrorException;
+
+ /**
+ * 获取智能表格内容权限
+ * 该接口用于获取智能表格字段/记录等内容权限信息。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/smartsheet/get_sheet_auth?access_token=ACCESS_TOKEN
+ *
+ * @param request 智能表格内容权限请求
+ * @return 智能表格内容权限
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetAuth smartSheetGetAuth(@NonNull WxCpDocSmartSheetAuthRequest request) throws WxErrorException;
+
+ /**
+ * 修改智能表格内容权限
+ * 该接口用于修改智能表格字段/记录等内容权限信息。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/smartsheet/mod_sheet_auth?access_token=ACCESS_TOKEN
+ *
+ * @param request 修改智能表格内容权限请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetModifyAuth(@NonNull WxCpDocSmartSheetModifyAuthRequest request) throws WxErrorException;
+
+ /**
+ * 获取智能表格工作表信息.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格工作表信息
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetGetSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 新增智能表格工作表.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格工作表信息
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetAddSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 删除智能表格工作表.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetDeleteSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 更新智能表格工作表.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetUpdateSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 获取智能表格视图.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格视图
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetGetViews(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 新增智能表格视图.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格视图
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetAddView(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 删除智能表格视图.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetDeleteViews(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 更新智能表格视图.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetUpdateView(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 获取智能表格字段.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格字段
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetGetFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 新增智能表格字段.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格字段
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetAddFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 删除智能表格字段.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetDeleteFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 更新智能表格字段.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetUpdateFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 获取智能表格记录.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格记录
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetGetRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 新增智能表格记录.
+ *
+ * @param request 智能表格请求
+ * @return 智能表格记录
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpDocSmartSheetResult smartSheetAddRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 删除智能表格记录.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetDeleteRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 更新智能表格记录.
+ *
+ * @param request 智能表格请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp smartSheetUpdateRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException;
+
+ /**
+ * 创建收集表
+ * 该接口用于创建收集表。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/create_collect?access_token=ACCESS_TOKEN
+ *
+ * @param request 创建收集表请求
+ * @return 创建收集表结果
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpFormCreateResult formCreate(@NonNull WxCpFormCreateRequest request) throws WxErrorException;
+
+ /**
+ * 编辑收集表
+ * 该接口用于编辑收集表。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/modify_collect?access_token=ACCESS_TOKEN
+ *
+ * @param request 编辑收集表请求
+ * @return wx cp base resp
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpBaseResp formModify(@NonNull WxCpFormModifyRequest request) throws WxErrorException;
+
+ /**
+ * 获取收集表信息
+ * 该接口用于读取收集表的信息。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_form_info?access_token=ACCESS_TOKEN
+ *
+ * @param formId 收集表id
+ * @return 收集表信息
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpFormInfoResult formInfo(@NonNull String formId) throws WxErrorException;
+
+ /**
+ * 获取收集表统计信息
+ * 该接口用于获取收集表的统计信息、已回答成员列表和未回答成员列表。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_form_statistic?access_token=ACCESS_TOKEN
+ *
+ * @param requests 收集表统计请求数组
+ * @return 收集表统计结果(包含 statistic_list)
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpFormStatisticResult formStatistic(@NonNull List requests) throws WxErrorException;
+
+ /**
+ * 单个收集表统计查询的兼容封装,底层仍按官方数组请求发送。
+ *
+ * @param request 收集表统计请求
+ * @return 收集表统计信息
+ * @throws WxErrorException the wx error exception
+ */
+ default WxCpFormStatistic formStatistic(@NonNull WxCpFormStatisticRequest request) throws WxErrorException {
+ WxCpFormStatisticResult result = formStatistic(Collections.singletonList(request));
+ List list = result == null ? null : result.getStatisticList();
+ return list == null || list.isEmpty() ? null : list.get(0);
+ }
+
+ /**
+ * 获取收集表答案
+ * 该接口用于读取收集表的答案。
+ *
+ * 请求方式:POST(HTTPS)
+ * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_form_answer?access_token=ACCESS_TOKEN
+ *
+ * @param request 收集表答案请求
+ * @return 收集表答案
+ * @throws WxErrorException the wx error exception
+ */
+ WxCpFormAnswer formAnswer(@NonNull WxCpFormAnswerRequest request) throws WxErrorException;
+
}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java
index 76012a2812..f66acc0252 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java
@@ -57,6 +57,19 @@ public interface WxCpService extends WxService {
*/
String getAccessToken(boolean forceRefresh) throws WxErrorException;
+ /**
+ *
+ * 获取会话存档access_token,本方法线程安全
+ * 会话存档相关接口需要使用会话存档secret获取单独的access_token
+ * 详情请见: https://developer.work.weixin.qq.com/document/path/91782
+ *
+ *
+ * @param forceRefresh 强制刷新
+ * @return 会话存档专用的access token
+ * @throws WxErrorException the wx error exception
+ */
+ String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException;
+
/**
* 获得jsapi_ticket,不强制刷新jsapi_ticket
*
@@ -194,6 +207,19 @@ public interface WxCpService extends WxService {
*/
String postWithoutToken(String url, String postData) throws WxErrorException;
+ /**
+ *
+ * 使用会话存档access token发起post请求
+ * 会话存档相关API需要使用会话存档专用的secret获取独立的access token
+ *
+ *
+ * @param url 接口地址
+ * @param postData 请求body字符串
+ * @return the string
+ * @throws WxErrorException the wx error exception
+ */
+ String postForMsgAudit(String url, String postData) throws WxErrorException;
+
/**
*
* Service没有实现某个API的时候,可以用这个,
@@ -455,6 +481,13 @@ public interface WxCpService extends WxService {
*/
WxCpOaWeDriveService getOaWeDriveService();
+ /**
+ * 获取OA效率工具 文档的服务类对象
+ *
+ * @return oa we doc service
+ */
+ WxCpOaWeDocService getOaWeDocService();
+
/**
* 获取会话存档相关接口的服务类对象
*
@@ -594,4 +627,11 @@ public interface WxCpService extends WxService {
* @return 智能机器人服务 intelligent robot service
*/
WxCpIntelligentRobotService getIntelligentRobotService();
+
+ /**
+ * 获取人事助手服务
+ *
+ * @return 人事助手服务 hr service
+ */
+ WxCpHrService getHrService();
}
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 bc18c9bc7a..7c72cb9a8c 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
@@ -59,6 +59,7 @@ public abstract class BaseWxCpServiceImpl implements WxCpService, RequestH
private final WxCpLivingService livingService = new WxCpLivingServiceImpl(this);
private final WxCpOaAgentService oaAgentService = new WxCpOaAgentServiceImpl(this);
private final WxCpOaWeDriveService oaWeDriveService = new WxCpOaWeDriveServiceImpl(this);
+ private final WxCpOaWeDocService oaWeDocService = new WxCpOaWeDocServiceImpl(this);
private final WxCpMsgAuditService msgAuditService = new WxCpMsgAuditServiceImpl(this);
private final WxCpTaskCardService taskCardService = new WxCpTaskCardServiceImpl(this);
private final WxCpExternalContactService externalContactService = new WxCpExternalContactServiceImpl(this);
@@ -75,6 +76,7 @@ public abstract class BaseWxCpServiceImpl implements WxCpService, RequestH
private final WxCpMeetingService meetingService = new WxCpMeetingServiceImpl(this);
private final WxCpCorpGroupService corpGroupService = new WxCpCorpGroupServiceImpl(this);
private final WxCpIntelligentRobotService intelligentRobotService = new WxCpIntelligentRobotServiceImpl(this);
+ private final WxCpHrService hrService = new WxCpHrServiceImpl(this);
/**
* 全局的是否正在刷新access token的锁.
@@ -301,6 +303,16 @@ public String postWithoutToken(String url, String postData) throws WxErrorExcept
return this.executeNormal(SimplePostRequestExecutor.create(this), url, postData);
}
+ @Override
+ public String postForMsgAudit(String url, String postData) throws WxErrorException {
+ // 获取会话存档专用的access token
+ String msgAuditAccessToken = getMsgAuditAccessToken(false);
+ // 拼接access_token参数
+ String urlWithToken = url + (url.contains("?") ? "&" : "?") + "access_token=" + msgAuditAccessToken;
+ // 使用executeNormal方法,不自动添加token
+ return this.executeNormal(SimplePostRequestExecutor.create(this), urlWithToken, postData);
+ }
+
/**
* 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求.
*/
@@ -584,6 +596,11 @@ public WxCpOaWeDriveService getOaWeDriveService() {
return oaWeDriveService;
}
+ @Override
+ public WxCpOaWeDocService getOaWeDocService() {
+ return oaWeDocService;
+ }
+
@Override
public WxCpMsgAuditService getMsgAuditService() {
return msgAuditService;
@@ -708,4 +725,9 @@ public WxCpCorpGroupService getCorpGroupService() {
public WxCpIntelligentRobotService getIntelligentRobotService() {
return this.intelligentRobotService;
}
+
+ @Override
+ public WxCpHrService getHrService() {
+ return this.hrService;
+ }
}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpHrServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpHrServiceImpl.java
new file mode 100644
index 0000000000..df71643d4c
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpHrServiceImpl.java
@@ -0,0 +1,80 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import com.google.gson.JsonObject;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.api.WxCpHrService;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldData;
+import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldDataResp;
+import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldInfoResp;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.util.List;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Hr.*;
+
+/**
+ * 人事助手相关接口实现类.
+ * 官方文档:https://developer.work.weixin.qq.com/document/path/99132
+ *
+ * @author leejoker created on 2024-01-01
+ */
+@RequiredArgsConstructor
+public class WxCpHrServiceImpl implements WxCpHrService {
+
+ private final WxCpService cpService;
+
+ @Override
+ public WxCpHrEmployeeFieldInfoResp getFieldInfo(List fields) throws WxErrorException {
+ JsonObject jsonObject = new JsonObject();
+ if (fields != null && !fields.isEmpty()) {
+ jsonObject.add("fields", WxCpGsonBuilder.create().toJsonTree(fields));
+ }
+ String response = this.cpService.post(
+ this.cpService.getWxCpConfigStorage().getApiUrl(GET_FIELD_INFO),
+ jsonObject.toString()
+ );
+ return WxCpHrEmployeeFieldInfoResp.fromJson(response);
+ }
+
+ @Override
+ public WxCpHrEmployeeFieldDataResp getEmployeeFieldInfo(String userid, List fields) throws WxErrorException {
+ return getEmployeeFieldInfo(userid, false, fields);
+ }
+
+ @Override
+ public WxCpHrEmployeeFieldDataResp getEmployeeFieldInfo(String userid, boolean getAll, List fields) throws WxErrorException {
+ if (userid == null || userid.trim().isEmpty()) {
+ throw new IllegalArgumentException("userid 不能为空");
+ }
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("userid", userid);
+ jsonObject.addProperty("get_all", getAll);
+ if (fields != null && !fields.isEmpty()) {
+ jsonObject.add("fields", WxCpGsonBuilder.create().toJsonTree(fields));
+ }
+ String response = this.cpService.post(
+ this.cpService.getWxCpConfigStorage().getApiUrl(GET_EMPLOYEE_FIELD_INFO),
+ jsonObject.toString()
+ );
+ return WxCpHrEmployeeFieldDataResp.fromJson(response);
+ }
+
+ @Override
+ public void updateEmployeeFieldInfo(String userid, List fieldList) throws WxErrorException {
+ if (userid == null || userid.trim().isEmpty()) {
+ throw new IllegalArgumentException("userid 不能为空");
+ }
+ if (fieldList == null || fieldList.isEmpty()) {
+ throw new IllegalArgumentException("fieldList 不能为空");
+ }
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("userid", userid);
+ jsonObject.add("field_list", WxCpGsonBuilder.create().toJsonTree(fieldList));
+ this.cpService.post(
+ this.cpService.getWxCpConfigStorage().getApiUrl(UPDATE_EMPLOYEE_FIELD_INFO),
+ jsonObject.toString()
+ );
+ }
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java
index 63dc7ac007..be6588bc7b 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java
@@ -23,6 +23,8 @@
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.MsgAudit.*;
@@ -37,16 +39,17 @@
public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
private final WxCpService cpService;
- /**
- * SDK初始化有效期,根据企微文档为7200秒
- */
- private static final int SDK_EXPIRES_TIME = 7200;
+ /** 每个线程持有独立 SDK 实例,懒初始化,线程内跨调用复用 */
+ private final ThreadLocal threadLocalSdk = new ThreadLocal<>();
+
+ /** 跟踪所有已创建的 SDK,用于 closeAllSdks() 统一清理 */
+ private final Set managedSdks = ConcurrentHashMap.newKeySet();
@Override
public WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, String passwd,
@NonNull long timeout) throws Exception {
- // 获取或初始化SDK
- long sdk = this.initSdk();
+ // 旧版 API:每次调用创建新 SDK,由调用方负责通过 Finance.DestroySdk(chatDatas.getSdk()) 释放
+ long sdk = this.createSdk();
long slice = Finance.NewSlice();
long ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice);
@@ -68,23 +71,39 @@ public WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, S
}
/**
- * 获取或初始化SDK,如果SDK已过期则重新初始化
+ * 获取当前线程的 SDK,不存在则初始化。
+ * SDK 在线程内跨调用复用,无需每次重新初始化。
*
* @return sdk id
* @throws WxErrorException 初始化失败时抛出异常
*/
- private synchronized long initSdk() throws WxErrorException {
- WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage();
-
- // 检查SDK是否已缓存且未过期
- if (!configStorage.isMsgAuditSdkExpired()) {
- long cachedSdk = configStorage.getMsgAuditSdk();
- if (cachedSdk > 0) {
- return cachedSdk;
+ private long getOrInitThreadLocalSdk() throws WxErrorException {
+ Long sdk = threadLocalSdk.get();
+ if (sdk != null && sdk > 0) {
+ // 校验句柄是否仍受管理:closeAllSdks() 后其他线程 ThreadLocal 可能保留已销毁的 id
+ if (managedSdks.contains(sdk)) {
+ return sdk;
}
+ log.warn("线程 [{}] 发现已失效的会话存档SDK句柄 sdk={},请检查调用逻辑", Thread.currentThread().getName(), sdk);
+ threadLocalSdk.remove();
+ throw new WxErrorException("线程 [" + Thread.currentThread().getName() + "] 获取会话存档SDK失败,请检查是否已调用 closeAllSdks()");
}
+ long newSdk = createSdk();
+ threadLocalSdk.set(newSdk);
+ managedSdks.add(newSdk);
+ log.info("线程 [{}] 初始化会话存档SDK成功,sdk={}", Thread.currentThread().getName(), newSdk);
+ return newSdk;
+ }
+
+ /**
+ * 创建并初始化一个新的会话存档 SDK 实例。
+ * 通常通过 {@link #getOrInitThreadLocalSdk()} 间接调用以复用 ThreadLocal 中的实例;
+ * 旧版直接暴露 sdk 的 API(如 {@link #getChatDatas})也会直接调用本方法,此时 SDK 由调用方自行管理。
+ * Finance.loadingLibraries() 底层依赖 System.load(),JVM 保证同一库不重复加载,多线程并发调用安全。
+ */
+ private long createSdk() throws WxErrorException {
+ WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage();
- // SDK未初始化或已过期,需要重新初始化
String configPath = configStorage.getMsgAuditLibPath();
if (StringUtils.isEmpty(configPath)) {
throw new WxErrorException("请配置会话存档sdk文件的路径,不要配错了!!");
@@ -130,55 +149,31 @@ private synchronized long initSdk() throws WxErrorException {
Finance.DestroySdk(sdk);
throw new WxErrorException("init sdk err ret " + ret);
}
-
- // 缓存SDK
- configStorage.updateMsgAuditSdk(sdk, SDK_EXPIRES_TIME);
- log.debug("初始化会话存档SDK成功,sdk={}", sdk);
-
return sdk;
}
- /**
- * 获取SDK并增加引用计数(原子操作)
- * 如果SDK未初始化或已过期,会自动初始化
- *
- * @return sdk id
- * @throws WxErrorException 初始化失败时抛出异常
- */
- private long acquireSdk() throws WxErrorException {
- WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage();
-
- // 尝试获取现有的有效SDK并增加引用计数(原子操作)
- long sdk = configStorage.acquireMsgAuditSdk();
-
- if (sdk > 0) {
- // 成功获取到有效的SDK
- return sdk;
- }
-
- // SDK未初始化或已过期,需要初始化
- // initSdk()方法已经是synchronized的,确保只有一个线程初始化
- sdk = this.initSdk();
-
- // 初始化后增加引用计数
- int refCount = configStorage.incrementMsgAuditSdkRefCount(sdk);
- if (refCount < 0) {
- // SDK已经被替换,需要重新获取
- return acquireSdk();
+ @Override
+ public void closeThreadLocalSdk() {
+ Long sdk = threadLocalSdk.get();
+ // 先从 managedSdks 摘除,摘除成功才调 DestroySdk,防止与 closeAllSdks() 并发时 double-free
+ if (sdk != null && managedSdks.remove(sdk)) {
+ Finance.DestroySdk(sdk);
+ log.info("线程 [{}] 关闭会话存档SDK,sdk={}", Thread.currentThread().getName(), sdk);
}
-
- return sdk;
+ threadLocalSdk.remove();
}
- /**
- * 释放SDK引用计数
- *
- * @param sdk sdk id
- */
- private void releaseSdk(long sdk) {
- if (sdk > 0) {
- cpService.getWxCpConfigStorage().releaseMsgAuditSdk(sdk);
+ @Override
+ public void closeAllSdks() {
+ // 逐一 remove 后再 Destroy,防止与 closeThreadLocalSdk() 并发时 double-free
+ Long[] sdks = managedSdks.toArray(new Long[0]);
+ for (Long sdk : sdks) {
+ if (managedSdks.remove(sdk)) {
+ Finance.DestroySdk(sdk);
+ log.info("关闭会话存档SDK,sdk={}", sdk);
+ }
}
+ threadLocalSdk.remove();
}
@Override
@@ -240,17 +235,18 @@ public void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String pr
* 为空字符串,拉取后续分片时直接填入上次返回的indexbuf即可。
*/
File targetFile = new File(targetFilePath);
- if (!targetFile.getParentFile().exists()) {
- targetFile.getParentFile().mkdirs();
+ File parentDir = targetFile.getParentFile();
+ if (parentDir != null && !parentDir.exists()) {
+ parentDir.mkdirs();
}
this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, i -> {
try {
// 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
- FileOutputStream outputStream = new FileOutputStream(targetFile, true);
- outputStream.write(i);
- outputStream.close();
+ try (FileOutputStream outputStream = new FileOutputStream(targetFile, true)) {
+ outputStream.write(i);
+ }
} catch (Exception e) {
- e.printStackTrace();
+ log.error("写入媒体文件分片失败,targetFilePath={}", targetFilePath, e);
}
});
}
@@ -280,7 +276,7 @@ public void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String pr
// 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
action.accept(Finance.GetData(mediaData));
} catch (Exception e) {
- e.printStackTrace();
+ log.error("处理媒体文件分片失败,sdkfileid={}", sdkfileid, e);
}
if (Finance.IsMediaDataFinish(mediaData) == 1) {
@@ -302,7 +298,7 @@ public List getPermitUserList(Integer type) throws WxErrorException {
if (type != null) {
jsonObject.addProperty("type", type);
}
- String responseContent = this.cpService.post(apiUrl, jsonObject.toString());
+ String responseContent = this.cpService.postForMsgAudit(apiUrl, jsonObject.toString());
return WxCpGsonBuilder.create().fromJson(GsonParser.parse(responseContent).getAsJsonArray("ids"),
new TypeToken>() {
}.getType());
@@ -313,83 +309,62 @@ public WxCpGroupChat getGroupChat(@NonNull String roomid) throws WxErrorExceptio
final String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_GROUP_CHAT);
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("roomid", roomid);
- String responseContent = this.cpService.post(apiUrl, jsonObject.toString());
+ String responseContent = this.cpService.postForMsgAudit(apiUrl, jsonObject.toString());
return WxCpGroupChat.fromJson(responseContent);
}
@Override
public WxCpAgreeInfo checkSingleAgree(@NonNull WxCpCheckAgreeRequest checkAgreeRequest) throws WxErrorException {
String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(CHECK_SINGLE_AGREE);
- String responseContent = this.cpService.post(apiUrl, checkAgreeRequest.toJson());
+ String responseContent = this.cpService.postForMsgAudit(apiUrl, checkAgreeRequest.toJson());
return WxCpAgreeInfo.fromJson(responseContent);
}
@Override
public List getChatRecords(long seq, @NonNull long limit, String proxy, String passwd,
@NonNull long timeout) throws Exception {
- // 获取SDK并自动增加引用计数(原子操作)
- long sdk = this.acquireSdk();
-
- try {
- long slice = Finance.NewSlice();
- long ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice);
- if (ret != 0) {
- Finance.FreeSlice(slice);
- throw new WxErrorException("getchatdata err ret " + ret);
- }
+ long sdk = this.getOrInitThreadLocalSdk();
- // 拉取会话存档
- String content = Finance.GetContentFromSlice(slice);
+ long slice = Finance.NewSlice();
+ long ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice);
+ if (ret != 0) {
Finance.FreeSlice(slice);
- WxCpChatDatas chatDatas = WxCpChatDatas.fromJson(content);
- if (chatDatas.getErrCode().intValue() != 0) {
- throw new WxErrorException(chatDatas.toJson());
- }
+ throw new WxErrorException("getchatdata err ret " + ret);
+ }
- List chatDataList = chatDatas.getChatData();
- return chatDataList != null ? chatDataList : Collections.emptyList();
- } finally {
- // 释放SDK引用计数(原子操作)
- this.releaseSdk(sdk);
+ // 拉取会话存档
+ String content = Finance.GetContentFromSlice(slice);
+ Finance.FreeSlice(slice);
+ WxCpChatDatas chatDatas = WxCpChatDatas.fromJson(content);
+ if (chatDatas.getErrCode().intValue() != 0) {
+ throw new WxErrorException(chatDatas.toJson());
}
+
+ List chatDataList = chatDatas.getChatData();
+ return chatDataList != null ? chatDataList : Collections.emptyList();
}
@Override
public WxCpChatModel getDecryptChatData(@NonNull WxCpChatDatas.WxCpChatData chatData,
@NonNull Integer pkcs1) throws Exception {
- // 获取SDK并自动增加引用计数(原子操作)
- long sdk = this.acquireSdk();
-
- try {
- String plainText = this.decryptChatData(sdk, chatData, pkcs1);
- return WxCpChatModel.fromJson(plainText);
- } finally {
- // 释放SDK引用计数(原子操作)
- this.releaseSdk(sdk);
- }
+ long sdk = this.getOrInitThreadLocalSdk();
+ String plainText = this.decryptChatData(sdk, chatData, pkcs1);
+ return WxCpChatModel.fromJson(plainText);
}
@Override
public String getChatRecordPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData,
@NonNull Integer pkcs1) throws Exception {
- // 获取SDK并自动增加引用计数(原子操作)
- long sdk = this.acquireSdk();
-
- try {
- return this.decryptChatData(sdk, chatData, pkcs1);
- } finally {
- // 释放SDK引用计数(原子操作)
- this.releaseSdk(sdk);
- }
+ long sdk = this.getOrInitThreadLocalSdk();
+ return this.decryptChatData(sdk, chatData, pkcs1);
}
@Override
public void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout,
@NonNull String targetFilePath) throws WxErrorException {
- // 获取SDK并自动增加引用计数(原子操作)
long sdk;
try {
- sdk = this.acquireSdk();
+ sdk = this.getOrInitThreadLocalSdk();
} catch (Exception e) {
throw new WxErrorException(e);
}
@@ -397,54 +372,43 @@ public void downloadMediaFile(@NonNull String sdkfileid, String proxy, String pa
// 使用AtomicReference捕获Lambda中的异常,以便在执行完后抛出
final java.util.concurrent.atomic.AtomicReference exceptionHolder = new java.util.concurrent.atomic.AtomicReference<>();
- try {
- File targetFile = new File(targetFilePath);
- if (!targetFile.getParentFile().exists()) {
- targetFile.getParentFile().mkdirs();
+ File targetFile = new File(targetFilePath);
+ File parentDir = targetFile.getParentFile();
+ if (parentDir != null && !parentDir.exists()) {
+ parentDir.mkdirs();
+ }
+ this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, i -> {
+ // 如果之前已经发生异常,不再继续处理
+ if (exceptionHolder.get() != null) {
+ return;
}
- this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, i -> {
- // 如果之前已经发生异常,不再继续处理
- if (exceptionHolder.get() != null) {
- return;
- }
- try {
- // 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
- FileOutputStream outputStream = new FileOutputStream(targetFile, true);
+ try {
+ // 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
+ try (FileOutputStream outputStream = new FileOutputStream(targetFile, true)) {
outputStream.write(i);
- outputStream.close();
- } catch (Exception e) {
- exceptionHolder.set(e);
}
- });
-
- // 检查是否发生异常,如果有则抛出
- Exception caughtException = exceptionHolder.get();
- if (caughtException != null) {
- throw new WxErrorException(caughtException);
+ } catch (Exception e) {
+ exceptionHolder.set(e);
}
- } finally {
- // 释放SDK引用计数(原子操作)
- this.releaseSdk(sdk);
+ });
+
+ // 检查是否发生异常,如果有则抛出
+ Exception caughtException = exceptionHolder.get();
+ if (caughtException != null) {
+ throw new WxErrorException(caughtException);
}
}
@Override
public void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout,
@NonNull Consumer action) throws WxErrorException {
- // 获取SDK并自动增加引用计数(原子操作)
long sdk;
try {
- sdk = this.acquireSdk();
+ sdk = this.getOrInitThreadLocalSdk();
} catch (Exception e) {
throw new WxErrorException(e);
}
-
- try {
- this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, action);
- } finally {
- // 释放SDK引用计数(原子操作)
- this.releaseSdk(sdk);
- }
+ this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, action);
}
}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaWeDocServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaWeDocServiceImpl.java
index fc5379dc73..7f5bf004db 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaWeDocServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaWeDocServiceImpl.java
@@ -4,18 +4,22 @@
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.bean.CommonUploadParam;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.cp.api.WxCpOaWeDocService;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.WxCpBaseResp;
import me.chanjar.weixin.cp.bean.oa.doc.*;
+import java.io.File;
+import java.util.List;
+
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Oa.*;
/**
- * 企业微信微盘接口实现类.
+ * 企业微信文档接口实现类.
*
- * @author Wang_Wong created on 2022-04-22
+ * @author Wang_Wong created on 2022-04-22
*/
@Slf4j
@RequiredArgsConstructor
@@ -57,11 +61,47 @@ public WxCpDocInfo docInfo(@NonNull String docId) throws WxErrorException {
@Override
public WxCpDocShare docShare(@NonNull String docId) throws WxErrorException {
+ return docShare(WxCpDocShareRequest.builder().docId(docId).build());
+ }
+
+ @Override
+ public WxCpDocShare docShare(@NonNull WxCpDocShareRequest request) throws WxErrorException {
String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_DOC_SHARE);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpDocShare.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpDocAuthInfo docGetAuth(@NonNull String docId) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_DOC_GET_AUTH);
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("docid", docId);
String responseContent = this.cpService.post(apiUrl, jsonObject.toString());
- return WxCpDocShare.fromJson(responseContent);
+ return WxCpDocAuthInfo.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp docModifyJoinRule(@NonNull WxCpDocModifyJoinRuleRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_MOD_DOC_JOIN_RULE);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp docModifyMember(@NonNull WxCpDocModifyMemberRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_MOD_DOC_MEMBER);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp docModifySafetySetting(
+ @NonNull WxCpDocModifySafetySettingRequest request
+ ) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage()
+ .getApiUrl(WEDOC_MOD_DOC_SAFETY_SETTING);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
}
@Override
@@ -86,4 +126,211 @@ public WxCpDocSheetData getSheetRangeData(@NonNull WxCpDocSheetGetDataRequest re
String responseContent = this.cpService.post(apiUrl, request.toJson());
return WxCpDocSheetData.fromJson(responseContent);
}
+
+ @Override
+ public WxCpDocData docGetData(@NonNull WxCpDocGetDataRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_GET_DOC_DATA);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpDocData.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp docModify(@NonNull WxCpDocModifyRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_MOD_DOC);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpDocImageUploadResult docUploadImage(@NonNull File file) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_UPLOAD_DOC_IMAGE);
+ String responseContent = this.cpService.upload(apiUrl, CommonUploadParam.fromFile("media", file));
+ return WxCpDocImageUploadResult.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp docAddAdmin(@NonNull WxCpDocAdminRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_ADD_ADMIN);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp docDeleteAdmin(@NonNull WxCpDocAdminRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_DEL_ADMIN);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpDocAdminListResult docGetAdminList(@NonNull String docId) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_GET_ADMIN_LIST);
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("docid", docId);
+ String responseContent = this.cpService.post(apiUrl, jsonObject.toString());
+ return WxCpDocAdminListResult.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpDocSmartSheetAuth smartSheetGetAuth(@NonNull WxCpDocSmartSheetAuthRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_GET_SHEET_AUTH);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpDocSmartSheetAuth.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp smartSheetModifyAuth(@NonNull WxCpDocSmartSheetModifyAuthRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_MOD_SHEET_AUTH);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpDocSmartSheetResult smartSheetGetSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_GET_SHEET);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpDocSmartSheetResult.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpDocSmartSheetResult smartSheetAddSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_ADD_SHEET);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpDocSmartSheetResult.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp smartSheetDeleteSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_DELETE_SHEET);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp smartSheetUpdateSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_UPDATE_SHEET);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpDocSmartSheetResult smartSheetGetViews(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_GET_VIEWS);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpDocSmartSheetResult.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpDocSmartSheetResult smartSheetAddView(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_ADD_VIEW);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpDocSmartSheetResult.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp smartSheetDeleteViews(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_DELETE_VIEWS);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp smartSheetUpdateView(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_UPDATE_VIEW);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpDocSmartSheetResult smartSheetGetFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_GET_FIELDS);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpDocSmartSheetResult.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpDocSmartSheetResult smartSheetAddFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_ADD_FIELDS);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpDocSmartSheetResult.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp smartSheetDeleteFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_DELETE_FIELDS);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp smartSheetUpdateFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_UPDATE_FIELDS);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpDocSmartSheetResult smartSheetGetRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_GET_RECORDS);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpDocSmartSheetResult.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpDocSmartSheetResult smartSheetAddRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_ADD_RECORDS);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpDocSmartSheetResult.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp smartSheetDeleteRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_DELETE_RECORDS);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp smartSheetUpdateRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_UPDATE_RECORDS);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpFormCreateResult formCreate(@NonNull WxCpFormCreateRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_CREATE_FORM);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpFormCreateResult.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpBaseResp formModify(@NonNull WxCpFormModifyRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_MODIFY_FORM);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpBaseResp.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpFormInfoResult formInfo(@NonNull String formId) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_GET_FORM_INFO);
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("formid", formId);
+ String responseContent = this.cpService.post(apiUrl, jsonObject.toString());
+ return WxCpFormInfoResult.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpFormStatisticResult formStatistic(@NonNull List requests) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_GET_FORM_STATISTIC);
+ String responseContent = this.cpService.post(apiUrl, WxCpFormStatisticRequest.toJson(requests));
+ return WxCpFormStatisticResult.fromJson(responseContent);
+ }
+
+ @Override
+ public WxCpFormAnswer formAnswer(@NonNull WxCpFormAnswerRequest request) throws WxErrorException {
+ String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_GET_FORM_ANSWER);
+ String responseContent = this.cpService.post(apiUrl, request.toJson());
+ return WxCpFormAnswer.fromJson(responseContent);
+ }
}
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 1042f88d67..ef78116e12 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
@@ -17,6 +17,7 @@
import org.apache.http.impl.client.CloseableHttpClient;
import java.io.IOException;
+import java.util.concurrent.locks.Lock;
/**
* The type Wx cp service apache http client.
@@ -74,6 +75,51 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
return this.configStorage.getAccessToken();
}
+ @Override
+ public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
+ if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getMsgAuditAccessToken();
+ }
+
+ Lock lock = this.configStorage.getMsgAuditAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getMsgAuditAccessToken();
+ }
+ // 使用会话存档secret获取access_token
+ String msgAuditSecret = this.configStorage.getMsgAuditSecret();
+ if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) {
+ throw new WxErrorException("会话存档secret未配置");
+ }
+ String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+ this.configStorage.getCorpId(), msgAuditSecret);
+
+ 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.updateMsgAuditAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+ } catch (IOException e) {
+ throw new WxRuntimeException(e);
+ }
+ } finally {
+ lock.unlock();
+ }
+ return this.configStorage.getMsgAuditAccessToken();
+ }
+
@Override
public void initHttp() {
ApacheHttpClientBuilder apacheHttpClientBuilder = this.configStorage
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 4b6a1e36ff..3ca041e7ec 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
@@ -17,6 +17,7 @@
import org.apache.hc.core5.http.HttpHost;
import java.io.IOException;
+import java.util.concurrent.locks.Lock;
/**
* The type Wx cp service apache http client.
@@ -75,6 +76,51 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
return this.configStorage.getAccessToken();
}
+ @Override
+ public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
+ if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getMsgAuditAccessToken();
+ }
+
+ Lock lock = this.configStorage.getMsgAuditAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getMsgAuditAccessToken();
+ }
+ // 使用会话存档secret获取access_token
+ String msgAuditSecret = this.configStorage.getMsgAuditSecret();
+ if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) {
+ throw new WxErrorException("会话存档secret未配置");
+ }
+ String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+ this.configStorage.getCorpId(), msgAuditSecret);
+
+ 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.updateMsgAuditAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+ } catch (IOException e) {
+ throw new WxRuntimeException(e);
+ }
+ } finally {
+ lock.unlock();
+ }
+ return this.configStorage.getMsgAuditAccessToken();
+ }
+
@Override
public void initHttp() {
HttpComponentsClientBuilder apacheHttpClientBuilder = DefaultHttpComponentsClientBuilder.get();
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 f2a50db471..7b651cbc08 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 getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
+ final WxCpConfigStorage configStorage = getWxCpConfigStorage();
+ if (!configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+ return configStorage.getMsgAuditAccessToken();
+ }
+ Lock lock = configStorage.getMsgAuditAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+ return configStorage.getMsgAuditAccessToken();
+ }
+ // 使用会话存档secret获取access_token
+ String msgAuditSecret = configStorage.getMsgAuditSecret();
+ if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) {
+ throw new WxErrorException("会话存档secret未配置");
+ }
+ String url = String.format(configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+ this.configStorage.getCorpId(), msgAuditSecret);
+ 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.updateMsgAuditAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+ } catch (IOException e) {
+ throw new WxRuntimeException(e);
+ }
+ } finally {
+ lock.unlock();
+ }
+ return configStorage.getMsgAuditAccessToken();
+ }
+
@Override
public String getAgentJsapiTicket(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 5081341851..eba9315649 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
@@ -13,6 +13,8 @@
import me.chanjar.weixin.cp.config.WxCpConfigStorage;
import me.chanjar.weixin.cp.constant.WxCpApiPathConsts;
+import java.util.concurrent.locks.Lock;
+
/**
* The type Wx cp service jodd http.
*
@@ -63,6 +65,45 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
return this.configStorage.getAccessToken();
}
+ @Override
+ public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
+ if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getMsgAuditAccessToken();
+ }
+
+ Lock lock = this.configStorage.getMsgAuditAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getMsgAuditAccessToken();
+ }
+ // 使用会话存档secret获取access_token
+ String msgAuditSecret = this.configStorage.getMsgAuditSecret();
+ if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) {
+ throw new WxErrorException("会话存档secret未配置");
+ }
+ HttpRequest request = HttpRequest.get(String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN),
+ this.configStorage.getCorpId(), msgAuditSecret));
+ 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.updateMsgAuditAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
+ } finally {
+ lock.unlock();
+ }
+ return this.configStorage.getMsgAuditAccessToken();
+ }
+
@Override
public void initHttp() {
if (this.configStorage.getHttpProxyHost() != null && this.configStorage.getHttpProxyPort() > 0) {
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 511c440e64..ce77b37805 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
@@ -12,6 +12,7 @@
import okhttp3.*;
import java.io.IOException;
+import java.util.concurrent.locks.Lock;
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.GET_TOKEN;
@@ -74,6 +75,52 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
return this.configStorage.getAccessToken();
}
+ @Override
+ public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException {
+ if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getMsgAuditAccessToken();
+ }
+
+ Lock lock = this.configStorage.getMsgAuditAccessTokenLock();
+ lock.lock();
+ try {
+ // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷
+ if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) {
+ return this.configStorage.getMsgAuditAccessToken();
+ }
+ // 使用会话存档secret获取access_token
+ String msgAuditSecret = this.configStorage.getMsgAuditSecret();
+ if (msgAuditSecret == null || msgAuditSecret.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(),
+ msgAuditSecret))
+ .get()
+ .build();
+ String resultContent = null;
+ try (Response response = client.newCall(request).execute()) {
+ resultContent = response.body().string();
+ } catch (IOException e) {
+ log.error(e.getMessage(), e);
+ }
+
+ WxError error = WxError.fromJson(resultContent, WxType.CP);
+ if (error.getErrorCode() != 0) {
+ throw new WxErrorException(error);
+ }
+ WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
+ this.configStorage.updateMsgAuditAccessToken(accessToken.getAccessToken(),
+ accessToken.getExpiresIn());
+ } finally {
+ lock.unlock();
+ }
+ return this.configStorage.getMsgAuditAccessToken();
+ }
+
@Override
public void initHttp() {
log.debug("WxCpServiceOkHttpImpl initHttp");
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/WxCpMsgTemplate.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/WxCpMsgTemplate.java
index 42e51afbb8..3c2d9fb977 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/WxCpMsgTemplate.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/WxCpMsgTemplate.java
@@ -6,6 +6,7 @@
import lombok.Data;
import lombok.NoArgsConstructor;
import me.chanjar.weixin.cp.bean.external.msg.Attachment;
+import me.chanjar.weixin.cp.bean.external.msg.TagFilter;
import me.chanjar.weixin.cp.bean.external.msg.Text;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
@@ -43,6 +44,12 @@ public class WxCpMsgTemplate implements Serializable {
@SerializedName("chat_id_list")
private List chatIdList;
+ /**
+ * 要进行群发的客户标签列表,同组标签之间按或关系进行筛选,不同组标签按且关系筛选,每组最多指定100个标签,支持规则组标签
+ */
+ @SerializedName("tag_filter")
+ private TagFilter tagFilter;
+
/**
* 发送企业群发消息的成员userid,当类型为发送给客户群时必填
*/
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/TagFilter.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/TagFilter.java
new file mode 100644
index 0000000000..da7851adbf
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/TagFilter.java
@@ -0,0 +1,20 @@
+package me.chanjar.weixin.cp.bean.external.msg;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 群发的客户标签
+ *
+ * @author Winnie
+ */
+@Data
+public class TagFilter implements Serializable {
+ private static final long serialVersionUID = -6756444546744020234L;
+
+ @SerializedName("group_list")
+ private List groupList;
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/TagList.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/TagList.java
new file mode 100644
index 0000000000..10ed191e90
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/TagList.java
@@ -0,0 +1,20 @@
+package me.chanjar.weixin.cp.bean.external.msg;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 客户标签列表
+ *
+ * @author Winnie
+ */
+@Data
+public class TagList implements Serializable {
+ private static final long serialVersionUID = 1133054307780310675L;
+
+ @SerializedName("tag_list")
+ private List tagList;
+}
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
new file mode 100644
index 0000000000..971e5958d1
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldData.java
@@ -0,0 +1,52 @@
+package me.chanjar.weixin.cp.bean.hr;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 人事助手-员工档案数据(单个员工).
+ *
+ * @author leejoker created on 2024-01-01
+ */
+@Data
+@NoArgsConstructor
+public class WxCpHrEmployeeFieldData implements Serializable {
+ private static final long serialVersionUID = 4593693598671765396L;
+
+ /**
+ * 员工userid.
+ */
+ @SerializedName("userid")
+ private String userid;
+
+ /**
+ * 字段数据列表.
+ */
+ @SerializedName("field_list")
+ private List fieldList;
+
+ /**
+ * 字段数据项.
+ */
+ @Data
+ @NoArgsConstructor
+ public static class FieldItem implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 字段key.
+ */
+ @SerializedName("field_key")
+ private String fieldKey;
+
+ /**
+ * 字段值.
+ */
+ @SerializedName("field_value")
+ private WxCpHrEmployeeFieldValue fieldValue;
+ }
+}
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
new file mode 100644
index 0000000000..07e286c2ef
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldDataResp.java
@@ -0,0 +1,38 @@
+package me.chanjar.weixin.cp.bean.hr;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.cp.bean.WxCpBaseResp;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.util.List;
+
+/**
+ * 人事助手-获取员工档案数据响应.
+ *
+ * @author leejoker created on 2024-01-01
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class WxCpHrEmployeeFieldDataResp extends WxCpBaseResp {
+ private static final long serialVersionUID = 6593693598671765396L;
+
+ /**
+ * 员工档案数据列表.
+ */
+ @SerializedName("employee_field_list")
+ private List employeeFieldList;
+
+ /**
+ * From json wx cp hr employee field data resp.
+ *
+ * @param json the json
+ * @return the wx cp hr employee field data resp
+ */
+ public static WxCpHrEmployeeFieldDataResp fromJson(String json) {
+ return WxCpGsonBuilder.create().fromJson(json, WxCpHrEmployeeFieldDataResp.class);
+ }
+}
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
new file mode 100644
index 0000000000..e355d8cc6a
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldInfo.java
@@ -0,0 +1,103 @@
+package me.chanjar.weixin.cp.bean.hr;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 人事助手-员工档案字段信息.
+ *
+ * @author leejoker created on 2024-01-01
+ */
+@Data
+@NoArgsConstructor
+public class WxCpHrEmployeeFieldInfo implements Serializable {
+ private static final long serialVersionUID = 2593693598671765396L;
+
+ /**
+ * 字段key.
+ */
+ @SerializedName("field_key")
+ private String fieldKey;
+
+ /**
+ * 字段英文名称.
+ */
+ @SerializedName("field_en_name")
+ private String fieldEnName;
+
+ /**
+ * 字段中文名称.
+ */
+ @SerializedName("field_zh_name")
+ private String fieldZhName;
+
+ /**
+ * 字段类型.
+ * 具体取值参见 {@link WxCpHrFieldType}
+ */
+ @SerializedName("field_type")
+ private Integer fieldType;
+
+ /**
+ * 获取字段类型枚举.
+ *
+ * @return 字段类型枚举,未匹配时返回 null
+ */
+ public WxCpHrFieldType getFieldTypeEnum() {
+ return fieldType == null ? null : WxCpHrFieldType.fromCode(fieldType);
+ }
+
+ /**
+ * 是否系统字段.
+ * 0: 否
+ * 1: 是
+ */
+ @SerializedName("is_sys")
+ private Integer isSys;
+
+ /**
+ * 字段详情.
+ */
+ @SerializedName("field_detail")
+ private FieldDetail fieldDetail;
+
+ /**
+ * 字段详情.
+ */
+ @Data
+ @NoArgsConstructor
+ public static class FieldDetail implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 选项列表(单选/多选字段专用).
+ */
+ @SerializedName("option_list")
+ private List