1
0
mirror of synced 2026-10-12 18:23:06 +08:00

🆕 #4118 【企业微信】增加微信客服知识库接口

This commit is contained in:
Binary Wang
2026-09-05 16:40:41 +08:00
committed by GitHub
parent 474f4c7a0f
commit c1591bae9c
11 changed files with 521 additions and 0 deletions

View File

@@ -291,4 +291,81 @@ public interface WxCpKfService {
*/
WxCpBaseResp cancelUpgradeService(String openKfid, String externalUserId)
throws WxErrorException;
/**
* 添加知识库分组。
*
* @param group 分组信息
* @return 新增结果,包含分组 ID
* @throws WxErrorException 异常
*/
WxCpKfKnowledgeGroupAddResp addKnowledgeGroup(WxCpKfKnowledgeGroup group) throws WxErrorException;
/**
* 删除知识库分组。
*
* @param groupId 分组 ID
* @return 接口返回结果
* @throws WxErrorException 异常
*/
WxCpBaseResp delKnowledgeGroup(String groupId) throws WxErrorException;
/**
* 修改知识库分组。
*
* @param group 分组信息,必须包含分组 ID 和名称
* @return 接口返回结果
* @throws WxErrorException 异常
*/
WxCpBaseResp modKnowledgeGroup(WxCpKfKnowledgeGroup group) throws WxErrorException;
/**
* 分页获取知识库分组。
*
* @param cursor 分页游标,可为空
* @param limit 每页数量,可为空
* @param groupId 指定分组 ID,可为空
* @return 分组列表
* @throws WxErrorException 异常
*/
WxCpKfKnowledgeGroupListResp listKnowledgeGroup(String cursor, Integer limit, String groupId) throws WxErrorException;
/**
* 添加知识库问答。
*
* @param intent 问答信息
* @return 新增结果,包含问答 ID
* @throws WxErrorException 异常
*/
WxCpKfKnowledgeIntentAddResp addKnowledgeIntent(WxCpKfKnowledgeIntent intent) throws WxErrorException;
/**
* 删除知识库问答。
*
* @param intentId 问答 ID
* @return 接口返回结果
* @throws WxErrorException 异常
*/
WxCpBaseResp delKnowledgeIntent(String intentId) throws WxErrorException;
/**
* 修改知识库问答。
*
* @param intent 问答信息,必须包含问答 ID
* @return 接口返回结果
* @throws WxErrorException 异常
*/
WxCpBaseResp modKnowledgeIntent(WxCpKfKnowledgeIntent intent) throws WxErrorException;
/**
* 分页获取知识库问答。
*
* @param cursor 分页游标,可为空
* @param limit 每页数量,可为空
* @param groupId 指定分组 ID,可为空
* @param intentId 指定问答 ID,可为空
* @return 问答列表
* @throws WxErrorException 异常
*/
WxCpKfKnowledgeIntentListResp listKnowledgeIntent(String cursor, Integer limit, String groupId, String intentId) throws WxErrorException;
}

View File

