Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions docs/CommonUploadParam-FormFields-Usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# 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)

## 兼容性

该功能向后兼容,现有的不使用额外表单字段的代码无需修改即可继续工作。
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

文档声明"该功能向后兼容",但实际上存在 API 破坏性变更。由于添加了新字段 formFields,Lombok 的 @AllArgsConstructor 将 2 参数构造函数替换为 3 参数构造函数,导致直接使用 new CommonUploadParam(name, data) 的现有代码无法编译。

建议更新文档说明这一变更,或者修改 CommonUploadParam 类以保持真正的向后兼容性(例如添加 2 参数构造函数)。

Suggested change
该功能向后兼容,现有的不使用额外表单字段的代码无需修改即可继续工作
- 对于通过 `fromFile``fromBytes` 等工厂方法创建 `CommonUploadParam` 的代码,本功能在行为层面是向后兼容的,现有代码无需修改即可继续工作
- 如果之前直接使用构造函数(例如 `new CommonUploadParam(name, data)`)创建对象,由于新增了 `formFields` 字段,构造函数签名可能发生变化,升级后需要改为使用上述工厂方法或根据新构造函数签名调整代码。

Copilot uses AI. Check for mistakes.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;

/**
* 通用文件上传参数
Expand All @@ -34,6 +36,13 @@ public class CommonUploadParam implements Serializable {
@NotNull
private CommonUploadData data;

/**
* 额外的表单字段,用于在上传文件的同时提交其他表单数据
* 例如:上传视频素材时需要提交description字段(JSON格式的视频描述信息)
*/
@Nullable
private Map<String, String> formFields;

/**
Copy link

Copilot AI Jan 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

此变更引入了 API 破坏性变更。添加新字段 formFields 后,Lombok 的 @AllArgsConstructor 会将原有的 2 参数构造函数替换为 3 参数构造函数,这会导致所有直接使用 new CommonUploadParam(name, data) 的代码无法编译。

建议保持向后兼容性,可以采用以下方案之一:

  1. 手动添加一个 2 参数构造函数并标记为 @Deprecated
  2. 使用 @Builder 模式替代 @AllArgsConstructor
  3. 移除 @AllArgsConstructor 并手动定义所需的构造函数

从当前 PR 的修改可以看出(WxOpenMaAuthServiceImplWxOpenUploadIcpMediaParam 需要显式传入 null),这确实是破坏性变更。

Suggested change
/**
/**
* 为保持向后兼容保留的 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);
}
/**

Copilot uses AI. Check for mistakes.
* 从文件构造
*
Expand All @@ -43,7 +52,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);
}

/**
Expand All @@ -55,11 +64,32 @@ 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));
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,19 @@ public String execute(String uri, CommonUploadParam param, WxType wxType) throws
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,19 @@ public String execute(String uri, CommonUploadParam param, WxType wxType) throws
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ public String execute(String uri, CommonUploadParam param, WxType wxType) throws
}
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,18 @@ public CommonUploadRequestExecutorOkHttpImpl(RequestHttp<OkHttpClient, OkHttpPro
@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()) {
Expand Down
Loading
Loading