1
0
mirror of synced 2026-03-22 04:35:02 +08:00

🎨 #3864 【公众号】通用上传方法增加“额外表单字段”支持,解决上传永久视频素材时需同时提交文件与description表单字段的问题

This commit is contained in:
Copilot
2026-03-03 13:27:08 +08:00
committed by GitHub
parent 3cd05c88f9
commit 2a9cff0180
7 changed files with 376 additions and 12 deletions

View File

@@ -0,0 +1,169 @@
# CommonUploadParam 额外表单字段功能使用示例
## 背景
微信公众号在上传永久视频素材时需要在POST请求中同时提交文件和一个名为`description`的表单字段该字段包含视频的描述信息JSON格式
根据微信公众号文档:
> 在上传视频素材时需要POST另一个表单id为description包含素材的描述信息内容格式为JSON格式如下
> ```json
> {
> "title": "VIDEO_TITLE",
> "introduction": "INTRODUCTION"
> }
> ```
## 解决方案
`CommonUploadParam` 类已经扩展支持额外的表单字段,可以在上传文件的同时提交其他表单数据。
## 使用示例
### 1. 基本用法 - 上传永久视频素材
```java
import me.chanjar.weixin.common.bean.CommonUploadParam;
import me.chanjar.weixin.common.util.json.WxGsonBuilder;
import me.chanjar.weixin.mp.api.WxMpService;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
public class VideoMaterialUploadExample {
public void uploadVideoMaterial(WxMpService wxMpService) throws Exception {
// 准备视频文件
File videoFile = new File("/path/to/video.mp4");
// 创建上传参数
CommonUploadParam uploadParam = CommonUploadParam.fromFile("media", videoFile);
// 准备视频描述信息JSON格式
Map<String, String> description = new HashMap<>();
description.put("title", "我的视频标题");
description.put("introduction", "这是一个精彩的视频介绍");
String descriptionJson = WxGsonBuilder.create().toJson(description);
// 添加description表单字段
uploadParam.addFormField("description", descriptionJson);
// 调用微信API上传
String url = "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=ACCESS_TOKEN&type=video";
String response = wxMpService.upload(url, uploadParam);
System.out.println("上传成功:" + response);
}
}
```
### 2. 链式调用风格
```java
import me.chanjar.weixin.common.bean.CommonUploadParam;
import com.google.gson.JsonObject;
public class ChainStyleExample {
public void uploadWithChainStyle(WxMpService wxMpService) throws Exception {
File videoFile = new File("/path/to/video.mp4");
// 准备描述信息
JsonObject description = new JsonObject();
description.addProperty("title", "视频标题");
description.addProperty("introduction", "视频介绍");
// 使用链式调用
String response = wxMpService.upload(
"https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=ACCESS_TOKEN&type=video",
CommonUploadParam.fromFile("media", videoFile)
.addFormField("description", description.toString())
);
System.out.println("上传成功:" + response);
}
}
```
### 3. 多个额外表单字段
```java
import me.chanjar.weixin.common.bean.CommonUploadParam;
public class MultipleFormFieldsExample {
public void uploadWithMultipleFields(WxMpService wxMpService) throws Exception {
File file = new File("/path/to/file.jpg");
// 可以添加多个表单字段
CommonUploadParam uploadParam = CommonUploadParam.fromFile("media", file)
.addFormField("field1", "value1")
.addFormField("field2", "value2")
.addFormField("field3", "value3");
String response = wxMpService.upload("https://api.weixin.qq.com/some/upload/url", uploadParam);
System.out.println("上传成功:" + response);
}
}
```
### 4. 从字节数组上传并添加表单字段
```java
import me.chanjar.weixin.common.bean.CommonUploadParam;
public class ByteArrayUploadExample {
public void uploadFromBytes(WxMpService wxMpService) throws Exception {
// 从字节数组创建上传参数
byte[] fileBytes = getFileBytes();
CommonUploadParam uploadParam = CommonUploadParam
.fromBytes("media", "video.mp4", fileBytes)
.addFormField("description", "{\"title\":\"标题\",\"introduction\":\"介绍\"}");
String response = wxMpService.upload(
"https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=ACCESS_TOKEN&type=video",
uploadParam
);
System.out.println("上传成功:" + response);
}
private byte[] getFileBytes() {
// 获取文件字节数组的逻辑
return new byte[0];
}
}
```
## API 说明
### CommonUploadParam 类
#### 构造方法
- `fromFile(String name, File file)` - 从文件创建上传参数
- `fromBytes(String name, String fileName, byte[] bytes)` - 从字节数组创建上传参数
#### 方法
- `addFormField(String fieldName, String fieldValue)` - 添加额外的表单字段,返回当前对象支持链式调用
- `getFormFields()` - 获取所有额外的表单字段Map类型
- `setFormFields(Map<String, String> formFields)` - 设置额外的表单字段
#### 属性
- `name` - 文件对应的接口参数名称media
- `data` - 上传数据CommonUploadData对象
- `formFields` - 额外的表单字段可选Map<String, String>类型)
## 注意事项
1. **表单字段是可选的**:如果不需要额外的表单字段,可以不调用`addFormField`方法
2. **JSON格式**对于需要JSON格式的表单字段如description需要先将对象转换为JSON字符串
3. **编码**表单字段值会使用UTF-8编码
4. **所有HTTP客户端支持**该功能在所有HTTP客户端实现中都得到支持OkHttp、Apache HttpClient、HttpComponents、JoddHttp
## 兼容性
- 对于通过 `fromFile``fromBytes` 等工厂方法创建 `CommonUploadParam` 的代码,本功能在行为层面是向后兼容的,现有代码无需修改即可继续工作。
- 如果之前直接使用构造函数(例如 `new CommonUploadParam(name, data)`)创建对象,由于新增了 `formFields` 字段,构造函数签名可能发生变化,升级后需要改为使用上述工厂方法或根据新构造函数签名调整代码。