@@ -317,4 +317,81 @@ public class WxCpKfServiceImpl implements WxCpKfService {
return WxCpKfGetServicerStatisticResp.fromJson(responseContent);
}
@Override
public WxCpKfKnowledgeGroupAddResp addKnowledgeGroup(WxCpKfKnowledgeGroup group) throws WxErrorException {
String url = cpService.getWxCpConfigStorage().getApiUrl(KNOWLEDGE_ADD_GROUP);
String responseContent = cpService.post(url, GSON.toJson(group));
return WxCpKfKnowledgeGroupAddResp.fromJson(responseContent);
}
@Override
public WxCpBaseResp delKnowledgeGroup(String groupId) throws WxErrorException {
return knowledgeBaseResp(KNOWLEDGE_DEL_GROUP, "group_id", groupId);
}
@Override
public WxCpBaseResp modKnowledgeGroup(WxCpKfKnowledgeGroup group) throws WxErrorException {
String url = cpService.getWxCpConfigStorage().getApiUrl(KNOWLEDGE_MOD_GROUP);
String responseContent = cpService.post(url, GSON.toJson(group));
return WxCpBaseResp.fromJson(responseContent);
}
@Override
public WxCpKfKnowledgeGroupListResp listKnowledgeGroup(String cursor, Integer limit, String groupId) throws WxErrorException {
String url = cpService.getWxCpConfigStorage().getApiUrl(KNOWLEDGE_LIST_GROUP);
String responseContent = cpService.post(url, GSON.toJson(listKnowledgeJson(cursor, limit, groupId, null)));
return WxCpKfKnowledgeGroupListResp.fromJson(responseContent);
}
@Override
public WxCpKfKnowledgeIntentAddResp addKnowledgeIntent(WxCpKfKnowledgeIntent intent) throws WxErrorException {
String url = cpService.getWxCpConfigStorage().getApiUrl(KNOWLEDGE_ADD_INTENT);
String responseContent = cpService.post(url, GSON.toJson(intent));
return WxCpKfKnowledgeIntentAddResp.fromJson(responseContent);
}
@Override
public WxCpBaseResp delKnowledgeIntent(String intentId) throws WxErrorException {
return knowledgeBaseResp(KNOWLEDGE_DEL_INTENT, "intent_id", intentId);
}
@Override
public WxCpBaseResp modKnowledgeIntent(WxCpKfKnowledgeIntent intent) throws WxErrorException {
String url = cpService.getWxCpConfigStorage().getApiUrl(KNOWLEDGE_MOD_INTENT);
String responseContent = cpService.post(url, GSON.toJson(intent));
return WxCpBaseResp.fromJson(responseContent);
}
@Override
public WxCpKfKnowledgeIntentListResp listKnowledgeIntent(String cursor, Integer limit, String groupId, String intentId) throws WxErrorException {
String url = cpService.getWxCpConfigStorage().getApiUrl(KNOWLEDGE_LIST_INTENT);
String responseContent = cpService.post(url, GSON.toJson(listKnowledgeJson(cursor, limit, groupId, intentId)));
return WxCpKfKnowledgeIntentListResp.fromJson(responseContent);
}
private WxCpBaseResp knowledgeBaseResp(String apiPath, String key, String value) throws WxErrorException {
String url = cpService.getWxCpConfigStorage().getApiUrl(apiPath);
JsonObject json = new JsonObject();
json.addProperty(key, value);
String responseContent = cpService.post(url, GSON.toJson(json));
return WxCpBaseResp.fromJson(responseContent);
}
private JsonObject listKnowledgeJson(String cursor, Integer limit, String groupId, String intentId) {
JsonObject json = new JsonObject();
if (cursor != null) {
json.addProperty("cursor", cursor);
}
if (limit != null) {
json.addProperty("limit", limit);
}
if (groupId != null) {
json.addProperty("group_id", groupId);
}
if (intentId != null) {
json.addProperty("intent_id", intentId);
}
return json;
}
}

View File

@@ -0,0 +1,22 @@
package me.chanjar.weixin.cp.bean.kf;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import java.io.Serializable;
/**
* 微信客服知识库分组。
*/
@Data
public class WxCpKfKnowledgeGroup implements Serializable {
private static final long serialVersionUID = -170690715179803477L;
@SerializedName("group_id")
private String groupId;
private String name;
@SerializedName("is_default")
private Integer isDefault;
}

View File

