diff --git a/docs/superpowers/plans/2026-08-22-channel-merchant-kf.md b/docs/superpowers/plans/2026-08-22-channel-merchant-kf.md new file mode 100644 index 000000000..99aa235cb --- /dev/null +++ b/docs/superpowers/plans/2026-08-22-channel-merchant-kf.md @@ -0,0 +1,45 @@ +# 视频号小店商家客服 API Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 为 `weixin-java-channel` 增加商家客服媒体上传和消息发送 API。 + +**Architecture:** 以独立的客服子服务封装两条官方 API,通过既有 `BaseWxChannelServiceImpl` 完成鉴权、JSON 请求与 multipart 上传。请求模型保持强类型,服务层测试使用可记录调用的测试替身,避免真实网络依赖。 + +**Tech Stack:** Java 8、Maven、TestNG、Lombok、Jackson 注解。 + +## Global Constraints + +- Java 8 兼容,不新增依赖。 +- API 路径固定为 `/channels/ec/commkf/cosupload` 和 `/channels/ec/commkf/sendmsg`。 +- 使用 TestNG,所有新增测试不使用真实微信凭据。 + +--- + +### Task 1: 请求与响应模型 + +**Files:** +- Create: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfCosUploadResponse.java` +- Create: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfSendMsgParam.java` +- Create: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfSendMsgResponse.java` +- Test: `weixin-java-channel/src/test/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfBeanTest.java` + +- [ ] Write JSON encode/decode tests for `request_id`, `open_id`, `msg_type`, `text.content`, `cos_url` and `msg_id`. +- [ ] Run the test and verify it fails because the classes do not exist. +- [ ] Add the minimal annotated model classes and nested message content types. +- [ ] Run the test and verify it passes. + +### Task 2: 服务入口与请求执行 + +**Files:** +- Create: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelKfService.java` +- Create: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelKfServiceImpl.java` +- Modify: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelService.java` +- Modify: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/BaseWxChannelServiceImpl.java` +- Modify: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java` +- Test: `weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelKfServiceImplTest.java` + +- [ ] Write tests proving the service uses the documented URLs, uploads `file`, `open_id`, `msg_type`, decodes both responses, and caches `getKfService()`. +- [ ] Run the test and verify it fails because the API is absent. +- [ ] Add the minimal service API, implementation, constants and cached service entry point. +- [ ] Run the focused tests and module test suite, then inspect `git diff --check`. diff --git a/docs/superpowers/specs/2026-08-22-channel-merchant-kf-design.md b/docs/superpowers/specs/2026-08-22-channel-merchant-kf-design.md new file mode 100644 index 000000000..12ecfd95c --- /dev/null +++ b/docs/superpowers/specs/2026-08-22-channel-merchant-kf-design.md @@ -0,0 +1,18 @@ +# 视频号小店商家客服 API 设计 + +## 目标 + +实现 Issue #3991 所列的商家客服媒体上传与消息发送 API,并使调用入口、请求模型和响应解析与现有 `weixin-java-channel` 服务保持一致。 + +## 设计 + +- 在 `WxChannelService` 暴露 `getKfService()`,由 `BaseWxChannelServiceImpl` 缓存并懒加载 `WxChannelKfServiceImpl`。 +- `WxChannelKfService` 提供媒体上传(带文件名和便捷重载)以及接收强类型请求参数的消息发送方法。上传请求通过既有 `CommonUploadParam` 发送 multipart 数据。 +- 消息模型用 `@JsonProperty` 显式映射微信字段,支持 text、image、video、file、product_share 和 order_share 六类内容;响应继承项目既有基础响应。 +- API 常量使用官方文档确认的 `/channels/ec/commkf/cosupload` 和 `/channels/ec/commkf/sendmsg` 路径。 + +## 质量边界 + +- 保持 Java 8 兼容,不增加依赖,不变更现有公共 API。 +- 使用 TestNG 覆盖请求/响应 JSON 映射、服务 URL、上传表单字段及服务入口缓存;测试不依赖真实微信凭据。 +- PR 使用 `Closes #3991` 关联并关闭原始 Issue;旧 PR #4037 在新 PR 创建后以替代说明关闭。 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 000000000..41fb153eb --- /dev/null +++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelKfService.java @@ -0,0 +1,41 @@ +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; + +/** 视频号小店商家客服服务。 */ +public interface WxChannelKfService { + + /** + * 上传多媒体资源。 + * + * @param openId 用户 open_id + * @param msgType 文件类型,仅支持 video、file、image + * @param file 文件字节内容 + * @return COS 地址 + * @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 地址 + * @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 52cc924bc..5a4c4d3d4 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 @@ -7,6 +7,15 @@ */ public interface WxChannelService extends BaseWxChannelService { + /** + * 商家客服服务。 + * + * @return 商家客服服务 + */ + default WxChannelKfService getKfService() { + throw new UnsupportedOperationException("WxChannelService implementation does not support 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 b167af0d4..427a24d3b 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 @@ -72,6 +72,7 @@ public abstract class BaseWxChannelServiceImpl implements WxChannelService private WxTalentService talentService = null; private WxChannelFavoriteService favoriteService = null; private WxChannelEwaybillService ewaybillService = null; + private WxChannelKfService kfService = null; protected WxChannelConfig config; private int retrySleepMillis = 1000; @@ -548,4 +549,12 @@ public synchronized WxChannelEwaybillService getEwaybillService() { return ewaybillService; } + @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 000000000..21bef92e1 --- /dev/null +++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelKfServiceImpl.java @@ -0,0 +1,45 @@ +package me.chanjar.weixin.channel.api.impl; + +import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Kf.COS_UPLOAD_URL; +import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Kf.SEND_MSG_URL; + +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.JsonUtils; +import me.chanjar.weixin.channel.util.ResponseUtils; +import me.chanjar.weixin.common.bean.CommonUploadParam; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor; + +/** 视频号小店商家客服服务实现。 */ +public class WxChannelKfServiceImpl implements WxChannelKfService { + + private final BaseWxChannelServiceImpl channelService; + + public WxChannelKfServiceImpl(BaseWxChannelServiceImpl channelService) { + this.channelService = channelService; + } + + @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 responseJson = channelService.upload(COS_UPLOAD_URL, uploadParam); + return ResponseUtils.decode(responseJson, WxChannelKfCosUploadResponse.class).getCosUrl(); + } + + @Override + public WxChannelKfSendMsgResponse sendMessage(WxChannelKfSendMsgParam param) throws WxErrorException { + String responseJson = channelService.executeWithoutLog(SimplePostRequestExecutor.create(channelService), SEND_MSG_URL, + JsonUtils.encode(param)); + return ResponseUtils.decode(responseJson, 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 000000000..e5261dbf6 --- /dev/null +++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfCosUploadResponse.java @@ -0,0 +1,20 @@ +package me.chanjar.weixin.channel.bean.kf; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse; + +/** 客服素材上传响应。 */ +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class WxChannelKfCosUploadResponse extends WxChannelBaseResponse { + + private static final long serialVersionUID = 1L; + + /** 素材在 COS 上的地址。 */ + @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 000000000..0653c5edc --- /dev/null +++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfSendMsgParam.java @@ -0,0 +1,90 @@ +package me.chanjar.weixin.channel.bean.kf; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.Serializable; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** 发送客服消息请求参数。 */ +@Data +@NoArgsConstructor +public class WxChannelKfSendMsgParam implements Serializable { + + private static final long serialVersionUID = 1L; + + /** 请求幂等标识。 */ + @JsonProperty("request_id") + private String requestId; + + /** 接收消息的用户 openid。 */ + @JsonProperty("open_id") + private String openId; + + /** 消息类型。 */ + @JsonProperty("msg_type") + private String msgType; + + /** 文本消息内容。 */ + @JsonProperty("text") + private Text 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 + public static class Text implements Serializable { + + private static final long serialVersionUID = 1L; + + @JsonProperty("content") + private String content; + } + + @Data + @NoArgsConstructor + public static class CosUrlMessage implements Serializable { + + private static final long serialVersionUID = 1L; + + @JsonProperty("cos_url") + private String cosUrl; + } + + @Data + @NoArgsConstructor + public static class ProductShareMessage implements Serializable { + + private static final long serialVersionUID = 1L; + + @JsonProperty("product_id") + private String productId; + } + + @Data + @NoArgsConstructor + public static class OrderShareMessage implements Serializable { + + private static final long serialVersionUID = 1L; + + @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 000000000..570e309fa --- /dev/null +++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfSendMsgResponse.java @@ -0,0 +1,20 @@ +package me.chanjar.weixin.channel.bean.kf; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse; + +/** 发送客服消息响应。 */ +@Data +@NoArgsConstructor +@EqualsAndHashCode(callSuper = true) +public class WxChannelKfSendMsgResponse extends WxChannelBaseResponse { + + private static final long serialVersionUID = 1L; + + /** 消息 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 86de88947..105879dae 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 @@ -49,6 +49,15 @@ public interface Favorite { String GET_FAVORITE_COUNT = "https://api.weixin.qq.com/channels/ec/favorites/count/get"; } + /** 商家客服相关接口 */ + 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 Category { diff --git a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelKfServiceImplTest.java b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelKfServiceImplTest.java new file mode 100644 index 000000000..02353aab3 --- /dev/null +++ b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelKfServiceImplTest.java @@ -0,0 +1,120 @@ +package me.chanjar.weixin.channel.api.impl; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertSame; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import me.chanjar.weixin.channel.bean.kf.WxChannelKfSendMsgParam; +import me.chanjar.weixin.channel.bean.kf.WxChannelKfSendMsgResponse; +import me.chanjar.weixin.channel.util.JsonUtils; +import me.chanjar.weixin.common.bean.CommonUploadParam; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.RequestExecutor; +import org.testng.annotations.Test; + +/** 商家客服服务离线测试。 */ +public class WxChannelKfServiceImplTest { + + @Test + public void shouldUploadMediaWithDocumentedUrlAndMultipartFields() throws WxErrorException { + RecordingChannelService channelService = new RecordingChannelService(); + channelService.uploadResult = "{\"errcode\":0,\"errmsg\":\"ok\",\"cos_url\":\"https://cos.example.com/image.png\"}"; + + String cosUrl = new WxChannelKfServiceImpl(channelService) + .uploadMedia("open-id", "image", "image.png", new byte[]{1, 2, 3}); + + assertEquals(channelService.uploadUrl, "https://api.weixin.qq.com/channels/ec/commkf/cosupload"); + assertNotNull(channelService.uploadParam); + assertEquals(channelService.uploadParam.getName(), "file"); + assertEquals(channelService.uploadParam.getData().getFileName(), "image.png"); + assertEquals(channelService.uploadParam.getData().readAllBytes(), new byte[]{1, 2, 3}); + assertEquals(channelService.uploadParam.getFormFields().get("open_id"), "open-id"); + assertEquals(channelService.uploadParam.getFormFields().get("msg_type"), "image"); + assertEquals(cosUrl, "https://cos.example.com/image.png"); + } + + @Test + public void shouldSendJsonMessageAndDecodeResponse() throws WxErrorException { + RecordingChannelService channelService = new RecordingChannelService(); + channelService.postResult = "{\"errcode\":0,\"errmsg\":\"ok\",\"msg_id\":\"message-id\"}"; + WxChannelKfSendMsgParam param = new WxChannelKfSendMsgParam(); + param.setRequestId("request-id"); + param.setOpenId("open-id"); + param.setMsgType("text"); + WxChannelKfSendMsgParam.Text text = new WxChannelKfSendMsgParam.Text(); + text.setContent("hello"); + param.setText(text); + + WxChannelKfSendMsgResponse response = new WxChannelKfServiceImpl(channelService).sendMessage(param); + + assertEquals(channelService.postUrl, "https://api.weixin.qq.com/channels/ec/commkf/sendmsg"); + assertEquals(channelService.postJson, + "{\"request_id\":\"request-id\",\"open_id\":\"open-id\",\"msg_type\":\"text\",\"text\":{\"content\":\"hello\"}}"); + assertEquals(channelService.executeWithoutLogCalled, true); + assertEquals(response.getMsgId(), "message-id"); + assertEquals(response.getErrCode(), 0); + } + + @Test + public void shouldProvideFreshUploadStreamForEachAttempt() throws IOException { + CommonUploadParam uploadParam = CommonUploadParam.fromBytes("file", "image.png", new byte[]{1, 2, 3}); + + assertEquals(readAllBytes(uploadParam.getData().getInputStream()), new byte[]{1, 2, 3}); + assertEquals(readAllBytes(uploadParam.getData().getInputStream()), new byte[]{1, 2, 3}); + } + + @Test + public void shouldCacheKfServiceEntryPoint() { + WxChannelServiceImpl channelService = new WxChannelServiceImpl(); + + assertSame(channelService.getKfService(), channelService.getKfService()); + } + + private static class RecordingChannelService extends WxChannelServiceImpl { + + private String uploadResult; + private String postResult; + private String uploadUrl; + private CommonUploadParam uploadParam; + private String postUrl; + private String postJson; + private boolean executeWithoutLogCalled; + + @Override + public String upload(String url, CommonUploadParam param) { + this.uploadUrl = url; + this.uploadParam = param; + return uploadResult; + } + + @Override + public String post(String url, Object obj) { + this.postUrl = url; + this.postJson = JsonUtils.encode(obj); + return postResult; + } + + @Override + @SuppressWarnings("unchecked") + public T executeWithoutLog(RequestExecutor executor, String uri, E data) { + this.executeWithoutLogCalled = true; + this.postUrl = uri; + this.postJson = (String) data; + return (T) postResult; + } + } + + private byte[] readAllBytes(InputStream inputStream) throws IOException { + try (InputStream stream = inputStream; ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + byte[] buffer = new byte[16]; + int count; + while ((count = stream.read(buffer)) != -1) { + outputStream.write(buffer, 0, count); + } + return outputStream.toByteArray(); + } + } +} 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 000000000..458cf44f2 --- /dev/null +++ b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfBeanTest.java @@ -0,0 +1,141 @@ +package me.chanjar.weixin.channel.bean.kf; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import me.chanjar.weixin.channel.util.JsonUtils; +import org.testng.annotations.Test; + +/** JSON serialization tests for channel customer service models. */ +public class WxChannelKfBeanTest { + + @Test + public void testSendMsgParamJson() { + WxChannelKfSendMsgParam param = createSendMsgParam("text"); + WxChannelKfSendMsgParam.Text text = new WxChannelKfSendMsgParam.Text(); + text.setContent("hello"); + param.setText(text); + + String json = JsonUtils.encode(param); + assertNotNull(json); + assertFalse(json.contains("requestId")); + assertFalse(json.contains("openId")); + assertFalse(json.contains("msgType")); + WxChannelKfSendMsgParam decoded = JsonUtils.decode(json, WxChannelKfSendMsgParam.class); + assertEquals(decoded.getRequestId(), "request-1"); + assertEquals(decoded.getOpenId(), "open-1"); + assertEquals(decoded.getMsgType(), "text"); + assertNotNull(decoded.getText()); + assertEquals(decoded.getText().getContent(), "hello"); + } + + @Test + public void testImageMessageJson() { + WxChannelKfSendMsgParam param = createSendMsgParam("image"); + WxChannelKfSendMsgParam.CosUrlMessage image = new WxChannelKfSendMsgParam.CosUrlMessage(); + image.setCosUrl("https://example.test/image"); + param.setImage(image); + + assertCosUrlMessageJson(param, "image", "https://example.test/image"); + } + + @Test + public void testVideoMessageJson() { + WxChannelKfSendMsgParam param = createSendMsgParam("video"); + WxChannelKfSendMsgParam.CosUrlMessage video = new WxChannelKfSendMsgParam.CosUrlMessage(); + video.setCosUrl("https://example.test/video"); + param.setVideo(video); + + assertCosUrlMessageJson(param, "video", "https://example.test/video"); + } + + @Test + public void testFileMessageJson() { + WxChannelKfSendMsgParam param = createSendMsgParam("file"); + WxChannelKfSendMsgParam.CosUrlMessage file = new WxChannelKfSendMsgParam.CosUrlMessage(); + file.setCosUrl("https://example.test/file"); + param.setFile(file); + + assertCosUrlMessageJson(param, "file", "https://example.test/file"); + } + + @Test + public void testProductShareMessageJson() { + WxChannelKfSendMsgParam param = createSendMsgParam("product_share"); + WxChannelKfSendMsgParam.ProductShareMessage product = + new WxChannelKfSendMsgParam.ProductShareMessage(); + product.setProductId("product-1"); + param.setProductShare(product); + + String json = JsonUtils.encode(param); + assertTrue(json.contains("\"product_share\":{\"product_id\":\"product-1\"}")); + assertFalse(json.contains("productShare")); + assertFalse(json.contains("productId")); + WxChannelKfSendMsgParam decoded = JsonUtils.decode(json, WxChannelKfSendMsgParam.class); + assertEquals(decoded.getProductShare().getProductId(), "product-1"); + } + + @Test + public void testOrderShareMessageJson() { + WxChannelKfSendMsgParam param = createSendMsgParam("order_share"); + WxChannelKfSendMsgParam.OrderShareMessage order = new WxChannelKfSendMsgParam.OrderShareMessage(); + order.setOrderId("order-1"); + param.setOrderShare(order); + + String json = JsonUtils.encode(param); + assertTrue(json.contains("\"order_share\":{\"order_id\":\"order-1\"}")); + assertFalse(json.contains("orderShare")); + assertFalse(json.contains("orderId")); + WxChannelKfSendMsgParam decoded = JsonUtils.decode(json, WxChannelKfSendMsgParam.class); + assertEquals(decoded.getOrderShare().getOrderId(), "order-1"); + } + + @Test + public void testCosUploadResponseJson() { + WxChannelKfCosUploadResponse response = JsonUtils.decode( + "{\"errcode\":0,\"errmsg\":\"ok\",\"cos_url\":\"https://example.test/media\"}", + WxChannelKfCosUploadResponse.class); + + assertEquals(response.getErrCode(), 0); + assertEquals(response.getCosUrl(), "https://example.test/media"); + assertEquals(JsonUtils.decode(JsonUtils.encode(response), WxChannelKfCosUploadResponse.class) + .getCosUrl(), "https://example.test/media"); + } + + @Test + public void testSendMsgResponseJson() { + WxChannelKfSendMsgResponse response = JsonUtils.decode( + "{\"errcode\":0,\"errmsg\":\"ok\",\"msg_id\":\"msg-1\"}", + WxChannelKfSendMsgResponse.class); + + assertEquals(response.getErrCode(), 0); + assertEquals(response.getMsgId(), "msg-1"); + assertEquals(JsonUtils.decode(JsonUtils.encode(response), WxChannelKfSendMsgResponse.class) + .getMsgId(), "msg-1"); + } + + private WxChannelKfSendMsgParam createSendMsgParam(String msgType) { + WxChannelKfSendMsgParam param = new WxChannelKfSendMsgParam(); + param.setRequestId("request-1"); + param.setOpenId("open-1"); + param.setMsgType(msgType); + return param; + } + + private void assertCosUrlMessageJson(WxChannelKfSendMsgParam param, String fieldName, + String cosUrl) { + String json = JsonUtils.encode(param); + assertTrue(json.contains("\"" + fieldName + "\":{\"cos_url\":\"" + cosUrl + "\"}")); + assertFalse(json.contains("cosUrl")); + WxChannelKfSendMsgParam decoded = JsonUtils.decode(json, WxChannelKfSendMsgParam.class); + if ("image".equals(fieldName)) { + assertEquals(decoded.getImage().getCosUrl(), cosUrl); + } else if ("video".equals(fieldName)) { + assertEquals(decoded.getVideo().getCosUrl(), cosUrl); + } else { + assertEquals(decoded.getFile().getCosUrl(), cosUrl); + } + } +} diff --git a/weixin-java-channel/src/test/resources/testng.xml b/weixin-java-channel/src/test/resources/testng.xml index f4850bdf2..579acb325 100644 --- a/weixin-java-channel/src/test/resources/testng.xml +++ b/weixin-java-channel/src/test/resources/testng.xml @@ -26,4 +26,10 @@ + + + + + + diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/CommonUploadParam.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/CommonUploadParam.java index 42e186950..34fb66da1 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/CommonUploadParam.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/CommonUploadParam.java @@ -78,7 +78,27 @@ 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), null); + return new CommonUploadParam(name, new ByteArrayUploadData(fileName, bytes), null); + } + + private static class ByteArrayUploadData extends CommonUploadData { + + private final byte[] bytes; + + private ByteArrayUploadData(@Nullable String fileName, byte[] bytes) { + super(fileName, new ByteArrayInputStream(bytes), bytes.length); + this.bytes = bytes; + } + + @Override + public ByteArrayInputStream getInputStream() { + return new ByteArrayInputStream(bytes); + } + + @Override + public byte[] readAllBytes() { + return bytes.clone(); + } } /**