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");
+ }
+}