@@ -0,0 +1,23 @@
package me.chanjar.weixin.cp.bean.kf;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import me.chanjar.weixin.cp.bean.WxCpBaseResp;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
/**
* 微信客服知识库分组新增返回结果。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WxCpKfKnowledgeGroupAddResp extends WxCpBaseResp {
private static final long serialVersionUID = 232872665454024387L;
@SerializedName("group_id")
private String groupId;
public static WxCpKfKnowledgeGroupAddResp fromJson(String json) {
return WxCpGsonBuilder.create().fromJson(json, WxCpKfKnowledgeGroupAddResp.class);
}
}

View File

@@ -0,0 +1,31 @@
package me.chanjar.weixin.cp.bean.kf;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import me.chanjar.weixin.cp.bean.WxCpBaseResp;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import java.util.List;
/**
* 微信客服知识库分组列表返回结果。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WxCpKfKnowledgeGroupListResp extends WxCpBaseResp {
private static final long serialVersionUID = -8350717377843545855L;
@SerializedName("next_cursor")
private String nextCursor;
@SerializedName("has_more")
private Integer hasMore;
@SerializedName("group_list")
private List<WxCpKfKnowledgeGroup> groupList;
public static WxCpKfKnowledgeGroupListResp fromJson(String json) {
return WxCpGsonBuilder.create().fromJson(json, WxCpKfKnowledgeGroupListResp.class);
}
}

View File

@@ -0,0 +1,109 @@
package me.chanjar.weixin.cp.bean.kf;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import java.io.Serializable;
import java.util.List;
/**
* 微信客服知识库问答。
*/
@Data
public class WxCpKfKnowledgeIntent implements Serializable {
private static final long serialVersionUID = -3777712166335763935L;
@SerializedName("group_id")
private String groupId;
@SerializedName("intent_id")
private String intentId;
private Question question;
@SerializedName("similar_questions")
private SimilarQuestions similarQuestions;
private List<Answer> answers;
public static WxCpKfKnowledgeIntent fromJson(String json) {
return WxCpGsonBuilder.create().fromJson(json, WxCpKfKnowledgeIntent.class);
}
@Data
public static class Question implements Serializable {
private static final long serialVersionUID = -1833564733770700525L;
private Text text;
@SerializedName("similar_questions")
private SimilarQuestions similarQuestions;
private List<Answer> answers;
}
@Data
public static class Text implements Serializable {
private static final long serialVersionUID = -4775152873471313775L;
private String content;
}
@Data
public static class SimilarQuestions implements Serializable {
private static final long serialVersionUID = -3338135520171174537L;
private List<Question> items;
}
@Data
public static class Answer implements Serializable {
private static final long serialVersionUID = 482114683168970317L;
private Text text;
private List<Attachment> attachments;
}
@Data
public static class Attachment implements Serializable {
private static final long serialVersionUID = 547601649734690079L;
@SerializedName("msgtype")
private String msgType;
private Image image;
private Video video;
private Link link;
@SerializedName("miniprogram")
private MiniProgram miniProgram;
}
@Data
public static class Image implements Serializable {
private static final long serialVersionUID = -6305485241850490695L;
@SerializedName("media_id")
private String mediaId;
private String name;
}
@Data
public static class Video implements Serializable {
private static final long serialVersionUID = 4002145709503795505L;
@SerializedName("media_id")
private String mediaId;
private String name;
}
@Data
public static class Link implements Serializable {
private static final long serialVersionUID = -1844812819777892606L;
private String title;
@SerializedName("pic_url")
private String picUrl;
private String desc;
private String url;
}
@Data
public static class MiniProgram implements Serializable {
private static final long serialVersionUID = 6893025025270416975L;
private String title;
@SerializedName("thumb_media_id")
private String thumbMediaId;
private String appid;
private String pagepath;
}
}

View File

