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

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

尝试创建若依系统项目(vue3+element-plus+vite) 持续更新...

若依官网&#xff1a;RuoYi 若依官方网站 |后台管理系统|权限管理系统|快速开发框架|企业管理系统|开源框架|微服务框架|前后端分离框架|开源后台系统|RuoYi|RuoYi-Vue|RuoYi-Cloud|RuoYi框架|RuoYi开源|RuoYi视频|若依视频|RuoYi开发文档|若依开发文档|Java开源框架|Java|Spri…

STM32--USART串口(3)数据包

一、前言 在实际的工程中肯会有同时发送多种数据的情况&#xff0c;比如要不停的发送x、y、z分别对应三种不同的数据。xyzxyzxyz&#xff0c;但接收方可能是从中间某个地方开始接收的&#xff0c;这就导致数据错位。所以我们就需要将数据进行分割&#xff0c;打包成一个一个的…

数仓建模维度建模理论知识

0. 思维导图 第 1 章 数据仓库概述 1.1 数据仓库概述 数据仓库是一个为数据分析而设计的企业级数据管理系统。数据仓库可集中、整合多个信息源的大量数据&#xff0c;借助数据仓库的分析能力&#xff0c;企业可从数据中获得宝贵的信息进而改进决策。同时&#xff0c;随着时间的…

Kotlin快速入门系列10

Kotlin的委托 委托模式是常见的设计模式之一。在委托模式中&#xff0c;有两个对象参与处理同一个请求&#xff0c;接受请求的对象将请求委托给另一个对象来处理。与Java一样&#xff0c;Kotlin也支持委托模式&#xff0c;通过关键字by。 类委托 类的委托即一个类中定义的方…

东南亚独立站的黄金机会-东南亚服务器租用托管的选择

作为一个独立站的企业&#xff0c;选择将服务器托管或租用东南亚的服务器是一个明智的决策。东南亚市场是一个适合做独立站的国家。 1、东南亚的社交媒体用户非常活跃。东南亚地区的人口众多&#xff0c;其中很大一部分人使用社交媒体平台进行社交和购物。据统计&#xff0c;东…

喜报|博睿数据算力调度可观测平台荣获信通院“算力服务领航者计划”优秀案例

近日&#xff0c;中国通信标准化协会云计算标准和开源推进委员会2023年度工作总结会暨算力服务工作组成果发布会在京举行。会上&#xff0c;“2023年算力服务领航者计划优秀案例名单”正式公布&#xff0c;博睿数据的核心产品算力调度可观测平台 Bonree ONE成功入选&#xff0c…

WordPress主题YIA的文章页评论内容为什么没有显示出来?

有些WordPress站长使用YIA主题后&#xff0c;在YIA主题设置的“基本”中没有开启“一键关闭评论功能”&#xff0c;而且文章也是允许评论的&#xff0c;但是评论框却不显示&#xff0c;最关键的是文章原本就有的评论内容也不显示&#xff0c;这是为什么呢&#xff1f; 根据YIA主…

获取真实 IP 地址(一):判断是否使用 CDN(附链接)

一、介绍 CDN&#xff0c;全称为内容分发网络&#xff08;Content Delivery Network&#xff09;&#xff0c;是一种网络架构&#xff0c;旨在提高用户对于网络上内容的访问速度和性能。CDN通过在全球各地部署分布式服务器节点来存储和分发静态和动态内容&#xff0c;从而减少…

2024牛客寒假训练营1总结

G题不开long long的后果&#xff0c;即使有思路也没用。(给我气的) E题&#xff0c;不看数据范围的后果&#xff0c;不能一题名取题啊。 using ll long long; void solve() {int n, m;std::cin >> n >> m;std::vector<int>a(n);for (int i 0; i < n; i)…

【python】英语单词文本处理

文章目录 前言一、环境实验所需的库终端指令 二、实现过程Version 1 起源Version 2 listVersion 3 arrayVersion 4 结构化数组Version 5 区分单元且打乱顺序Version 6 可视化 三、txt文件 前言 缘起自懒得考小孩儿单词&#xff0c;最终效果如图&#xff1a; 本文记录了英语单词…

