Skip to content

Commit 0122878

Browse files
authored
🆕 #4105 【视频号】增加微信小店商家客服接口
1 parent 5bb9dee commit 0122878

14 files changed

Lines changed: 594 additions & 1 deletion

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# 视频号小店商家客服 API Implementation Plan
2+
3+
> **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.
4+
5+
**Goal:** 为 `weixin-java-channel` 增加商家客服媒体上传和消息发送 API。
6+
7+
**Architecture:** 以独立的客服子服务封装两条官方 API,通过既有 `BaseWxChannelServiceImpl` 完成鉴权、JSON 请求与 multipart 上传。请求模型保持强类型,服务层测试使用可记录调用的测试替身,避免真实网络依赖。
8+
9+
**Tech Stack:** Java 8、Maven、TestNG、Lombok、Jackson 注解。
10+
11+
## Global Constraints
12+
13+
- Java 8 兼容,不新增依赖。
14+
- API 路径固定为 `/channels/ec/commkf/cosupload` 和 `/channels/ec/commkf/sendmsg`。
15+
- 使用 TestNG,所有新增测试不使用真实微信凭据。
16+
17+
---
18+
19+
### Task 1: 请求与响应模型
20+
21+
**Files:**
22+
- Create: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfCosUploadResponse.java`
23+
- Create: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfSendMsgParam.java`
24+
- Create: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfSendMsgResponse.java`
25+
- Test: `weixin-java-channel/src/test/java/me/chanjar/weixin/channel/bean/kf/WxChannelKfBeanTest.java`
26+
27+
- [ ] Write JSON encode/decode tests for `request_id`, `open_id`, `msg_type`, `text.content`, `cos_url` and `msg_id`.
28+
- [ ] Run the test and verify it fails because the classes do not exist.
29+
- [ ] Add the minimal annotated model classes and nested message content types.
30+
- [ ] Run the test and verify it passes.
31+
32+
### Task 2: 服务入口与请求执行
33+
34+
**Files:**
35+
- Create: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelKfService.java`
36+
- Create: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelKfServiceImpl.java`
37+
- Modify: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelService.java`
38+
- Modify: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/BaseWxChannelServiceImpl.java`
39+
- Modify: `weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java`
40+
- Test: `weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelKfServiceImplTest.java`
41+
42+
- [ ] Write tests proving the service uses the documented URLs, uploads `file`, `open_id`, `msg_type`, decodes both responses, and caches `getKfService()`.
43+
- [ ] Run the test and verify it fails because the API is absent.
44+
- [ ] Add the minimal service API, implementation, constants and cached service entry point.
45+
- [ ] Run the focused tests and module test suite, then inspect `git diff --check`.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# 视频号小店商家客服 API 设计
2+
3+
## 目标
4+
5+
实现 Issue #3991 所列的商家客服媒体上传与消息发送 API,并使调用入口、请求模型和响应解析与现有 `weixin-java-channel` 服务保持一致。
6+
7+
## 设计
8+
9+
- 在 `WxChannelService` 暴露 `getKfService()`,由 `BaseWxChannelServiceImpl` 缓存并懒加载 `WxChannelKfServiceImpl`。
10+
- `WxChannelKfService` 提供媒体上传(带文件名和便捷重载)以及接收强类型请求参数的消息发送方法。上传请求通过既有 `CommonUploadParam` 发送 multipart 数据。
11+
- 消息模型用 `@JsonProperty` 显式映射微信字段,支持 text、image、video、file、product_share 和 order_share 六类内容;响应继承项目既有基础响应。
12+
- API 常量使用官方文档确认的 `/channels/ec/commkf/cosupload` 和 `/channels/ec/commkf/sendmsg` 路径。
13+
14+
## 质量边界
15+
16+
- 保持 Java 8 兼容,不增加依赖,不变更现有公共 API。
17+
- 使用 TestNG 覆盖请求/响应 JSON 映射、服务 URL、上传表单字段及服务入口缓存;测试不依赖真实微信凭据。
18+
- PR 使用 `Closes #3991` 关联并关闭原始 Issue;旧 PR #4037 在新 PR 创建后以替代说明关闭。
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package me.chanjar.weixin.channel.api;
2+
3+
import me.chanjar.weixin.channel.bean.kf.WxChannelKfSendMsgParam;
4+
import me.chanjar.weixin.channel.bean.kf.WxChannelKfSendMsgResponse;
5+
import me.chanjar.weixin.common.error.WxErrorException;
6+
7+
/** 视频号小店商家客服服务。 */
8+
public interface WxChannelKfService {
9+
10+
/**
11+
* 上传多媒体资源。
12+
*
13+
* @param openId 用户 open_id
14+
* @param msgType 文件类型,仅支持 video、file、image
15+
* @param file 文件字节内容
16+
* @return COS 地址
17+
* @throws WxErrorException 微信异常
18+
*/
19+
String uploadMedia(String openId, String msgType, byte[] file) throws WxErrorException;
20+
21+
/**
22+
* 上传多媒体资源。
23+
*
24+
* @param openId 用户 open_id
25+
* @param msgType 文件类型,仅支持 video、file、image
26+
* @param fileName 文件名
27+
* @param file 文件字节内容
28+
* @return COS 地址
29+
* @throws WxErrorException 微信异常
30+
*/
31+
String uploadMedia(String openId, String msgType, String fileName, byte[] file) throws WxErrorException;
32+
33+
/**
34+
* 发送客服消息。
35+
*
36+
* @param param 请求参数
37+
* @return 发送结果
38+
* @throws WxErrorException 微信异常
39+
*/
40+
WxChannelKfSendMsgResponse sendMessage(WxChannelKfSendMsgParam param) throws WxErrorException;
41+
}