View File

@@ -10,6 +10,8 @@ import org.springframework.lang.Nullable;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* 通用文件上传参数
@@ -34,6 +36,27 @@ public class CommonUploadParam implements Serializable {
@NotNull
private CommonUploadData data;
/**
* 额外的表单字段,用于在上传文件的同时提交其他表单数据
* 例如上传视频素材时需要提交description字段JSON格式的视频描述信息
*/
@Nullable
private Map<String, String> formFields;
/**
* 为保持向后兼容保留的 2 参数构造函数。
* <p>
* 仅设置文件参数名和上传数据,额外表单字段将为 {@code null}。
*
* @param name 参数名media
* @param data 上传数据
* @deprecated 请使用包含 formFields 参数的构造函数或静态工厂方法 {@link #fromFile(String, File)}、{@link #fromBytes(String, String, byte[])}
*/
@Deprecated
public CommonUploadParam(@NotNull String name, @NotNull CommonUploadData data) {
this(name, data, null);
}
/**
* 从文件构造
*
@@ -43,7 +66,7 @@ public class CommonUploadParam implements Serializable {
*/
@SneakyThrows
public static CommonUploadParam fromFile(String name, File file) {
return new CommonUploadParam(name, CommonUploadData.fromFile(file));
return new CommonUploadParam(name, CommonUploadData.fromFile(file), null);
}
/**
@@ -55,11 +78,32 @@ 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));
return new CommonUploadParam(name, new CommonUploadData(fileName, new ByteArrayInputStream(bytes), bytes.length), null);
}
/**
* 添加额外的表单字段
*
* @param fieldName 表单字段名
* @param fieldValue 表单字段值
* @return 当前对象,支持链式调用
*/
public CommonUploadParam addFormField(String fieldName, String fieldValue) {
if (fieldName == null || fieldName.trim().isEmpty()) {
throw new IllegalArgumentException("表单字段名不能为空");
}
if (fieldValue == null) {
throw new IllegalArgumentException("表单字段值不能为null");
}
if (this.formFields == null) {
this.formFields = new HashMap<>();
}
this.formFields.put(fieldName, fieldValue);
return this;
}
@Override
public String toString() {
return String.format("{name:%s, data:%s}", name, data);
return String.format("{name:%s, data:%s, formFields:%s}", name, data, formFields);
}
}