2024美赛数学建模B题思路分析 - 搜索潜水器

1 赛题 问题B&#xff1a;搜索潜水器 总部位于希腊的小型海上巡航潜艇&#xff08;MCMS&#xff09;公司&#xff0c;制造能够将人类运送到海洋最深处的潜水器。潜水器被移动到该位置&#xff0c;并不受主船的束缚。MCMS现在希望用他们的潜水器带游客在爱奥尼亚海底探险&…

react 之 useCallback

简单讲述下useCallback的使用方法&#xff0c;useCallback也是用来缓存的&#xff0c;只不过是用于做函数缓存 // useCallbackimport { memo, useCallback, useState } from "react"const Input memo(function Input ({ onChange }) {console.log(子组件重新渲染了…

物流自动化移动机器人|HEGERLS三维智能四向穿梭车助力优化企业供应链

智能化仓库/仓储贯穿于物流的各个环节&#xff0c;不局限于存储、输送、分拣、搬运等单一作业环节的自动化&#xff0c;更多的是利用科技手段实现整个物流供应链流程的自动化与智能化&#xff0c;将传统自动化仓储物流各环节进行多维度的有效融合。 例如在数智化物流仓储的建设…

OpenHarmony—Hap包签名工具

概述 为了保证OpenHarmony应用的完整性和来源可靠&#xff0c;在应用构建时需要对应用进行签名。经过签名的应用才能在真机设备上安装、运行、和调试。developtools_hapsigner仓 提供了签名工具的源码&#xff0c;包含密钥对生成、CSR文件生成、证书生成、Profile文件签名、Ha…

华为数通方向HCIP-DataCom H12-821题库(单选题:401-420)

第401题 R1的配置如图所示,此时在R1查看FIB表时,关于目的网段192.168.1.0/24的下跳是以下哪一项? A、10.0.23.3 B、10.0.12.2 C、10.0.23.2 D、10.0.12.1 【答案】A 【答案解析】 该题目考查的是路由的递归查询和 RIB 以及 FIB 的关系。在 RIB 中,静态路由写的是什么,下…

Node.js之内存限制理解_对处理前端打包内存溢出有所帮助

Node.js内存限制理解_对处理前端打包内存溢出有所帮助 文章目录 Node.js内存限制理解_对处理前端打包内存溢出有所帮助Node.js内存限制1. 查看Node.js默认内存限制1. Ndos.js_V20.10.02. Node.js_V18.16.0 2. V8引擎垃圾回收相关Heap organization堆组织 Node.js内存限制 默认情…

(十二)springboot实战——SSE服务推送事件案例实现

前言 SSE&#xff08;Server-Sent Events&#xff0c;服务器推送事件&#xff09;是一种基于HTTP协议的服务器推送技术。它允许服务器向客户端发送异步的、无限长的数据流&#xff0c;而无需客户端不断地轮询或发起请求。这种技术可以用来实现实时通信、在线聊天、即时更新等功…

Entity实体设计

Entity实体设计 &#x1f4a1;用来和数据库中的表对应&#xff0c;解决的是数据格式在Java和数据库间的转换。 &#xff08;一&#xff09;设计思想 数据库Java表类行对象字段&#xff08;列&#xff09;属性 &#xff08;二&#xff09;实体Entity编程 编码规范 &#x1f4a…

文本检测学习笔记_CTPN

论文地址&#xff1a;https://arxiv.org/pdf/1609.03605.pdf 开源代码&#xff1a;https://github.com/lvliguoren/pytorch_ctpn?tabreadme-ov-file 本文主要的的内容 提出了一种将文本视为由密集排列的具有固定宽度的文本候选区域组成的序列的方法。这些文本候选区域可以通…

三.Linux权限管控 1-5.Linux的root用户用户和用户组查看权限控制信息chmod命令chown命令

目录 三.Linux权限管控 1.Linux的root用户 root用户&#xff08;超级管理员&#xff09; su和exit命令 sudo命令 为普通用户配置sudo认证 三.Linux权限管控 2.用户和用户组 用户&#xff0c;用户组 用户组管理 用户管理 getent---查看系统中的用户 三.Linux权限管控…