@@ -0,0 +1,23 @@
package me.chanjar.weixin.cp.bean.kf;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import me.chanjar.weixin.cp.bean.WxCpBaseResp;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
/**
* 微信客服知识库问答新增返回结果。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WxCpKfKnowledgeIntentAddResp extends WxCpBaseResp {
private static final long serialVersionUID = -2693926545826175543L;
@SerializedName("intent_id")
private String intentId;
public static WxCpKfKnowledgeIntentAddResp fromJson(String json) {
return WxCpGsonBuilder.create().fromJson(json, WxCpKfKnowledgeIntentAddResp.class);
}
}

View File

@@ -0,0 +1,31 @@
package me.chanjar.weixin.cp.bean.kf;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import me.chanjar.weixin.cp.bean.WxCpBaseResp;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import java.util.List;
/**
* 微信客服知识库问答列表返回结果。
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WxCpKfKnowledgeIntentListResp extends WxCpBaseResp {
private static final long serialVersionUID = -724770244623284115L;
@SerializedName("next_cursor")
private String nextCursor;
@SerializedName("has_more")
private Integer hasMore;
@SerializedName("intent_list")
private List<WxCpKfKnowledgeIntent> intentList;
public static WxCpKfKnowledgeIntentListResp fromJson(String json) {
return WxCpGsonBuilder.create().fromJson(json, WxCpKfKnowledgeIntentListResp.class);
}
}

View File

@@ -1719,6 +1719,23 @@ public interface WxCpApiPathConsts {
*/
String CUSTOMER_CANCEL_UPGRADE_SERVICE = "/cgi-bin/kf/customer/cancel_upgrade_service";
/** 添加知识库分组。 */
String KNOWLEDGE_ADD_GROUP = "/cgi-bin/kf/knowledge/add_group";
/** 删除知识库分组。 */
String KNOWLEDGE_DEL_GROUP = "/cgi-bin/kf/knowledge/del_group";
/** 修改知识库分组。 */
String KNOWLEDGE_MOD_GROUP = "/cgi-bin/kf/knowledge/mod_group";
/** 获取知识库分组列表。 */
String KNOWLEDGE_LIST_GROUP = "/cgi-bin/kf/knowledge/list_group";
/** 添加知识库问答。 */
String KNOWLEDGE_ADD_INTENT = "/cgi-bin/kf/knowledge/add_intent";
/** 删除知识库问答。 */
String KNOWLEDGE_DEL_INTENT = "/cgi-bin/kf/knowledge/del_intent";
/** 修改知识库问答。 */
String KNOWLEDGE_MOD_INTENT = "/cgi-bin/kf/knowledge/mod_intent";
/** 获取知识库问答列表。 */
String KNOWLEDGE_LIST_INTENT = "/cgi-bin/kf/knowledge/list_intent";
}
/**

View File

@@ -0,0 +1,110 @@
package me.chanjar.weixin.cp.bean.kf;
import com.google.gson.JsonParser;
import me.chanjar.weixin.cp.api.WxCpKfService;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.api.impl.WxCpKfServiceImpl;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import org.testng.annotations.Test;
import org.mockito.ArgumentCaptor;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class WxCpKfKnowledgeTest {
@Test
public void testKnowledgeGroupListFromJson() {
String json = "{\"errcode\":0,\"errmsg\":\"ok\",\"next_cursor\":\"next\",\"has_more\":1,\"group_list\":[{\"group_id\":\"group-1\",\"name\":\"默认分组\",\"is_default\":1}]}";
WxCpKfKnowledgeGroupListResp response = WxCpKfKnowledgeGroupListResp.fromJson(json);
assertThat(response.getNextCursor()).isEqualTo("next");
assertThat(response.getHasMore()).isEqualTo(1);
assertThat(response.getGroupList()).singleElement().satisfies(group -> {
assertThat(group.getGroupId()).isEqualTo("group-1");
assertThat(group.getName()).isEqualTo("默认分组");
assertThat(group.getIsDefault()).isEqualTo(1);
});
}
@Test
public void testKnowledgeIntentToJsonAndFromJson() {
String json = "{\"group_id\":\"group-1\",\"intent_id\":\"intent-1\",\"question\":{\"text\":{\"content\":\"主问题\"}},\"similar_questions\":{\"items\":[{\"text\":{\"content\":\"相似问题\"}}]},\"answers\":[{\"text\":{\"content\":\"回答\"},\"attachments\":[{\"msgtype\":\"image\",\"image\":{\"media_id\":\"media-1\"}}]}]}";
WxCpKfKnowledgeIntent intent = WxCpKfKnowledgeIntent.fromJson(json);
String serialized = WxCpGsonBuilder.create().toJson(intent);
assertThat(intent.getQuestion().getText().getContent()).isEqualTo("主问题");
assertThat(intent.getSimilarQuestions().getItems()).hasSize(1);
assertThat(intent.getAnswers().get(0).getAttachments().get(0).getImage().getMediaId()).isEqualTo("media-1");
assertThat(JsonParser.parseString(serialized)).isEqualTo(JsonParser.parseString(json));
}
@Test
public void testKnowledgeIntentListFromJson() {
String json = "{\"errcode\":0,\"errmsg\":\"ok\",\"intent_list\":[{\"group_id\":\"group-1\",\"intent_id\":\"intent-1\",\"question\":{\"text\":{\"content\":\"主问题\"},\"similar_questions\":{\"items\":[{\"text\":{\"content\":\"相似问题\"}}]},\"answers\":[{\"text\":{\"content\":\"回答\"}}]}}]}";
WxCpKfKnowledgeIntentListResp response = WxCpKfKnowledgeIntentListResp.fromJson(json);
WxCpKfKnowledgeIntent intent = response.getIntentList().get(0);
assertThat(intent.getQuestion().getSimilarQuestions().getItems()).hasSize(1);
assertThat(intent.getQuestion().getAnswers()).singleElement().satisfies(answer ->
assertThat(answer.getText().getContent()).isEqualTo("回答"));
}
@Test
public void testKnowledgeApiRequests() throws Exception {
WxCpService cpService = mock(WxCpService.class, RETURNS_DEEP_STUBS);
when(cpService.getWxCpConfigStorage().getApiUrl(anyString())).thenAnswer(invocation -> invocation.getArgument(0));
when(cpService.post(anyString(), anyString())).thenReturn("{\"errcode\":0,\"errmsg\":\"ok\",\"group_id\":\"group-1\",\"intent_id\":\"intent-1\"}");
WxCpKfService service = new WxCpKfServiceImpl(cpService);
WxCpKfKnowledgeGroup group = new WxCpKfKnowledgeGroup();
group.setName("常见问题");
WxCpKfKnowledgeIntent intent = WxCpKfKnowledgeIntent.fromJson("{\"group_id\":\"group-1\",\"intent_id\":\"intent-1\",\"question\":{\"text\":{\"content\":\"主问题\"}},\"answers\":[{\"text\":{\"content\":\"回答\"}}]}");
assertThat(service.addKnowledgeGroup(group).getGroupId()).isEqualTo("group-1");
group.setGroupId("group-1");
service.delKnowledgeGroup("group-1");
service.modKnowledgeGroup(group);
service.listKnowledgeGroup("cursor", 100, "group-1");
assertThat(service.addKnowledgeIntent(intent).getIntentId()).isEqualTo("intent-1");
service.delKnowledgeIntent("intent-1");
service.modKnowledgeIntent(intent);
service.listKnowledgeIntent("cursor", 100, "group-1", "intent-1");
service.listKnowledgeIntent(null, null, null, null);
ArgumentCaptor<String> urlCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> requestCaptor = ArgumentCaptor.forClass(String.class);
verify(cpService, times(9)).post(urlCaptor.capture(), requestCaptor.capture());
assertThat(urlCaptor.getAllValues()).isEqualTo(Arrays.asList(
"/cgi-bin/kf/knowledge/add_group", "/cgi-bin/kf/knowledge/del_group",
"/cgi-bin/kf/knowledge/mod_group", "/cgi-bin/kf/knowledge/list_group",
"/cgi-bin/kf/knowledge/add_intent", "/cgi-bin/kf/knowledge/del_intent",
"/cgi-bin/kf/knowledge/mod_intent", "/cgi-bin/kf/knowledge/list_intent",
"/cgi-bin/kf/knowledge/list_intent"));
assertJsonRequests(requestCaptor.getAllValues(), Arrays.asList(
"{\"name\":\"常见问题\"}", "{\"group_id\":\"group-1\"}",
"{\"group_id\":\"group-1\",\"name\":\"常见问题\"}", "{\"cursor\":\"cursor\",\"limit\":100,\"group_id\":\"group-1\"}",
"{\"group_id\":\"group-1\",\"intent_id\":\"intent-1\",\"question\":{\"text\":{\"content\":\"主问题\"}},\"answers\":[{\"text\":{\"content\":\"回答\"}}]}", "{\"intent_id\":\"intent-1\"}",
"{\"group_id\":\"group-1\",\"intent_id\":\"intent-1\",\"question\":{\"text\":{\"content\":\"主问题\"}},\"answers\":[{\"text\":{\"content\":\"回答\"}}]}", "{\"cursor\":\"cursor\",\"limit\":100,\"group_id\":\"group-1\",\"intent_id\":\"intent-1\"}",
"{}"));
}
private void assertJsonRequests(List<String> actual, List<String> expected) {
assertThat(actual).hasSameSizeAs(expected);
for (int i = 0; i < actual.size(); i++) {
assertThat(JsonParser.parseString(actual.get(i))).isEqualTo(JsonParser.parseString(expected.get(i)));
}
}
}

View File

@@ -28,6 +28,7 @@
<class name="me.chanjar.weixin.cp.bean.message.WxCpXmlOutTextMessageTest"/>
<class name="me.chanjar.weixin.cp.bean.todo.WxCpTodoServiceMockTest"/>
<class name="me.chanjar.weixin.cp.util.crypto.WxCpIntelligentRobotCryptUtilTest"/>
<class name="me.chanjar.weixin.cp.bean.kf.WxCpKfKnowledgeTest"/>
</classes>
</test>
</suite>