🆕 #4105 【视频号】增加微信小店商家客服接口
This commit is contained in:
45
docs/superpowers/plans/2026-08-22-channel-merchant-kf.md
Normal file
45
docs/superpowers/plans/2026-08-22-channel-merchant-kf.md
Normal file
@@ -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`.
|
||||
@@ -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 创建后以替代说明关闭。
|
||||
@@ -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;
|
||||
}
|
||||
@@ -7,6 +7,15 @@ package me.chanjar.weixin.channel.api;
|
||||
*/
|
||||
public interface WxChannelService extends BaseWxChannelService {
|
||||
|
||||
/**
|
||||
* 商家客服服务。
|
||||
*
|
||||
* @return 商家客服服务
|
||||
*/
|
||||
default WxChannelKfService getKfService() {
|
||||
throw new UnsupportedOperationException("WxChannelService implementation does not support getKfService()");
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础接口服务
|
||||
*
|
||||
|
||||
@@ -72,6 +72,7 @@ public abstract class BaseWxChannelServiceImpl<H, P> 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 abstract class BaseWxChannelServiceImpl<H, P> implements WxChannelService
|
||||
return ewaybillService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized WxChannelKfService getKfService() {
|
||||
if (kfService == null) {
|
||||
kfService = new WxChannelKfServiceImpl(this);
|
||||
}
|
||||
return kfService;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -49,6 +49,15 @@ public class WxChannelApiUrlConstants {
|
||||
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 {
|
||||
|
||||
|
||||
@@ -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, E> T executeWithoutLog(RequestExecutor<T, E> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,4 +26,10 @@
|
||||
<class name="me.chanjar.weixin.channel.api.impl.WxChannelProductAssistantServiceImplTest"/>
|
||||
</classes>
|
||||
</test>
|
||||
<test name="Kf_Service_Test">
|
||||
<classes>
|
||||
<class name="me.chanjar.weixin.channel.bean.kf.WxChannelKfBeanTest"/>
|
||||
<class name="me.chanjar.weixin.channel.api.impl.WxChannelKfServiceImplTest"/>
|
||||
</classes>
|
||||
</test>
|
||||
</suite>
|
||||
|
||||
@@ -78,7 +78,27 @@ public class CommonUploadParam implements Serializable {
|
||||
*/
|
||||
@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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user