diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java index 82f6db917..e874b26f4 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java @@ -2,6 +2,8 @@ package me.chanjar.weixin.cp.api; import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.media.MediaUploadByUrlReq; +import me.chanjar.weixin.cp.bean.media.MediaUploadByUrlResult; import java.io.File; import java.io.IOException; @@ -133,4 +135,21 @@ public interface WxCpMediaService { * @throws WxErrorException the wx error exception */ String uploadImg(File file) throws WxErrorException; + + /** + * 生成异步上传任务 + * 跟上传临时素材拿到的media_id使用场景是不通用的,目前适配的接口如下:https://developer.work.weixin.qq.com/document/path/96488#%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF%E8%AF%B4%E6%98%8E + * @param req 请求参数 + * @return 返回异步任务id + * @throws WxErrorException the wx error exception + */ + String uploadByUrl(MediaUploadByUrlReq req) throws WxErrorException; + + /** + * 查询异步任务结果 + * @param jobId 任务id。最长为128字节,60分钟内有效 + * @return 返回异步任务结果 + * @throws WxErrorException the wx error exception + */ + MediaUploadByUrlResult uploadByUrl(String jobId) throws WxErrorException; } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java index 863dd7c1d..427ce9d89 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java @@ -1,5 +1,6 @@ package me.chanjar.weixin.cp.api.impl; +import com.google.gson.JsonObject; import lombok.RequiredArgsConstructor; import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; import me.chanjar.weixin.common.error.WxErrorException; @@ -9,8 +10,12 @@ import me.chanjar.weixin.common.util.http.BaseMediaDownloadRequestExecutor; import me.chanjar.weixin.common.util.http.InputStreamData; import me.chanjar.weixin.common.util.http.MediaInputStreamUploadRequestExecutor; import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor; +import me.chanjar.weixin.common.util.json.GsonHelper; +import me.chanjar.weixin.common.util.json.GsonParser; import me.chanjar.weixin.cp.api.WxCpMediaService; import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.media.MediaUploadByUrlReq; +import me.chanjar.weixin.cp.bean.media.MediaUploadByUrlResult; import java.io.File; import java.io.IOException; @@ -20,7 +25,12 @@ import java.net.URL; import java.nio.file.Files; import java.util.UUID; -import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.*; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.GET_UPLOAD_BY_URL_RESULT; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.IMG_UPLOAD; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.JSSDK_MEDIA_GET; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.MEDIA_GET; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.MEDIA_UPLOAD; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.UPLOAD_BY_URL; /** *
@@ -119,4 +129,20 @@ public class WxCpMediaServiceImpl implements WxCpMediaService {
     return this.mainService.execute(MediaUploadRequestExecutor.create(this.mainService.getRequestHttp()), url, file)
       .getUrl();
   }
+
+  @Override
+  public String uploadByUrl(MediaUploadByUrlReq req) throws WxErrorException {
+    final String url = this.mainService.getWxCpConfigStorage().getApiUrl(UPLOAD_BY_URL);
+    String responseContent = this.mainService.post(url, req.toJson());
+    return GsonHelper.getString(GsonParser.parse(responseContent), "jobid");
+  }
+
+  @Override
+  public MediaUploadByUrlResult uploadByUrl(String jobId) throws WxErrorException {
+    final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_UPLOAD_BY_URL_RESULT);
+    JsonObject jsonObject = new JsonObject();
+    jsonObject.addProperty("jobid", jobId);
+    String post = this.mainService.post(url, jsonObject.toString());
+    return MediaUploadByUrlResult.fromJson(post);
+  }
 }
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/media/MediaUploadByUrlReq.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/media/MediaUploadByUrlReq.java
new file mode 100644
index 000000000..c5cb21bde
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/media/MediaUploadByUrlReq.java
@@ -0,0 +1,58 @@
+package me.chanjar.weixin.cp.bean.media;
+
+import lombok.Data;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+/**
+ * 生成异步上传任务
+ * @author imyzt
+ * @date 2025/04/27
+ */
+@Data
+public class MediaUploadByUrlReq {
+
+  /**
+   * 场景值。1-客户联系入群欢迎语素材(目前仅支持1)。 注意:每个场景值有对应的使用范围,详见上面的「使用场景说明」
+   */
+  private Integer scene;
+
+  /**
+   * 媒体文件类型。目前仅支持video-视频,file-普通文件 不超过32字节。
+   */
+  private String type;
+
+  /**
+   * 文件名,标识文件展示的名称。比如,使用该media_id发消息时,展示的文件名由该字段控制。 不超过128字节。
+   */
+  private String filename;
+
+  /**
+   * 文件cdn url。url要求支持Range分块下载 不超过1024字节。 如果为腾讯云cos链接,则需要设置为「公有读」权限。
+   */
+  private String url;
+
+  /**
+   * 文件md5。对比从url下载下来的文件md5是否一致。 不超过32字节。
+   */
+  private String md5;
+
+  /**
+   * From json wx cp base resp.
+   *
+   * @param json the json
+   * @return the wx cp base resp
+   */
+  public static MediaUploadByUrlReq fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, MediaUploadByUrlReq.class);
+  }
+
+  /**
+   * To json string.
+   *
+   * @return the string
+   */
+  public String toJson() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/media/MediaUploadByUrlResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/media/MediaUploadByUrlResult.java
new file mode 100644
index 000000000..cc931eed3
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/media/MediaUploadByUrlResult.java
@@ -0,0 +1,82 @@
+package me.chanjar.weixin.cp.bean.media;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import me.chanjar.weixin.cp.bean.WxCpBaseResp;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * 异步上传企微素材
+ * @author imyzt
+ * @date 2025/4/27
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class MediaUploadByUrlResult extends WxCpBaseResp implements Serializable {
+
+  private static final long serialVersionUID = 330834334738622341L;
+
+  /**
+   * 任务状态。1-处理中,2-完成,3-异常失败
+   */
+  @SerializedName("status")
+  private Integer status;
+
+  @SerializedName("detail")
+  private Detail detail;
+
+  @Data
+  public static class Detail {
+
+    /**
+     * 任务失败返回码。当status为3时返回非0,其他返回0
+     * 830001 url非法 确认url是否支持Range分块下载
+     * 830003 url下载数据失败 确认url本身是否能正常访问
+     * 45001 文件大小超过限制 确认文件在5字节~200M范围内
+     * 301019 文件MD5不匹配 确认url对应的文件内容md5,跟所填的md5参数是否一致
+     * 注意: status=2时,此处微信并未返回任何值
+     */
+    @SerializedName("errcode")
+    private Integer errCode;
+
+    /**
+     * 注意: status=2时,此处微信并未返回任何值
+     */
+    @SerializedName("errmsg")
+    private String errMsg;
+
+    /**
+     * 媒体文件上传后获取的唯一标识,3天内有效。当status为2时返回。
+     */
+    @SerializedName("media_id")
+    private String mediaId;
+
+    /**
+     * 媒体文件创建的时间戳。当status为2时返回。
+     */
+    @SerializedName("created_at")
+    private String createdAt;
+  }
+
+  /**
+   * From json wx cp media upload by url result.
+   *
+   * @param json the json
+   * @return the wx cp media upload by url result
+   */
+  public static MediaUploadByUrlResult fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, MediaUploadByUrlResult.class);
+  }
+
+  /**
+   * To json string.
+   *
+   * @return the string
+   */
+  public String toJson() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/message/WxCpXmlMessage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/message/WxCpXmlMessage.java
index fb4213f50..81d09a11c 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/message/WxCpXmlMessage.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/message/WxCpXmlMessage.java
@@ -198,6 +198,13 @@ public class WxCpXmlMessage implements Serializable {
   @XStreamAlias("SelectedItems")
   private List selectedItems;
 
+  /**
+   * 异步任务id
+   */
+  @XStreamAlias("JobId")
+  @XStreamConverter(value = XStreamCDataConverter.class)
+  private String jobId;
+
   /**
    * 微信客服
    * 调用拉取消息接口时,需要传此token,用于校验请求的合法性
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java
index d70f0ff4c..093d386e6 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java
@@ -238,6 +238,12 @@ public interface WxCpApiPathConsts {
      * The constant JSSDK_MEDIA_GET.
      */
     String JSSDK_MEDIA_GET = "/cgi-bin/media/get/jssdk";
+
+    /** The constant GET_UPLOAD_BY_URL_RESULT. */
+    String GET_UPLOAD_BY_URL_RESULT = "/cgi-bin/media/get_upload_by_url_result";
+
+    /** The constant UPLOAD_BY_URL. */
+    String UPLOAD_BY_URL = "/cgi-bin/media/upload_by_url";
   }
 
   /**
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpConsts.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpConsts.java
index 606dcea6d..3d51c9e2c 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpConsts.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpConsts.java
@@ -219,6 +219,11 @@ public class WxCpConsts {
      */
     public static final String CUSTOMER_ACQUISITION = "customer_acquisition";
 
