@@ -88,6 +88,11 @@ public class WxConsts { | |||
* 小程序卡片(要求小程序与公众号已关联) | |||
*/ | |||
public static final String MINIPROGRAMPAGE = "miniprogrampage"; | |||
/** | |||
* 任务卡片消息 | |||
*/ | |||
public static final String TASKCARD = "taskcard"; | |||
} | |||
/** | |||
@@ -84,6 +84,11 @@ public class WxCpConsts { | |||
*/ | |||
public static final String LOCATION_SELECT = "location_select"; | |||
/** | |||
* 任务卡片事件推送. | |||
*/ | |||
public static final String TASKCARD_CLICK = "taskcard_click"; | |||
} | |||
/** | |||
@@ -301,6 +301,13 @@ public interface WxCpService { | |||
*/ | |||
WxCpChatService getChatService(); | |||
/** | |||
* 获取任务卡片服务 | |||
* | |||
* @return 任务卡片服务 | |||
*/ | |||
WxCpTaskCardService getTaskCardService(); | |||
WxCpAgentService getAgentService(); | |||
WxCpOAService getOAService(); | |||
@@ -0,0 +1,30 @@ | |||
package me.chanjar.weixin.cp.api; | |||
import me.chanjar.weixin.common.error.WxErrorException; | |||
import java.util.List; | |||
/** | |||
* <pre> | |||
* 任务卡片管理接口. | |||
* Created by Jeff on 2019-05-16. | |||
* </pre> | |||
* | |||
* @author <a href="https://github.com/domainname">Jeff</a> | |||
* @date 2019-05-16 | |||
*/ | |||
public interface WxCpTaskCardService { | |||
/** | |||
* <pre> | |||
* 更新任务卡片消息状态 | |||
* 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/91579 | |||
* | |||
* 注意: 这个方法使用WxCpConfigStorage里的agentId | |||
* </pre> | |||
* | |||
* @param userIds 企业的成员ID列表 | |||
* @param taskId 任务卡片ID | |||
* @param clickedKey 已点击按钮的Key | |||
*/ | |||
void update(List<String> userIds, String taskId, String clickedKey) throws WxErrorException; | |||
} |
@@ -46,6 +46,7 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH | |||
private WxCpTagService tagService = new WxCpTagServiceImpl(this); | |||
private WxCpAgentService agentService = new WxCpAgentServiceImpl(this); | |||
private WxCpOAService oaService = new WxCpOAServiceImpl(this); | |||
private WxCpTaskCardService taskCardService = new WxCpTaskCardServiceImpl(this); | |||
/** | |||
* 全局的是否正在刷新access token的锁 | |||
@@ -392,6 +393,11 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH | |||
return oaService; | |||
} | |||
@Override | |||
public WxCpTaskCardService getTaskCardService() { | |||
return taskCardService; | |||
} | |||
@Override | |||
public RequestHttp<?, ?> getRequestHttp() { | |||
return this; | |||
@@ -0,0 +1,39 @@ | |||
package me.chanjar.weixin.cp.api.impl; | |||
import lombok.RequiredArgsConstructor; | |||
import me.chanjar.weixin.common.error.WxErrorException; | |||
import me.chanjar.weixin.common.util.json.WxGsonBuilder; | |||
import me.chanjar.weixin.cp.api.WxCpService; | |||
import me.chanjar.weixin.cp.api.WxCpTaskCardService; | |||
import java.util.HashMap; | |||
import java.util.List; | |||
import java.util.Map; | |||
/** | |||
* <pre> | |||
* 任务卡片管理接口. | |||
* Created by Jeff on 2019-05-16. | |||
* </pre> | |||
* | |||
* @author <a href="https://github.com/domainname">Jeff</a> | |||
* @date 2019-05-16 | |||
*/ | |||
@RequiredArgsConstructor | |||
public class WxCpTaskCardServiceImpl implements WxCpTaskCardService { | |||
private final WxCpService mainService; | |||
@Override | |||
public void update(List<String> userIds, String taskId, String clickedKey) throws WxErrorException { | |||
Integer agentId = this.mainService.getWxCpConfigStorage().getAgentId(); | |||
Map<String, Object> data = new HashMap<>(4); | |||
data.put("userids", userIds); | |||
data.put("agentid", agentId); | |||
data.put("task_id", taskId); | |||
data.put("clicked_key", clickedKey); | |||
String url = "https://qyapi.weixin.qq.com/cgi-bin/message/update_taskcard"; | |||
this.mainService.post(url, WxGsonBuilder.create().toJson(data)); | |||
} | |||
} |
@@ -1,26 +1,18 @@ | |||
package me.chanjar.weixin.cp.bean; | |||
import java.io.Serializable; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import org.apache.commons.lang3.StringUtils; | |||
import com.google.gson.JsonArray; | |||
import com.google.gson.JsonObject; | |||
import lombok.Data; | |||
import me.chanjar.weixin.common.api.WxConsts.KefuMsgType; | |||
import me.chanjar.weixin.cp.bean.article.MpnewsArticle; | |||
import me.chanjar.weixin.cp.bean.article.NewArticle; | |||
import me.chanjar.weixin.cp.bean.messagebuilder.FileBuilder; | |||
import me.chanjar.weixin.cp.bean.messagebuilder.ImageBuilder; | |||
import me.chanjar.weixin.cp.bean.messagebuilder.MarkdownMsgBuilder; | |||
import me.chanjar.weixin.cp.bean.messagebuilder.MpnewsBuilder; | |||
import me.chanjar.weixin.cp.bean.messagebuilder.NewsBuilder; | |||
import me.chanjar.weixin.cp.bean.messagebuilder.TextBuilder; | |||
import me.chanjar.weixin.cp.bean.messagebuilder.TextCardBuilder; | |||
import me.chanjar.weixin.cp.bean.messagebuilder.VideoBuilder; | |||
import me.chanjar.weixin.cp.bean.messagebuilder.VoiceBuilder; | |||
import me.chanjar.weixin.cp.bean.messagebuilder.*; | |||
import me.chanjar.weixin.cp.bean.taskcard.TaskCardButton; | |||
import org.apache.commons.lang3.StringUtils; | |||
import java.io.Serializable; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
/** | |||
* 消息. | |||
@@ -49,6 +41,12 @@ public class WxCpMessage implements Serializable { | |||
private List<NewArticle> articles = new ArrayList<>(); | |||
private List<MpnewsArticle> mpnewsArticles = new ArrayList<>(); | |||
/** | |||
* 任务卡片特有的属性 | |||
*/ | |||
private String taskId; | |||
private List<TaskCardButton> taskButtons = new ArrayList<>(); | |||
/** | |||
* 获得文本消息builder. | |||
*/ | |||
@@ -112,6 +110,13 @@ public class WxCpMessage implements Serializable { | |||
return new FileBuilder(); | |||
} | |||
/** | |||
* 获得任务卡片消息builder. | |||
*/ | |||
public static TaskCardBuilder TASKCARD() { | |||
return new TaskCardBuilder(); | |||
} | |||
/** | |||
* <pre> | |||
@@ -124,6 +129,7 @@ public class WxCpMessage implements Serializable { | |||
* {@link KefuMsgType#NEWS} | |||
* {@link KefuMsgType#MPNEWS} | |||
* {@link KefuMsgType#MARKDOWN} | |||
* {@link KefuMsgType#TASKCARD} | |||
* </pre> | |||
* | |||
* @param msgType 消息类型 | |||
@@ -249,6 +255,42 @@ public class WxCpMessage implements Serializable { | |||
messageJson.add("mpnews", newsJsonObject); | |||
break; | |||
} | |||
case KefuMsgType.TASKCARD: { | |||
JsonObject text = new JsonObject(); | |||
text.addProperty("title", this.getTitle()); | |||
text.addProperty("description", this.getDescription()); | |||
if (StringUtils.isNotBlank(this.getUrl())) { | |||
text.addProperty("url", this.getUrl()); | |||
} | |||
text.addProperty("task_id", this.getTaskId()); | |||
JsonArray buttonJsonArray = new JsonArray(); | |||
for (TaskCardButton button : this.getTaskButtons()) { | |||
JsonObject buttonJson = new JsonObject(); | |||
buttonJson.addProperty("key", button.getKey()); | |||
buttonJson.addProperty("name", button.getName()); | |||
if (StringUtils.isNotBlank(button.getReplaceName())) { | |||
buttonJson.addProperty("replace_name", button.getReplaceName()); | |||
} | |||
if (StringUtils.isNotBlank(button.getColor())) { | |||
buttonJson.addProperty("color", button.getColor()); | |||
} | |||
if (button.getBold() != null) { | |||
buttonJson.addProperty("is_bold", button.getBold()); | |||
} | |||
buttonJsonArray.add(buttonJson); | |||
} | |||
text.add("btn", buttonJsonArray); | |||
messageJson.add("taskcard", text); | |||
break; | |||
} | |||
default: { | |||
// do nothing | |||
} | |||
@@ -0,0 +1,42 @@ | |||
package me.chanjar.weixin.cp.bean; | |||
import com.google.gson.annotations.SerializedName; | |||
import lombok.AllArgsConstructor; | |||
import lombok.Data; | |||
import lombok.NoArgsConstructor; | |||
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; | |||
import java.io.Serializable; | |||
import java.util.List; | |||
/** | |||
* <pre> | |||
* 更新任务卡片消息状态的返回类 | |||
* 参考文档:https://work.weixin.qq.com/api/doc#90000/90135/91579 | |||
* Created by Jeff on 2019-05-16. | |||
* </pre> | |||
* | |||
* @author <a href="https://github.com/domainname">Jeff</a> | |||
* @date 2019-05-16 | |||
*/ | |||
@Data | |||
@AllArgsConstructor | |||
@NoArgsConstructor | |||
public class WxCpTaskCardUpdateResult implements Serializable { | |||
@SerializedName("errcode") | |||
private Integer errcode; | |||
@SerializedName("errmsg") | |||
private String errmsg; | |||
/** | |||
* 用户列表 | |||
*/ | |||
@SerializedName("invaliduser") | |||
private List<String> invalidUsers; | |||
public static WxCpTaskCardUpdateResult fromJson(String json) { | |||
return WxCpGsonBuilder.create().fromJson(json, WxCpTaskCardUpdateResult.class); | |||
} | |||
} |
@@ -1,18 +1,8 @@ | |||
package me.chanjar.weixin.cp.bean; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.io.Serializable; | |||
import java.nio.charset.StandardCharsets; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import java.util.Map; | |||
import com.thoughtworks.xstream.annotations.XStreamImplicit; | |||
import org.apache.commons.io.IOUtils; | |||
import com.thoughtworks.xstream.annotations.XStreamAlias; | |||
import com.thoughtworks.xstream.annotations.XStreamConverter; | |||
import com.thoughtworks.xstream.annotations.XStreamImplicit; | |||
import lombok.Data; | |||
import lombok.extern.slf4j.Slf4j; | |||
import me.chanjar.weixin.common.api.WxConsts; | |||
@@ -22,6 +12,15 @@ import me.chanjar.weixin.cp.config.WxCpConfigStorage; | |||
import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil; | |||
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; | |||
import me.chanjar.weixin.cp.util.xml.XStreamTransformer; | |||
import org.apache.commons.io.IOUtils; | |||
import java.io.IOException; | |||
import java.io.InputStream; | |||
import java.io.Serializable; | |||
import java.nio.charset.StandardCharsets; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import java.util.Map; | |||
/** | |||
* <pre> | |||
@@ -157,6 +156,10 @@ public class WxCpXmlMessage implements Serializable { | |||
@XStreamConverter(value = XStreamCDataConverter.class) | |||
private String recognition; | |||
@XStreamAlias("TaskId") | |||
@XStreamConverter(value = XStreamCDataConverter.class) | |||
private String taskId; | |||
/** | |||
* 通讯录变更事件. | |||
* 请参考常量 me.chanjar.weixin.cp.WxCpConsts.ContactChangeType | |||
@@ -0,0 +1,68 @@ | |||
package me.chanjar.weixin.cp.bean.messagebuilder; | |||
import me.chanjar.weixin.common.api.WxConsts; | |||
import me.chanjar.weixin.cp.bean.WxCpMessage; | |||
import me.chanjar.weixin.cp.bean.taskcard.TaskCardButton; | |||
import java.util.List; | |||
/** | |||
* <pre> | |||
* 任务卡片消息Builder | |||
* 用法: WxCustomMessage m = WxCustomMessage.TASKCARD().title(...)....toUser(...).build(); | |||
* </pre> | |||
* | |||
* @author <a href="https://github.com/domainname">Jeff</a> | |||
* @date 2019-05-16 | |||
*/ | |||
public class TaskCardBuilder extends BaseBuilder<TaskCardBuilder> { | |||
private String title; | |||
private String description; | |||
private String url; | |||
private String taskId; | |||
/** | |||
* 按钮个数为1~2个 | |||
*/ | |||
private List<TaskCardButton> buttons; | |||
public TaskCardBuilder() { | |||
this.msgType = WxConsts.KefuMsgType.TASKCARD; | |||
} | |||
public TaskCardBuilder title(String title) { | |||
this.title = title; | |||
return this; | |||
} | |||
public TaskCardBuilder description(String description) { | |||
this.description = description; | |||
return this; | |||
} | |||
public TaskCardBuilder url(String url) { | |||
this.url = url; | |||
return this; | |||
} | |||
public TaskCardBuilder taskId(String taskId) { | |||
this.taskId = taskId; | |||
return this; | |||
} | |||
public TaskCardBuilder buttons(List<TaskCardButton> buttons) { | |||
this.buttons = buttons; | |||
return this; | |||
} | |||
@Override | |||
public WxCpMessage build() { | |||
WxCpMessage m = super.build(); | |||
m.setSafe(null); | |||
m.setTitle(this.title); | |||
m.setDescription(this.description); | |||
m.setUrl(this.url); | |||
m.setTaskId(this.taskId); | |||
m.setTaskButtons(this.buttons); | |||
return m; | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
package me.chanjar.weixin.cp.bean.taskcard; | |||
import lombok.Builder; | |||
import lombok.Data; | |||
/** | |||
* <pre> | |||
* 任务卡片按钮 | |||
* Created by Jeff on 2019-05-16. | |||
* </pre> | |||
* | |||
* @author <a href="https://github.com/domainname">Jeff</a> | |||
* @date 2019-05-16 | |||
*/ | |||
@Data | |||
@Builder | |||
public class TaskCardButton { | |||
private String key; | |||
private String name; | |||
private String replaceName; | |||
private String color; | |||
private Boolean bold; | |||
} |
@@ -0,0 +1,65 @@ | |||
package me.chanjar.weixin.cp.api.impl; | |||
import com.google.inject.Inject; | |||
import me.chanjar.weixin.common.error.WxErrorException; | |||
import me.chanjar.weixin.cp.api.ApiTestModule; | |||
import me.chanjar.weixin.cp.api.WxCpService; | |||
import me.chanjar.weixin.cp.bean.WxCpMessage; | |||
import me.chanjar.weixin.cp.bean.WxCpMessageSendResult; | |||
import me.chanjar.weixin.cp.bean.taskcard.TaskCardButton; | |||
import org.testng.annotations.Guice; | |||
import org.testng.annotations.Test; | |||
import java.util.Arrays; | |||
import static org.testng.Assert.assertNotNull; | |||
/** | |||
* 测试任务卡片服务 | |||
* | |||
* @author <a href="https://github.com/domainname">Jeff</a> | |||
* @date 2019-05-16 | |||
*/ | |||
@Guice(modules = ApiTestModule.class) | |||
public class WxCpTaskCardServiceImplTest { | |||
@Inject | |||
private WxCpService wxCpService; | |||
@Test | |||
public void testSendTaskCard() throws WxErrorException { | |||
TaskCardButton btn1 = TaskCardButton.builder() | |||
.key("key1") | |||
.name("同意") | |||
.replaceName("已同意") | |||
.bold(true) | |||
.build(); | |||
TaskCardButton btn2 = TaskCardButton.builder() | |||
.key("key2") | |||
.name("拒绝") | |||
.replaceName("已拒绝") | |||
.color("red") | |||
.build(); | |||
WxCpMessage message = WxCpMessage.TASKCARD() | |||
.toUser("jeff|mr.t") | |||
.title("有一个待审批的请求") | |||
.description("申请:购买图书\n金额:100 元") | |||
.taskId("task_1") | |||
.url("http://www.qq.com") | |||
.buttons(Arrays.asList(btn1, btn2)) | |||
.build(); | |||
WxCpMessageSendResult messageSendResult = this.wxCpService.messageSend(message); | |||
assertNotNull(messageSendResult); | |||
System.out.println(messageSendResult); | |||
System.out.println(messageSendResult.getInvalidPartyList()); | |||
System.out.println(messageSendResult.getInvalidUserList()); | |||
System.out.println(messageSendResult.getInvalidTagList()); | |||
} | |||
@Test | |||
public void testUpdate() throws Exception { | |||
wxCpService.getTaskCardService().update(Arrays.asList("jeff", "mr.t"), "task_1", "key1"); | |||
} | |||
} |
@@ -1,9 +1,11 @@ | |||
package me.chanjar.weixin.cp.bean; | |||
import org.testng.annotations.*; | |||
import me.chanjar.weixin.cp.bean.article.MpnewsArticle; | |||
import me.chanjar.weixin.cp.bean.article.NewArticle; | |||
import me.chanjar.weixin.cp.bean.taskcard.TaskCardButton; | |||
import org.testng.annotations.Test; | |||
import java.util.Arrays; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
@@ -67,7 +69,7 @@ public class WxCpMessageTest { | |||
WxCpMessage reply = WxCpMessage.NEWS().toUser("OPENID").addArticle(article1).addArticle(article2).build(); | |||
assertThat(reply.toJson()) | |||
.isEqualTo( "{\"touser\":\"OPENID\",\"msgtype\":\"news\",\"news\":{\"articles\":" + | |||
.isEqualTo("{\"touser\":\"OPENID\",\"msgtype\":\"news\",\"news\":{\"articles\":" + | |||
"[{\"title\":\"Happy Day\",\"description\":\"Is Really A Happy Day\",\"url\":\"URL\",\"picurl\":\"PIC_URL\"}," + | |||
"{\"title\":\"Happy Day\",\"description\":\"Is Really A Happy Day\",\"url\":\"URL\",\"picurl\":\"PIC_URL\"}]}," + | |||
"\"safe\":\"0\"}"); | |||
@@ -97,7 +99,7 @@ public class WxCpMessageTest { | |||
WxCpMessage reply = WxCpMessage.MPNEWS().toUser("OPENID").addArticle(article1, article2).build(); | |||
assertThat(reply.toJson()) | |||
.isEqualTo( "{\"touser\":\"OPENID\",\"msgtype\":\"mpnews\",\"mpnews\":{\"articles\":" + | |||
.isEqualTo("{\"touser\":\"OPENID\",\"msgtype\":\"mpnews\",\"mpnews\":{\"articles\":" + | |||
"[{\"title\":\"Happy Day\",\"thumb_media_id\":\"thumb\",\"author\":\"aaaaaa\"," + | |||
"\"content_source_url\":\"nice url\",\"content\":\"hahaha\",\"digest\":\"digest\",\"show_cover_pic\":\"heihei\"}" + | |||
",{\"title\":\"Happy Day\",\"thumb_media_id\":\"thumb\",\"author\":\"aaaaaa\"," + | |||
@@ -112,4 +114,30 @@ public class WxCpMessageTest { | |||
.isEqualTo("{\"touser\":\"OPENID\",\"msgtype\":\"mpnews\",\"mpnews\":{\"media_id\":\"mmm\"},\"safe\":\"0\"}"); | |||
} | |||
public void testTaskCardBuilder() { | |||
TaskCardButton button1 = TaskCardButton.builder() | |||
.key("yes") | |||
.name("批准") | |||
.replaceName("已批准") | |||
.color("blue") | |||
.bold(true) | |||
.build(); | |||
TaskCardButton button2 = TaskCardButton.builder() | |||
.key("yes") | |||
.name("拒绝") | |||
.replaceName("已拒绝") | |||
.color("red") | |||
.bold(false) | |||
.build(); | |||
WxCpMessage reply = WxCpMessage.TASKCARD().toUser("OPENID") | |||
.title("任务卡片") | |||
.description("有一条待处理任务") | |||
.url("http://www.qq.com") | |||
.taskId("task_123") | |||
.buttons(Arrays.asList(button1, button2)) | |||
.build(); | |||
assertThat(reply.toJson()) | |||
.isEqualTo("{\"touser\":\"OPENID\",\"msgtype\":\"taskcard\",\"taskcard\":{\"title\":\"任务卡片\",\"description\":\"有一条待处理任务\",\"url\":\"http://www.qq.com\",\"task_id\":\"task_123\",\"btn\":[{\"key\":\"yes\",\"name\":\"批准\",\"replace_name\":\"已批准\",\"color\":\"blue\",\"is_bold\":true},{\"key\":\"yes\",\"name\":\"拒绝\",\"replace_name\":\"已拒绝\",\"color\":\"red\",\"is_bold\":false}]}}"); | |||
} | |||
} |
@@ -1,9 +1,11 @@ | |||
package me.chanjar.weixin.cp.bean; | |||
import me.chanjar.weixin.common.api.WxConsts; | |||
import org.testng.annotations.*; | |||
import org.testng.annotations.Test; | |||
import static org.testng.Assert.*; | |||
import static me.chanjar.weixin.cp.WxCpConsts.EventType.TASKCARD_CLICK; | |||
import static org.testng.Assert.assertEquals; | |||
import static org.testng.Assert.assertNotNull; | |||
@Test | |||
public class WxCpXmlMessageTest { | |||
@@ -149,4 +151,26 @@ public class WxCpXmlMessageTest { | |||
assertEquals(wxMessage.getExtAttrs().getItems().get(0).getName(), "爱好"); | |||
} | |||
public void testTaskCardEvent() { | |||
String xml = "<xml>" + | |||
"<ToUserName><![CDATA[toUser]]></ToUserName>" + | |||
"<FromUserName><![CDATA[FromUser]]></FromUserName>" + | |||
"<CreateTime>123456789</CreateTime>" + | |||
"<MsgType><![CDATA[event]]></MsgType>" + | |||
"<Event><![CDATA[taskcard_click]]></Event>" + | |||
"<EventKey><![CDATA[key111]]></EventKey>" + | |||
"<TaskId><![CDATA[taskid111]]></TaskId >" + | |||
"<AgentID>1</AgentID>" + | |||
"</xml>"; | |||
WxCpXmlMessage wxMessage = WxCpXmlMessage.fromXml(xml); | |||
assertEquals(wxMessage.getToUserName(), "toUser"); | |||
assertEquals(wxMessage.getFromUserName(), "FromUser"); | |||
assertEquals(wxMessage.getCreateTime(), Long.valueOf(123456789L)); | |||
assertEquals(wxMessage.getMsgType(), WxConsts.XmlMsgType.EVENT); | |||
assertEquals(wxMessage.getAgentId(), Integer.valueOf(1)); | |||
assertEquals(wxMessage.getEvent(), TASKCARD_CLICK); | |||
assertEquals(wxMessage.getEventKey(), "key111"); | |||
assertEquals(wxMessage.getTaskId(), "taskid111"); | |||
} | |||
} |
@@ -109,6 +109,7 @@ public class WxMpKefuMessage implements Serializable { | |||
* {@link WxConsts.KefuMsgType#MPNEWS} | |||
* {@link WxConsts.KefuMsgType#WXCARD} | |||
* {@link WxConsts.KefuMsgType#MINIPROGRAMPAGE} | |||
* {@link WxConsts.KefuMsgType#TASKCARD} | |||
* </pre> | |||
* | |||
*/ | |||