JAVA后端上传图片至企微临时素材

news2024/12/22 22:34:52

1.使用场景

在使用企业微信API接口中,往往开发者需要使用自定义的资源,比如发送本地图片消息,设置通讯录自定义头像等。
为了实现同一资源文件,一次上传可以多次使用,这里提供了素材管理接口:以media_id来标识资源文件,实现文件的上传与下载。

以发送消息为示例:

image-20240202111547787

以JSSDK选图片上传为示例:

image-20240202111618496

上传的媒体文件限制

所有文件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

image-20240202105135565

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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1429116.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

jupyter notebook找不到自己创建的环境 无法识别 解决方法

问题描述&#xff1a; 这是最近遇到的一个关于Anaconda的小问题。 用conda创建一个名为 pytorch 的环境想学习pytorch&#xff0c;安装完一切之后在 jupyter 中找不到 pytorch 这个虚拟环境&#xff0c;与之相关的库也都无法调用 解决方法&#xff1a; 实际上是由于在虚拟环境…

docker踩坑记录

踩坑记录 1.1 后台启动容器&#xff0c;实际没有启动 现象&#xff1a; 后台启动centos&#xff0c;结果执行docker ps命令&#xff0c;容器没启动。 原因&#xff1a; docker是以容器启动的&#xff0c;必须要有个前台进程&#xff0c;若是全部都是后台deamon守护进程&…

实体对齐与知识融合工具综述

目录 前言1 实体对齐概述1.1 实体对齐的核心1.2 实体对齐的目标 2 传统实体对齐方法2.1 等价关系推理2.2 相似度计算2.3 特征计算 3 基于表示学习的实体对齐方法3.1 嵌入式方法3.2 语义关系的捕捉3.3 低维向量空间的优势 4 知识融合工具4.1 Silk4.2 openEA4.3 EAKit 结语 前言 …

docker安装定制gocd-agent

一、定制gocd-agent FROM gocd/gocd-agent-alpine-3.12:v21.1.0 MAINTAINER xxx "xxx163.com" # 切换到 root 用户 USER root # 安装 expect、jdk、docker RUN apk update && apk add expect && apk add openjdk8 && apk add docker &&…

XML传参方式

