diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelKfService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelKfService.java new file mode 100644 index 0000000000..316c3eaf88 --- /dev/null +++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelKfService.java @@ -0,0 +1,48 @@ +package me.chanjar.weixin.channel.api; + +import me.chanjar.weixin.channel.bean.kf.WxChannelKfSendMsgParam; +import me.chanjar.weixin.channel.bean.kf.WxChannelKfSendMsgResponse; +import me.chanjar.weixin.common.error.WxErrorException; + +/** + * 视频号小店 商家客服服务 + * + * @author GitHub Copilot + */ +public interface WxChannelKfService { + + /** + * 上传多媒体资源. + * + * @param openId 用户 open_id + * @param msgType 文件类型,仅支持:video/file/image + * @param file 文件字节内容 + * @return cos_url + * + * @throws WxErrorException 异常 + */ + String uploadMedia(String openId, String msgType, byte[] file) throws WxErrorException; + + /** + * 上传多媒体资源. + * + * @param openId 用户 open_id + * @param msgType 文件类型,仅支持:video/file/image + * @param fileName 文件名 + * @param file 文件字节内容 + * @return cos_url + * + * @throws WxErrorException 异常 + */ + String uploadMedia(String openId, String msgType, String fileName, byte[] file) throws WxErrorException; + + /** + * 发送客服消息. + * + * @param param 请求参数 + * @return 发送结果 + * + * @throws WxErrorException 异常 + */ + WxChannelKfSendMsgResponse sendMessage(WxChannelKfSendMsgParam param) throws WxErrorException; +} diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelService.java index 50a029c196..e1c63aee5b 100644 --- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelService.java +++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelService.java @@ -182,4 +182,11 @@ public interface WxChannelService extends BaseWxChannelService { */ WxChannelLiveDashboardService getLiveDashboardService(); + /** + * 商家客服服务 + * + * @return 商家客服服务 + */ + WxChannelKfService getKfService(); + } diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/BaseWxChannelServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/BaseWxChannelServiceImpl.java index 1a608e1f6a..4d9e86aaf6 100644 --- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/BaseWxChannelServiceImpl.java +++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/BaseWxChannelServiceImpl.java @@ -60,6 +60,7 @@ public abstract class BaseWxChannelServiceImpl implements WxChannelService private WxChannelVipService vipService = null; private WxChannelCompassFinderService compassFinderService = null; private WxChannelLiveDashboardService liveDashboardService = null; + private WxChannelKfService kfService = null; protected WxChannelConfig config; private int retrySleepMillis = 1000; @@ -473,4 +474,12 @@ public synchronized WxChannelLiveDashboardService getLiveDashboardService() { return liveDashboardService; } + @Override + public synchronized WxChannelKfService getKfService() { + if (kfService == null) { + kfService = new WxChannelKfServiceImpl(this); + } + return kfService; + } + } diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelKfServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelKfServiceImpl.java new file mode 100644 index 0000000000..bd32006ae5 --- /dev/null +++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelKfServiceImpl.java @@ -0,0 +1,50 @@ +package me.chanjar.weixin.channel.api.impl; + +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.channel.api.WxChannelKfService; +import me.chanjar.weixin.channel.bean.kf.WxChannelKfCosUploadResponse; +import me.chanjar.weixin.channel.bean.kf.WxChannelKfSendMsgParam; +import me.chanjar.weixin.channel.bean.kf.WxChannelKfSendMsgResponse; +import me.chanjar.weixin.channel.util.ResponseUtils; +import me.chanjar.weixin.common.bean.CommonUploadParam; +import me.chanjar.weixin.common.error.WxErrorException; + +import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Kf.COS_UPLOAD_URL; +import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Kf.SEND_MSG_URL; + +/** + * 视频号小店 商家客服服务实现 + * + * @author GitHub Copilot + */ +@Slf4j +public class WxChannelKfServiceImpl implements WxChannelKfService { + + /** 微信商店服务 */ + private final BaseWxChannelServiceImpl shopService; + + public WxChannelKfServiceImpl(BaseWxChannelServiceImpl shopService) { + this.shopService = shopService; + } + + @Override + public String uploadMedia(String openId, String msgType, byte[] file) throws WxErrorException { + return uploadMedia(openId, msgType, null, file); + } + + @Override + public String uploadMedia(String openId, String msgType, String fileName, byte[] file) throws WxErrorException { + CommonUploadParam uploadParam = CommonUploadParam.fromBytes("file", fileName, file) + .addFormField("open_id", openId) + .addFormField("msg_type", msgType); + String resJson = shopService.upload(COS_UPLOAD_URL, uploadParam); + WxChannelKfCosUploadResponse response = ResponseUtils.decode(resJson, WxChannelKfCosUploadResponse.class); + return response.getCosUrl(); + } + + @Override + public WxChannelKfSendMsgResponse sendMessage(WxChannelKfSendMsgParam param) throws WxErrorException { + String resJson = shopService.post(SEND_MSG_URL, param); + return ResponseUtils.decode(resJson, WxChannelKfSendMsgResponse.class); + } +} diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfCosUploadResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfCosUploadResponse.java new file mode 100644 index 0000000000..a84ab7c520 --- /dev/null +++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfCosUploadResponse.java @@ -0,0 +1,24 @@ +package me.chanjar.weixin.channel.bean.kf; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse; + +import java.io.Serializable; + +/** + * 上传多媒体资源返回结果 + * + * @author GitHub Copilot + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class WxChannelKfCosUploadResponse extends WxChannelBaseResponse implements Serializable { + + private static final long serialVersionUID = -8073026558742450133L; + + /** 多媒体 cos_url */ + @JsonProperty("cos_url") + private String cosUrl; +} diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfSendMsgParam.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfSendMsgParam.java new file mode 100644 index 0000000000..7b1528b02d --- /dev/null +++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfSendMsgParam.java @@ -0,0 +1,104 @@ +package me.chanjar.weixin.channel.bean.kf; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 发送客服消息请求参数 + * + * @author GitHub Copilot + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@JsonInclude(Include.NON_NULL) +public class WxChannelKfSendMsgParam implements Serializable { + + private static final long serialVersionUID = -7384287911696365032L; + + /** 唯一任务ID,填写后按任务ID去重 */ + @JsonProperty("request_id") + private String requestId; + + /** 用户 open_id */ + @JsonProperty("open_id") + private String openId; + + /** 消息类型 */ + @JsonProperty("msg_type") + private String msgType; + + /** 文本消息 */ + @JsonProperty("text") + private TextMessage text; + + /** 图片消息 */ + @JsonProperty("image") + private CosUrlMessage image; + + /** 视频消息 */ + @JsonProperty("video") + private CosUrlMessage video; + + /** 文件消息 */ + @JsonProperty("file") + private CosUrlMessage file; + + /** 商品卡片消息 */ + @JsonProperty("product_share") + private ProductShareMessage productShare; + + /** 订单卡片消息 */ + @JsonProperty("order_share") + private OrderShareMessage orderShare; + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class TextMessage implements Serializable { + + private static final long serialVersionUID = -5001585611550636499L; + + @JsonProperty("content") + private String content; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class CosUrlMessage implements Serializable { + + private static final long serialVersionUID = 8403720861098936947L; + + @JsonProperty("cos_url") + private String cosUrl; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class ProductShareMessage implements Serializable { + + private static final long serialVersionUID = -3049552399099249795L; + + @JsonProperty("product_id") + private String productId; + } + + @Data + @NoArgsConstructor + @AllArgsConstructor + public static class OrderShareMessage implements Serializable { + + private static final long serialVersionUID = 7136546635145180607L; + + @JsonProperty("order_id") + private String orderId; + } +} diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfSendMsgResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfSendMsgResponse.java new file mode 100644 index 0000000000..35547f155d --- /dev/null +++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfSendMsgResponse.java @@ -0,0 +1,24 @@ +package me.chanjar.weixin.channel.bean.kf; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse; + +import java.io.Serializable; + +/** + * 发送客服消息返回结果 + * + * @author GitHub Copilot + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class WxChannelKfSendMsgResponse extends WxChannelBaseResponse implements Serializable { + + private static final long serialVersionUID = -4994877385473101709L; + + /** 消息ID */ + @JsonProperty("msg_id") + private String msgId; +} diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java index 6c2076d85b..5d00f78f2c 100644 --- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java +++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java @@ -217,6 +217,15 @@ public interface Order { String DECODE_SENSITIVE_INFO_URL = "https://api.weixin.qq.com/channels/ec/order/sensitiveinfo/decode"; } + /** 商家客服相关接口 */ + public interface Kf { + + /** 上传多媒体资源 */ + String COS_UPLOAD_URL = "https://api.weixin.qq.com/channels/ec/commkf/cosupload"; + /** 发送消息 */ + String SEND_MSG_URL = "https://api.weixin.qq.com/channels/ec/commkf/sendmsg"; + } + /** 售后相关接口 */ public interface AfterSale { diff --git a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfBeanTest.java b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfBeanTest.java new file mode 100644 index 0000000000..b55c05455b --- /dev/null +++ b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfBeanTest.java @@ -0,0 +1,50 @@ +package me.chanjar.weixin.channel.bean.kf; + +import me.chanjar.weixin.channel.util.JsonUtils; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +/** + * @author GitHub Copilot + */ +public class WxChannelKfBeanTest { + + @Test + public void testSendMsgParamEncode() { + WxChannelKfSendMsgParam param = new WxChannelKfSendMsgParam(); + param.setRequestId("63abd34b-656b-4082-b364-5f74226e1a20"); + param.setOpenId("o7eep4jVQelr2eyoDSmE1xxxxxx"); + param.setMsgType("text"); + param.setText(new WxChannelKfSendMsgParam.TextMessage("测试消息123")); + + String json = JsonUtils.encode(param); + assertNotNull(json); + assertTrue(json.contains("\"request_id\":\"63abd34b-656b-4082-b364-5f74226e1a20\"")); + assertTrue(json.contains("\"open_id\":\"o7eep4jVQelr2eyoDSmE1xxxxxx\"")); + assertTrue(json.contains("\"msg_type\":\"text\"")); + assertTrue(json.contains("\"text\":{\"content\":\"测试消息123\"}")); + } + + @Test + public void testSendMsgResponseDecode() { + String json = "{\"msg_id\":\"3886839959369302016\",\"errmsg\":\"ok\",\"errcode\":0}"; + WxChannelKfSendMsgResponse response = JsonUtils.decode(json, WxChannelKfSendMsgResponse.class); + assertNotNull(response); + assertEquals(response.getMsgId(), "3886839959369302016"); + assertEquals(response.getErrCode(), 0); + assertEquals(response.getErrMsg(), "ok"); + } + + @Test + public void testCosUploadResponseDecode() { + String json = "{\"cos_url\":\"https://channels.weixin.qq.com/shop/commkf/downloadmedia?encrypted_param=xxxxx×tamp=xxxxx&openid=xxxxxx&msg_type=7\",\"errmsg\":\"ok\",\"errcode\":0}"; + WxChannelKfCosUploadResponse response = JsonUtils.decode(json, WxChannelKfCosUploadResponse.class); + assertNotNull(response); + assertTrue(response.getCosUrl().contains("channels.weixin.qq.com/shop/commkf/downloadmedia")); + assertEquals(response.getErrCode(), 0); + assertEquals(response.getErrMsg(), "ok"); + } +}