View File

@@ -44,11 +44,19 @@ public class CommonUploadRequestExecutorApacheImpl extends CommonUploadRequestEx
if (param != null) {
CommonUploadData data = param.getData();
InnerStreamBody part = new InnerStreamBody(data.getInputStream(), ContentType.DEFAULT_BINARY, data.getFileName(), data.getLength());
HttpEntity entity = MultipartEntityBuilder
MultipartEntityBuilder entityBuilder = MultipartEntityBuilder
.create()
.addPart(param.getName(), part)
.setMode(HttpMultipartMode.RFC6532)
.build();
.setMode(HttpMultipartMode.RFC6532);
// 添加额外的表单字段
if (param.getFormFields() != null && !param.getFormFields().isEmpty()) {
for (java.util.Map.Entry<String, String> entry : param.getFormFields().entrySet()) {
entityBuilder.addTextBody(entry.getKey(), entry.getValue(), ContentType.TEXT_PLAIN.withCharset("UTF-8"));
}
}
HttpEntity entity = entityBuilder.build();
httpPost.setEntity(entity);
}
String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE);

View File

@@ -41,11 +41,19 @@ public class CommonUploadRequestExecutorHttpComponentsImpl extends CommonUploadR
if (param != null) {
CommonUploadData data = param.getData();
InnerStreamBody part = new InnerStreamBody(data.getInputStream(), ContentType.DEFAULT_BINARY, data.getFileName(), data.getLength());
HttpEntity entity = MultipartEntityBuilder
MultipartEntityBuilder entityBuilder = MultipartEntityBuilder
.create()
.addPart(param.getName(), part)
.setMode(HttpMultipartMode.EXTENDED)
.build();
.setMode(HttpMultipartMode.EXTENDED);
// 添加额外的表单字段
if (param.getFormFields() != null && !param.getFormFields().isEmpty()) {
for (java.util.Map.Entry<String, String> entry : param.getFormFields().entrySet()) {
entityBuilder.addTextBody(entry.getKey(), entry.getValue(), ContentType.TEXT_PLAIN.withCharset("UTF-8"));
}
}
HttpEntity entity = entityBuilder.build();
httpPost.setEntity(entity);
}
String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE);

View File

@@ -39,6 +39,14 @@ public class CommonUploadRequestExecutorJoddHttpImpl extends CommonUploadRequest
}
request.withConnectionProvider(requestHttp.getRequestHttpClient());
request.form(param.getName(), new CommonUploadParamToUploadableAdapter(param.getData()));
// 添加额外的表单字段
if (param.getFormFields() != null && !param.getFormFields().isEmpty()) {
for (java.util.Map.Entry<String, String> entry : param.getFormFields().entrySet()) {
request.form(entry.getKey(), entry.getValue());
}
}
HttpResponse response = request.send();
response.charset(StandardCharsets.UTF_8.name());
String responseContent = response.bodyText();

View File