‎weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelService.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@
77
*/
88
public interface WxChannelService extends BaseWxChannelService {
99

10+
/**
11+
* 商家客服服务。
12+
*
13+
* @return 商家客服服务
14+
*/
15+
default WxChannelKfService getKfService() {
16+
throw new UnsupportedOperationException("WxChannelService implementation does not support getKfService()");
17+
}
18+
1019
/**
1120
* 基础接口服务
1221
*

‎weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/BaseWxChannelServiceImpl.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ public abstract class BaseWxChannelServiceImpl<H, P> implements WxChannelService
7272
private WxTalentService talentService = null;
7373
private WxChannelFavoriteService favoriteService = null;
7474
private WxChannelEwaybillService ewaybillService = null;
75+
private WxChannelKfService kfService = null;
7576

7677
protected WxChannelConfig config;
7778
private int retrySleepMillis = 1000;
@@ -548,4 +549,12 @@ public synchronized WxChannelEwaybillService getEwaybillService() {
548549
return ewaybillService;
549550
}
550551

552+
@Override
553+
public synchronized WxChannelKfService getKfService() {
554+
if (kfService == null) {
555+
kfService = new WxChannelKfServiceImpl(this);
556+
}
557+
return kfService;
558+
}
559+
551560
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package me.chanjar.weixin.channel.api.impl;
2+
3+
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Kf.COS_UPLOAD_URL;
4+
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Kf.SEND_MSG_URL;
5+
6+
import me.chanjar.weixin.channel.api.WxChannelKfService;
7+
import me.chanjar.weixin.channel.bean.kf.WxChannelKfCosUploadResponse;
8+
import me.chanjar.weixin.channel.bean.kf.WxChannelKfSendMsgParam;
9+
import me.chanjar.weixin.channel.bean.kf.WxChannelKfSendMsgResponse;
10+
import me.chanjar.weixin.channel.util.JsonUtils;
11+
import me.chanjar.weixin.channel.util.ResponseUtils;
12+
import me.chanjar.weixin.common.bean.CommonUploadParam;
13+
import me.chanjar.weixin.common.error.WxErrorException;
14+
import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
15+
16+
/** 视频号小店商家客服服务实现。 */
17+
public class WxChannelKfServiceImpl implements WxChannelKfService {
18+
19+
private final BaseWxChannelServiceImpl<?, ?> channelService;
20+
21+
public WxChannelKfServiceImpl(BaseWxChannelServiceImpl<?, ?> channelService) {
22+
this.channelService = channelService;
23+
}
24+
25+
@Override
26+
public String uploadMedia(String openId, String msgType, byte[] file) throws WxErrorException {
27+
return uploadMedia(openId, msgType, null, file);
28+
}
29+
30+
@Override
31+
public String uploadMedia(String openId, String msgType, String fileName, byte[] file) throws WxErrorException {
32+
CommonUploadParam uploadParam = CommonUploadParam.fromBytes("file", fileName, file)
33+
.addFormField("open_id", openId)
34+
.addFormField("msg_type", msgType);
35+
String responseJson = channelService.upload(COS_UPLOAD_URL, uploadParam);
36+
return ResponseUtils.decode(responseJson, WxChannelKfCosUploadResponse.class).getCosUrl();
37+
}
38+
39+
@Override
40+
public WxChannelKfSendMsgResponse sendMessage(WxChannelKfSendMsgParam param) throws WxErrorException {
41+
String responseJson = channelService.executeWithoutLog(SimplePostRequestExecutor.create(channelService), SEND_MSG_URL,
42+
JsonUtils.encode(param));
43+
return ResponseUtils.decode(responseJson, WxChannelKfSendMsgResponse.class);
44+
}
45+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package me.chanjar.weixin.channel.bean.kf;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Data;
5+
import lombok.EqualsAndHashCode;
6+
import lombok.NoArgsConstructor;
7+
import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
8+
9+
/** 客服素材上传响应。 */
10+
@Data
11+
@NoArgsConstructor
12+
@EqualsAndHashCode(callSuper = true)
13+
public class WxChannelKfCosUploadResponse extends WxChannelBaseResponse {
14+
15+
private static final long serialVersionUID = 1L;
16+
17+
/** 素材在 COS 上的地址。 */
18+
@JsonProperty("cos_url")
19+
private String cosUrl;
20+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package me.chanjar.weixin.channel.bean.kf;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import java.io.Serializable;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
/** 发送客服消息请求参数。 */
9+
@Data
10+
@NoArgsConstructor
11+
public class WxChannelKfSendMsgParam implements Serializable {
12+
13+
private static final long serialVersionUID = 1L;
14+
15+
/** 请求幂等标识。 */
16+
@JsonProperty("request_id")
17+
private String requestId;
18+
19+
/** 接收消息的用户 openid。 */
20+
@JsonProperty("open_id")
21+
private String openId;
22+
23+
/** 消息类型。 */
24+
@JsonProperty("msg_type")
25+
private String msgType;
26+
27+
/** 文本消息内容。 */
28+
@JsonProperty("text")
29+
private Text text;
30+
31+
/** 图片消息内容。 */
32+
@JsonProperty("image")
33+
private CosUrlMessage image;
34+
35+
/** 视频消息内容。 */
36+
@JsonProperty("video")
37+
private CosUrlMessage video;
38+
39+
/** 文件消息内容。 */
40+
@JsonProperty("file")
41+
private CosUrlMessage file;
42+
43+
/** 商品卡片消息内容。 */
44+
@JsonProperty("product_share")
45+
private ProductShareMessage productShare;
46+
47+
/** 订单卡片消息内容。 */
48+
@JsonProperty("order_share")
49+
private OrderShareMessage orderShare;
50+
51+
@Data
52+
@NoArgsConstructor
53+
public static class Text implements Serializable {
54+
55+
private static final long serialVersionUID = 1L;
56+
57+
@JsonProperty("content")
58+
private String content;
59+
}
60+
61+
@Data
62+
@NoArgsConstructor
63+
public static class CosUrlMessage implements Serializable {
64+
65+
private static final long serialVersionUID = 1L;
66+
67+
@JsonProperty("cos_url")
68+
private String cosUrl;
69+
}
70+
71+
@Data
72+
@NoArgsConstructor
73+
public static class ProductShareMessage implements Serializable {
74+
75+
private static final long serialVersionUID = 1L;
76+
77+
@JsonProperty("product_id")
78+
private String productId;
79+
}
80+
81+
@Data
82+
@NoArgsConstructor
83+
public static class OrderShareMessage implements Serializable {
84+
85+
private static final long serialVersionUID = 1L;
86+
87+
@JsonProperty("order_id")
88+
private String orderId;
89+
}
90+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package me.chanjar.weixin.channel.bean.kf;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Data;
5+
import lombok.EqualsAndHashCode;
6+
import lombok.NoArgsConstructor;
7+
import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
8+
9+
/** 发送客服消息响应。 */
10+
@Data
11+
@NoArgsConstructor
12+
@EqualsAndHashCode(callSuper = true)
13+
public class WxChannelKfSendMsgResponse extends WxChannelBaseResponse {
14+
15+
private static final long serialVersionUID = 1L;
16+
17+
/** 消息 id。 */
18+
@JsonProperty("msg_id")
19+
private String msgId;
20+
}

‎weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ public interface Favorite {
4949
String GET_FAVORITE_COUNT = "https://api.weixin.qq.com/channels/ec/favorites/count/get";
5050
}
5151

52+
/** 商家客服相关接口 */
53+
public interface Kf {
54+
55+
/** 上传客服素材 */
56+
String COS_UPLOAD_URL = "https://api.weixin.qq.com/channels/ec/commkf/cosupload";
57+
/** 发送客服消息 */
58+
String SEND_MSG_URL = "https://api.weixin.qq.com/channels/ec/commkf/sendmsg";
59+
}
60+
5261
/** 商品类目相关接口 */
5362
public interface Category {
5463

0 commit comments

Comments
 (0)