+    /**
+     * 异步上传临时素材结果回调通知
+     */
+    public static final String UPLOAD_MEDIA_JOB_FINISH = "upload_media_job_finish";
+
   }
 
   /**
diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImplTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImplTest.java
index b964aad51..381a4c145 100644
--- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImplTest.java
+++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImplTest.java
@@ -7,6 +7,8 @@ import me.chanjar.weixin.common.error.WxErrorException;
 import me.chanjar.weixin.cp.api.ApiTestModule;
 import me.chanjar.weixin.cp.api.TestConstants;
 import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.bean.media.MediaUploadByUrlReq;
+import me.chanjar.weixin.cp.bean.media.MediaUploadByUrlResult;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Guice;
 import org.testng.annotations.Test;
@@ -127,4 +129,38 @@ public class WxCpMediaServiceImplTest {
     assertThat(file).isNotNull();
     System.out.println(file);
   }
+
+  /**
+   * Test upload media by url.
+   *
+   * @throws WxErrorException the wx error exception
+   */
+  @Test
+  public void testUploadMediaByUrl() throws WxErrorException {
+    MediaUploadByUrlReq req = new MediaUploadByUrlReq();
+    req.setScene(1);
+    req.setType("video");
+    req.setFilename("mov_bbb");
+    req.setUrl("https://www.w3school.com.cn/example/html5/mov_bbb.mp4");
+    req.setMd5("198918f40ecc7cab0fc4231adaf67c96");
+    String jobId = this.wxService.getMediaService().uploadByUrl(req);
+    System.out.println(jobId);
+  }
+
+  /**
+   * Test upload media by url.
+   *
+   * @throws WxErrorException the wx error exception
+   */
+  @Test
+  public void testUploadMediaByUrlResult() throws WxErrorException, InterruptedException {
+    String jobId = "job1745801375_5GIKWuFF3M7hcIkeSNMqs_W26xy5VeSWjLaLFTEdSfQ";
+    MediaUploadByUrlResult result = this.wxService.getMediaService().uploadByUrl(jobId);
+    System.out.println(result);
+  }
+
+  @Test
+  public void testUploadMediaJobFinishEvent() throws WxErrorException {
+    File file = this.wxService.getMediaService().getJssdkFile("....");
+  }
 }
diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/bean/message/WxCpXmlMessageTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/bean/message/WxCpXmlMessageTest.java
index a760a17ff..5bcfe9698 100644
--- a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/bean/message/WxCpXmlMessageTest.java
+++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/bean/message/WxCpXmlMessageTest.java
@@ -6,6 +6,7 @@ import me.chanjar.weixin.cp.util.xml.XStreamTransformer;
 import org.testng.annotations.Test;
 
 import static me.chanjar.weixin.cp.constant.WxCpConsts.EventType.TASKCARD_CLICK;
+import static me.chanjar.weixin.cp.constant.WxCpConsts.EventType.UPLOAD_MEDIA_JOB_FINISH;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertNotNull;
@@ -421,4 +422,24 @@ public class WxCpXmlMessageTest {
     assertThat(wxCpXmlMessage.getApprovalInfo().getApprovalNodes().get(0).getItems().get(0).getItemName()).isNotEmpty();
     assertThat(wxCpXmlMessage.getApprovalInfo().getNotifyNodes().get(0).getItemName()).isNotEmpty();
   }
+
+  /**
+   * Test open approval change.
+   */
+  public void testUploadMediaJobFinishEvent() {
+    String xml = "\n" +
+      "\t\n" +
+      "\t\n" +
+      "\t1425284517\n" +
+      "\t\n" +
+      "\t\n" +
+      "\t\n" +
+      "";
+
+    WxCpXmlMessage wxCpXmlMessage = WxCpXmlMessage.fromXml(xml);
+    assertThat(wxCpXmlMessage).isNotNull();
+    assertThat(wxCpXmlMessage.getJobId()).isNotEmpty();
+    assertThat(wxCpXmlMessage.getJobId()).isEqualTo("jobid_S0MrnndvRG5fadSlLwiBqiDDbM143UqTmKP3152FZk4");
+    assertThat(wxCpXmlMessage.getEvent()).isEqualTo(UPLOAD_MEDIA_JOB_FINISH);
+  }
 }