@@ -31,10 +31,18 @@ public class CommonUploadRequestExecutorOkHttpImpl extends CommonUploadRequestEx
@Override
public String execute(String uri, CommonUploadParam param, WxType wxType) throws WxErrorException, IOException {
RequestBody requestBody = new CommonUpdateDataToRequestBodyAdapter(param.getData());
RequestBody body = new MultipartBody.Builder()
MultipartBody.Builder bodyBuilder = new MultipartBody.Builder()
.setType(MediaType.get("multipart/form-data"))
.addFormDataPart(param.getName(), param.getData().getFileName(), requestBody)
.build();
.addFormDataPart(param.getName(), param.getData().getFileName(), requestBody);
// 添加额外的表单字段
if (param.getFormFields() != null && !param.getFormFields().isEmpty()) {
for (java.util.Map.Entry<String, String> entry : param.getFormFields().entrySet()) {
bodyBuilder.addFormDataPart(entry.getKey(), entry.getValue());
}
}
RequestBody body = bodyBuilder.build();
Request request = new Request.Builder().url(uri).post(body).build();
try (Response response = requestHttp.getRequestHttpClient().newCall(request).execute()) {

View File

@@ -0,0 +1,119 @@
package me.chanjar.weixin.common.bean;
import org.testng.Assert;
import org.testng.annotations.Test;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
/**
* CommonUploadParam 单元测试
*
* @author Binary Wang
*/
@Test
public class CommonUploadParamTest {
@Test
public void testFromFile() {
File file = new File("test.txt");
CommonUploadParam param = CommonUploadParam.fromFile("media", file);
Assert.assertNotNull(param);
Assert.assertEquals(param.getName(), "media");
Assert.assertNotNull(param.getData());
Assert.assertNull(param.getFormFields());
}
@Test
public void testFromBytes() {
byte[] bytes = "test content".getBytes();
CommonUploadParam param = CommonUploadParam.fromBytes("media", "test.txt", bytes);
Assert.assertNotNull(param);
Assert.assertEquals(param.getName(), "media");
Assert.assertNotNull(param.getData());
Assert.assertEquals(param.getData().getFileName(), "test.txt");
Assert.assertNull(param.getFormFields());
}
@Test
public void testAddFormField() {
File file = new File("test.txt");
CommonUploadParam param = CommonUploadParam.fromFile("media", file);
// 添加单个表单字段
param.addFormField("title", "测试标题");
Assert.assertNotNull(param.getFormFields());
Assert.assertEquals(param.getFormFields().size(), 1);
Assert.assertEquals(param.getFormFields().get("title"), "测试标题");
// 添加多个表单字段
param.addFormField("introduction", "测试介绍");
Assert.assertEquals(param.getFormFields().size(), 2);
Assert.assertEquals(param.getFormFields().get("introduction"), "测试介绍");
}
@Test
public void testAddFormFieldChaining() {
File file = new File("test.txt");
CommonUploadParam param = CommonUploadParam.fromFile("media", file)
.addFormField("title", "测试标题")
.addFormField("introduction", "测试介绍");
Assert.assertNotNull(param.getFormFields());
Assert.assertEquals(param.getFormFields().size(), 2);
Assert.assertEquals(param.getFormFields().get("title"), "测试标题");
Assert.assertEquals(param.getFormFields().get("introduction"), "测试介绍");
}
@Test
public void testConstructorWithFormFields() {
CommonUploadData data = new CommonUploadData("test.txt", null, 0);
Map<String, String> formFields = new HashMap<>();
formFields.put("title", "测试标题");
formFields.put("introduction", "测试介绍");
CommonUploadParam param = new CommonUploadParam("media", data, formFields);
Assert.assertNotNull(param.getFormFields());
Assert.assertEquals(param.getFormFields().size(), 2);
Assert.assertEquals(param.getFormFields().get("title"), "测试标题");
Assert.assertEquals(param.getFormFields().get("introduction"), "测试介绍");
}
@Test
public void testToString() {
File file = new File("test.txt");
CommonUploadParam param = CommonUploadParam.fromFile("media", file)
.addFormField("title", "测试标题");
String str = param.toString();
Assert.assertTrue(str.contains("name:media"));
Assert.assertTrue(str.contains("formFields:"));
}
@Test(expectedExceptions = IllegalArgumentException.class)
public void testAddFormFieldWithNullFieldName() {
File file = new File("test.txt");
CommonUploadParam param = CommonUploadParam.fromFile("media", file);
param.addFormField(null, "value");
}
@Test(expectedExceptions = IllegalArgumentException.class)
public void testAddFormFieldWithEmptyFieldName() {
File file = new File("test.txt");
CommonUploadParam param = CommonUploadParam.fromFile("media", file);
param.addFormField("", "value");
}
@Test(expectedExceptions = IllegalArgumentException.class)
public void testAddFormFieldWithNullFieldValue() {
File file = new File("test.txt");
CommonUploadParam param = CommonUploadParam.fromFile("media", file);
param.addFormField("fieldName", null);
}
}