1.使用场景
在使用企业微信API接口中,往往开发者需要使用自定义的资源,比如发送本地图片消息,设置通讯录自定义头像等。
为了实现同一资源文件,一次上传可以多次使用,这里提供了素材管理接口:以media_id来标识资源文件,实现文件的上传与下载。
以发送消息为示例:
以JSSDK选图片上传为示例:
上传的媒体文件限制
所有文件size必须大于5个字节
- 图片(image):10MB,支持JPG,PNG格式
- 语音(voice) :2MB,播放长度不超过60s,仅支持AMR格式
- 视频(video) :10MB,支持MP4格式
- 普通文件(file):20MB
HTTP上传文件方法简析
HTTP是文本协议,若需要传递二进制文件需要依赖于multipart/form-data格式
1. 构造HTTP请求包
单个文件的multipart/form-data格式,如下:
--分隔符[换行]
Content-Disposition: form-data; name="表单名"; filename="文件名"; filelength=文件内容大小[换行]
Content-Type: 类型[换行]
[换行]
文件的二进制内容[换行]
--分隔符--
Content-Type根据不同文件类型可以设置对应不同的值,如下表格:
文件类型 | Content-Type |
---|---|
普通文件 | application/octet-stream |
jpg图片 | image/jpg |
png图片 | image/png |
bmp图片 | image/bmp |
amr音频 | voice/amr |
mp4视频 | video/mp4 |
若我们设置:分隔符为acebdf13572468,文件名为wework.txt,文件内容为mytext,由于上传临时素材要求name固定为media,那么构造的请求内容为:
--acebdf13572468
Content-Disposition: form-data; name="media";filename="wework.txt"; filelength=6
Content-Type: application/octet-stream
mytext
--acebdf13572468--
2. 设置HTTP头部信息
POST URL HTTP/1.1[换行]
Content-Type: multipart/form-data; boundary=分隔符[换行]
Content-Length: 请求体内容大小[换行]
[换行]
第1步构造的请求体内容
假定我们将第1步组装的文件内容上传到企业微信临时素材,分隔符取第1步设定值acebdf13572468,那么就得到如下:
POST https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=accesstoken001&type=file HTTP/1.1
Content-Type: multipart/form-data; boundary=acebdf13572468
Content-Length: 168
--acebdf13572468
Content-Disposition: form-data; name="media";filename="wework.txt"; filelength=6
Content-Type: application/octet-stream
mytext
--acebdf13572468--
上传临时素材
素材上传得到media_id,该media_id仅三天内有效
media_id在同一企业内应用之间可以共享
**请求方式:**POST(HTTPS)
**请求地址:**https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE
使用multipart/form-data POST上传文件, 文件标识名为"media"
参数说明:
参数 | 必须 | 说明 |
---|---|---|
access_token | 是 | 调用接口凭证 |
type | 是 | 媒体文件类型,分别有图片(image)、语音(voice)、视频(video),普通文件(file) |
POST的请求包中,form-data中媒体文件标识,应包含有 filename、filelength、content-type等信息
filename标识文件展示的名称。比如,使用该media_id发消息时,展示的文件名由该字段控制
请求示例:
POST https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=accesstoken001&type=file HTTP/1.1
Content-Type: multipart/form-data; boundary=-------------------------acebdf13572468
Content-Length: 220
---------------------------acebdf13572468
Content-Disposition: form-data; name="media";filename="wework.txt"; filelength=6
Content-Type: application/octet-stream
mytext
---------------------------acebdf13572468--
返回数据:
{
"errcode": 0,
"errmsg": "",
"type": "image",
"media_id": "1G6nrLmr5EC3MMb_-zK1dDdzmd0p7cNliYu9V5w7o8K0",
"created_at": "1380000000"
}
参数说明:
参数 | 说明 |
---|---|
type | 媒体文件类型,分别有图片(image)、语音(voice)、视频(video),普通文件(file) |
media_id | 媒体文件上传后获取的唯一标识,3天内有效 |
created_at | 媒体文件上传时间戳 |
上传企微临时素材,对应企微api文档链接:https://developer.work.weixin.qq.com/document/path/90253
2.控制层接口
控制层接收方式可以有文件方式接收,也可以前端用图片转换成base64格式字符串方式后端接收。
文件方式接收
@RequestMapping(method = RequestMethod.POST, value = "v1/uploadQwMedia")
public ScaResponseParam<JSONObject> uploadQwMedia(String type, String title, int orgId, @RequestParam("file")MultipartFile file) {
try {
if(file == null){
throw new IllegalArgumentException("没有上传图片");
}
if(StringUtils.isEmpty(type)){
throw new IllegalArgumentException("缺少type参数");
}
InputStream inputStream = file.getInputStream();
FindMediaIdReq req = new FindMediaIdReq();
req.setTitle(title);//对应企微api文档中的fileName参数,用于控制展示的文件名称
req.setType(type);//对应企微api文档中的type参数
String mediaId = scaGuideOneCustOneCodeService.uploadQwMedia(orgId, req, inputStream);
return ScaResponseParam.OK().fluentSetData(new JSONObject().fluentPut("mediaId", mediaId));
} catch (IllegalArgumentException e) {
log.error(e.getMessage(), e);
return ScaResponseParam.ERROR(e.getMessage());
} catch (Exception e) {
log.error("上传客户专属码到企微临时素材异常:{}-{}",e.getMessage(), e);
return ScaResponseParam.ERROR("上传客户专属码到企微临时素材异常");
}
}
base64方式接收
@RequestMapping(value = "/base64ImgUpload", method = RequestMethod.POST)
public ScaResponseParam<JSONObject> base64ImgUpload(@RequestBody OneCustOneCodeUploadQwImgRequest request) {
try {
BASE64Decoder decoder = new BASE64Decoder();
byte[] bytes = decoder.decodeBuffer(request.getBase64Str());
for (int i = 0; i < bytes.length; ++i) {
if (bytes[i] < 0) {
bytes[i] += 256;
}
}
int orgId = request.getOrgId();
InputStream inputStream = new ByteArrayInputStream(bytes);
FindMediaIdReq req = new FindMediaIdReq();
req.setTitle(request.getTitle());//对应企微api文档中的fileName参数,用于控制展示的文件名称
req.setType(request.getType());//对应企微api文档中的type参数
String mediaId = scaGuideOneCustOneCodeService.uploadQwMedia(orgId, req, inputStream);
return ScaResponseParam.OK().fluentSetData(new JSONObject().fluentPut("mediaId", mediaId));
} catch (IllegalArgumentException e) {
log.error(e.getMessage(), e);
return ScaResponseParam.ERROR(e.getMessage());
} catch (Exception e) {
log.error("base64上传客户专属码到企微临时素材异常:{}-{}",e.getMessage(), e);
return ScaResponseParam.ERROR("base64上传客户专属码到企微临时素材异常");
}
}
请求参数OneCustOneCodeUploadQwImgRequest
类
@Data
@SuppressWarnings("all")
public class OneCustOneCodeUploadQwImgRequest {
@ApiModelProperty(value = "文件类型,图片为image", required = true)
private String type;
@ApiModelProperty(value = "文件名称", required = true)
private String title;
@ApiModelProperty(value = "图片base64格式字符串", required = true)
private String base64Str;
@ApiModelProperty(value = "orgId", required = true)
private int orgId;
}
4.Service层接口
@Override
public String uploadQwMedia(Integer orgId, FindMediaIdReq reqBean, InputStream inputStream){
Map<String, Object> map = qyWeiXinService.uploadMediaForKf(orgId, reqBean, inputStream);
String mediaId = map.get("media_id").toString();
String createdAt = DateUtil.date2Str(new Date(Long.valueOf(map.get("created_at").toString()) * 1000));
log.info("上传企微素材返回,mediaId:{},createAt:{}", mediaId, createdAt);
return mediaId;
}
qyWeiXinService
中的uploadMediaForKf
方法
@Override
public Map<String, Object> uploadMediaForKf(Integer orgId, FindMediaIdReq reqBean, InputStream inputStream) {
ScaQyWxBuConfig scaQyWxBuConfig = ScaQyWxBuConfig.getConfigByOrgId(orgId);//获取配置的secret和corpId等信息
if(scaQyWxBuConfig == null){
log.error("客服企微参数配置为空, orgId:{}", orgId);
throw new RuntimeException("获取客服企微配置参数失败");
}
String accessToken = this.getKfAccessToken(scaQyWxBuConfig);//获取accessToken
String title = reqBean.getTitle();
//调用企微api上传图片文件到企微临时素材
return WinXinMessageUtil.mediaUploadByInputStream(accessToken, inputStream, reqBean.getType(), title);
}
调用企微api上传图片文件到企微临时素材方法,对应上面的WinXinMessageUtil.mediaUploadByInputStream
方法
public static Map<String, Object> mediaUploadByInputStream(String accessToken, InputStream inputStream, String type, String fileName) {
String upUrl = "https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=" + accessToken + "&type=" + type;
StringBuffer buffer = new StringBuffer();
BufferedReader reader = null;
try {
URL urlObj = new URL(upUrl);
HttpURLConnection con = (HttpURLConnection) urlObj.openConnection();
con.setRequestMethod("POST"); // 以Post方式提交表单,默认get方式
con.setDoInput(true);
con.setDoOutput(true);
con.setUseCaches(false); // post方式不能使用缓存
// 设置请求头信息
con.setRequestProperty("Connection", "Keep-Alive");
con.setRequestProperty("Charset", "UTF-8");
// 设置边界
String BOUNDARY = "----------" + System.currentTimeMillis();
con.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
// 请求正文信息
StringBuilder sb = new StringBuilder();
sb.append("--"); // 必须多两道线
sb.append(BOUNDARY);
sb.append("\r\n");
sb.append("Content-Disposition: form-data;name=\"media\";filename=\"" + fileName + "\"\r\n");
sb.append("Content-Type:application/octet-stream\r\n\r\n");
byte[] head = sb.toString().getBytes("utf-8");
// 获得输出流
OutputStream out = new DataOutputStream(con.getOutputStream());
// 输出表头
out.write(head);
// 把文件已流文件的方式 推入到url中
DataInputStream in = new DataInputStream(inputStream);
int bytes;
byte[] bufferOut = new byte[1024];
while ((bytes = in.read(bufferOut)) != -1) {
out.write(bufferOut, 0, bytes);
}
in.close();
// 结尾部分
byte[] foot = ("\r\n--" + BOUNDARY + "--\r\n").getBytes("utf-8");// 定义最后数据分隔线
out.write(foot);
out.flush();
out.close();
// 定义BufferedReader输入流来读取URL的响应
InputStream conInputStream = con.getInputStream();
reader = new BufferedReader(new InputStreamReader(conInputStream));
String line;
while ((line = reader.readLine()) != null) {
buffer.append(line);
}
String result = buffer.toString();
log.warn("{}上传临时素材结果:{}", fileName, result);
Map<String, Object> map = JSON.parseObject(result, Map.class);
if (!Objects.equals(map.get("errcode"), 0)) {
throw new IllegalArgumentException("上传临时素材异常:" + map.get("errmsg"));
}
return map;
} catch (IOException e) {
log.warn("上传临时素材{}异常:{}-{}", fileName, e.getMessage(), e);
throw new IllegalArgumentException("上传临时素材异常", e);
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
获取token的方法,对应上面的getKfAccessToken
方法
这里先从redis缓存获取,获取不到再调用企微api接口获取,可以根据实际情况进行变通。
private String getKfAccessToken(ScaQyWxBuConfig scaQyWxBuConfig) {
String corpId = scaQyWxBuConfig.getCorpId();//从配置中获取的corpId
String secret = scaQyWxBuConfig.getSecretKf();//从配置中获取的secret
if (StringUtils.isEmpty(corpId) || StringUtils.isEmpty(secret)) {
return null;
}
String key = "HYP_GUIDE_" + corpId + "AccessToken" + secret;
//优先从redis缓存中获取
String accessToken = jedisCluster.get(key);
if (StringUtils.isEmpty(accessToken)) {
//缓存中获取不到再调用企微api接口获取accessToken
try {
accessToken = WinXinMessageUtil.getAccessToken(corpId, secret);
jedisCluster.set(key, accessToken);
jedisCluster.expire(key, 7000);//设置过期时间
} catch (Exception e) {
log.error(e.getMessage());
}
}
return accessToken;
}
3.调用企微api接口获取accessToken
信息,
对应上面的WinXinMessageUtil.getAccessToken
public static String getAccessToken(String CorpID, String Secret) throws Exception {
String access_token = "";
CloseableHttpClient httpclient = HttpClients.createDefault();
try {
HttpGet httpGet = new HttpGet("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=" + CorpID + "&corpsecret=" + Secret);
CloseableHttpResponse response1 = httpclient.execute(httpGet);
JSONObject resultJsonObject;
try {
HttpEntity httpEntity = response1.getEntity();
if (httpEntity != null) {
try {
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(httpEntity.getContent(), "UTF-8"), 8 * 1024);
StringBuilder entityStringBuilder = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
entityStringBuilder.append(line);
}
// 利用从HttpEntity中得到的String生成JsonObject
resultJsonObject = new JSONObject(entityStringBuilder.toString().trim());
access_token = resultJsonObject.get("access_token") + "";
} catch (Exception e) {
log.warn("获取企微token异常:{}-{}", e.getMessage(), e);
}
}
} finally {
response1.close();
}
} finally {
httpclient.close();
}
return access_token;
}