export function groupLoginAPI(xmlData) {return http.post(/tis/group/1.0/login, xmlData, {headers: {Content-Type: application/xml,X-Requested-With: AAServer/4.0,}}) }import {groupLoginAPI} from "../api/user"; function (e) { //xml格式传参let groupX…

anaconda离线安装包的方法

当设备没有网络时&#xff0c;可以使用有网络的设备先下载所需安装包&#xff0c;然后离线拷贝到需要安装的设备&#xff0c;最后安装。 一. 下载所需安装包 下载命令&#xff1a;使用pip download。详细描述参见pip download -h 以"blind-watermark"为例。 pip …

【Linux】理解系统中一个被打开的文件

文件系统 前言一、C语言文件接口二、系统文件接口三、文件描述符四、struct file 对象五、stdin、stdout、stderr六、文件描述符的分配规则七、重定向1. 重定向的原理2. dup23. 重谈 stderr 八、缓冲区1. 缓冲区基础2. 深入理解缓冲区3. 用户缓冲区和内核缓冲区4. FILE 前言 首…

查看域控组策略是否在客户端生效

要查看域控制器上的组策略是否已在客户端生效&#xff0c;可以按照以下步骤操作&#xff1a; 使用 RSOP (Resultant Set of Policy): 在客户端计算机上&#xff0c;以管理员身份打开命令提示符或者 PowerShell&#xff0c;并运行 gpresult /h GPReport.html 或 gpresult /v 命令…

10MHz 到 80MHz、10:1 LVDS 并串转换器(串化器)/串并转换器(解串器)MS1023/MS1224

产品简述 MS1023 串化器和 MS1224 解串器是一对 10bit 并串 / 串并转 换芯片&#xff0c;用于在 LVDS 差分底板上传输和接收 10MHz 至 80MHz 的并行字速率的串行数据。起始 / 停止位加载后&#xff0c;转换为负载编 码输出&#xff0c;串行数据速率介于 120Mbps…

Python实现利用仅有像素级标注的json文件生成框标注的json文件,并存放到新文件夹

import json import os # create rectangle labels based on polygon labels, and store in a new folder def create_rectangle_shapes(polygon_shapes):rectangle_shapes []for polygon_shape in polygon_shapes:# 获取多边形的坐标点points polygon_shape[points]# 找到最…

监测Tomcat项目宕机重启脚本(Linux)

1.准备好写好的脚本 #!/bin/sh # 获取tomcat的PID TOMCAT_PID$(ps -ef | grep tomcat | grep -v tomcatMonitor |grep -v grep | awk {print $2}) # tomcat的启动文件位置 START_TOMCAT/mnt/tomcat/bin/startup.sh # 需要监测的一个GET请求地址 MONITOR_URLhttp://localhost:…

消息总线在微服务中的应用

直连式配置中心 上一篇文章介绍了 Spring Cloud 中的分布式配置组件 Config&#xff0c;每个服务节点可以从Config Server 拉取外部配置信息。但是似乎还有一个悬而未决的问题&#xff0c;那就是当服务节点数量非常庞大的时候&#xff0c;我们不可能一台一台服务器挨个去手工触…

django+flask警务案件信息管理系统python-5dg53-vue

1&#xff09;用户在后台页面各种操作可及时得到反馈。 &#xff08;2&#xff09;该平台是提供给多个用户使用的平台&#xff0c;警员使用之前需要注册登录。登录验证后&#xff0c;警员才可进行各种操作[10]。 &#xff08;3&#xff09;管理员用户拥有信息新增&#xff0c;修…

计算机二级C语言的注意事项及相应真题-2-程序修改

目录 11.找出n的所有因子&#xff0c;统计因子的个数&#xff0c;并判断n 是否是”完数”12.计算s所指字符串中含有t所指字符串的数目13.将一个由八进制数字组成的字符串转换为与其面值相等的十进制整数14.根据整型形参m的值&#xff0c;计算如下公式的值15.从低位开始依次取长…

Springboot多种方法处理静态资源:设置并访问静态资源目录

&#xff5e;目录嗷&#xff5e; 静态文件application设置方法 配置详解编写配置优缺点 设置配置类方法 配置详解编写配置优缺点 总结 作者&#xff1a;Mintimate 博客&#xff1a;https://www.mintimate.cn Mintimate’s Blog&#xff0c;只为与你分享 静态文件 静态资源&…

Pycharm python用matplotlib 3D绘图显示空白解决办法

问题原因&#xff1a; matplotlib版本升级之后显示代码变了&#xff0c;修改为新的 # ax Axes3D(fig) # 原代码 ax fig.add_axes(Axes3D(fig)) # 新代码import numpy as np import matplotlib.pyplot as plt from matplotlib import cm from mpl_toolkits.mplot3d import Ax…

pytest的常用插件和Allure测试报告

pytest常用插件 pytest-html插件 安装&#xff1a; pip install pytest-html -U 用途&#xff1a; 生成html的测试报告 用法&#xff1a; ​在.ini配置文件里面添加 addopts --htmlreport.html --self-contained-html 效果&#xff1a; 执行结果中存在html测试报告路…

前端工程化之:webpack1-8(loader)

一、loader webpack 做的事情&#xff0c;仅仅是分析出各种模块的依赖关系&#xff0c;然后形成资源列表&#xff0c;最终打包生成到指定的文件中。 更多的功能需要借助 webpack loaders (加载器)和 webpack plugins (插件)完成。 webpack loader &#xff1a; loader 本质上是…

【Java开发岗面试】八股文—微服务、消息中间件

声明&#xff1a; 背景&#xff1a;本人为24届双非硕校招生&#xff0c;已经完整经历了一次秋招&#xff0c;拿到了三个offer。本专题旨在分享自己的一些Java开发岗面试经验&#xff08;主要是校招&#xff09;&#xff0c;包括我自己总结的八股文、算法、项目介绍、HR面和面试…

python 基础知识点(蓝桥杯python科目个人复习计划32)

今日复习内容&#xff1a;基础算法中的位运算 1.简介 位运算就是对二进制进行操作的运算方式&#xff0c;分为与运算&#xff0c;或运算&#xff0c;异或运算&#xff0c;取反&#xff0c;左移和右移。 &#xff08;1&#xff09;与运算 xyx&y000010100111 (2)或运算 …