当前位置: 首页 > news >正文

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-streammytext
--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-streammytext
--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-streammytext
---------------------------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层接口

    @Overridepublic 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方法

 @Overridepublic 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);//获取accessTokenString 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();//从配置中获取的corpIdString secret = scaQyWxBuConfig.getSecretKf();//从配置中获取的secretif (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接口获取accessTokentry {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生成JsonObjectresultJsonObject = 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;
}

相关文章:

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

1.使用场景 在使用企业微信API接口中&#xff0c;往往开发者需要使用自定义的资源&#xff0c;比如发送本地图片消息&#xff0c;设置通讯录自定义头像等。 为了实现同一资源文件&#xff0c;一次上传可以多次使用&#xff0c;这里提供了素材管理接口&#xff1a;以media_id来…...

MySQL-----初识

一 SQL的基本概述 基本概述 ▶SQL全称: Structured Query Language&#xff0c;是结构化查询语言&#xff0c;用于访问和处理数据库的标准的计算机语言。SQL语言1974年由Boyce和Chamberlin提出&#xff0c;并首先在IBM公司研制的关系数据库系统SystemR上实现。 ▶美国国家标…...

[基础IO]文件描述符{重定向/perror/磁盘结构/inode/软硬链接}

文章目录 1. 再识重定向2.浅谈perror()3.初始文件系统4.软硬链接 1. 再识重定向 图解./sf > file.txt 2>&1 1中内容拷贝给2 使得2指向file 再学一个 把file的内容传给cat cat拿到后再给file2 2.浅谈perror() open()接口调用失败返回-1,并且错误码errno被适当的设置,…...

NAS系统折腾记 – Emby搭建家庭多媒体服务器

Emby简介 Emby是一款优秀的媒体服务器软件&#xff0c;致力于为用户提供丰富的多媒体体验。通过Emby&#xff0c;您可以方便地在家庭内的各种设备上观看您喜爱的电影、电视剧和其他视频内容。而且&#xff0c;Emby还具备强大的媒体管理功能&#xff0c;让您的影视资源井然有序…...

#从零开始# 在深度学习环境中,如何用 pycharm配置使用 pipenv 虚拟环境

为Python项目创建虚拟环境 在深度学习环境和一般python环境中安装pipenv基本一致&#xff0c;只需要确认好pipenv指定的python版本即可,安装pipenv前&#xff0c;可以通过python --version来确认安装版本 快捷键&#xff1a;crtl alt S 查看interpreter&#xff0c;查看所有…...

Cmake编译Opencv3.3.1遇到有些文件无法下载的错误解决:

前言&#xff1a; 对于&#xff0c;opencv有些配置文件错误并未致命&#xff0c;所以&#xff0c;有错误也不影响后续的编译&#xff1a;但是&#xff0c;后引用如果要用&#xff0c;在回过头来还是要解决的。 问题表述&#xff1a; 比如&#xff0c;有些文件下载的错误&am…...

Python基础知识:Python序列以及序列的索引、切片、相乘和相加

索引 索引就是序列中的每个元素所在的位置&#xff0c;可以通过从左往右的正数索引&#xff0c;也可以通过从右往左的负数索引。 从左往右的正数索引&#xff1a;在python序列中&#xff0c;第一个元素的索引值为0&#xff0c;第二个元素的索引值为1&#xff0c;以此类推&…...

回归预测 | Matlab实现CPO-GRU【24年新算法】冠豪猪优化门控循环单元多变量回归预测

回归预测 | Matlab实现CPO-GRU【24年新算法】冠豪猪优化门控循环单元多变量回归预测 目录 回归预测 | Matlab实现CPO-GRU【24年新算法】冠豪猪优化门控循环单元多变量回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现CPO-GRU【24年新算法】冠豪猪优化…...

开源项目TARZAN-NAV | 基于springboot的现代化导航网站系统

TARZAN-NAV 导航网站 一个基于 Spring Boot、MyBatis-Plus、h2database、ehcache、Docker、websocket等技术栈实现的导航网站系统&#xff0c;采用主流的互联网技术架构、全新的UI设计、支持一键源码部署&#xff0c;拥有完整的仪表板、导航管理&#xff0c;用户管理、评论管理…...

SQL查询数据之多表(关联)查询

数据表&#xff1a; 关联查询主要分为&#xff1a;&#xff08;inner join&#xff09;交叉关联、&#xff08;left join&#xff09;左关联、&#xff08;right join&#xff09;右关联 &#xff08;inner join&#xff09;交叉关联&#xff1a; 定义&#xff1a;&#xff0…...

常见的web前端开发框架介绍

Web前端开发框架是为了简化网页设计和开发的流程而创建的工具集。它们提供了预定义的组件、工具和库&#xff0c;帮助开发者快速构建交互式的用户界面。以下是一些常见的Web前端开发框架&#xff0c;以及它们的原理、基础技术和应用场景的介绍&#xff1a; 1. React **…...

CSS 选择器与相关规则详解

CSS&#xff08;Cascading Style Sheets&#xff09;的选择器是网页样式设计中至关重要的工具&#xff0c;它们允许开发者精确地定位并应用样式到HTML文档中的元素。下面将逐一介绍几种主要的选择器类型&#xff0c;以及相关的注释和规则。 1. 类选择器 (Class Selector) 类选…...

基于springboot的宠物店系统的设计与实现

文章目录 项目介绍主要功能截图&#xff1a;部分代码展示设计总结项目获取方式 &#x1f345; 作者主页&#xff1a;超级无敌暴龙战士塔塔开 &#x1f345; 简介&#xff1a;Java领域优质创作者&#x1f3c6;、 简历模板、学习资料、面试题库【关注我&#xff0c;都给你】 &…...

Llama2大模型开源,大模型的Android时代来了?

就昨天凌晨,微软和Meta宣布Llama2大模型开源且进一步放开商用,一下朋友圈刷屏。要知道,开源界最强大的模型就是过去Meta开源的Llama,而现在Llama2更强大,又开放商用,更有微软大模型霸主企业撑腰(微软既投资大模型界的IOS——ChatGPT,又联合发布大模型的Android——Llam…...

取出list中指定数量数据操作,操作完了删除这些数据

直接看代码吧,有注释 package com.ep.crm.task;import java.util.ArrayList; import java.util.List; import java.util.Random;public class Test {public static void main(String[] args) {List<String> list new ArrayList<String>();// 生成随机整数Random …...

Cocos XR的WebBox实现流程

1. 正常3D场景下的webview 1.1 组件角色 Cocos Creator正常3D场景下只有在UI组件才支持webview&#xff0c;即作为下图中的UI Nodes(Canvas Node)的子节点&#xff0c;和3D组件是隔离开的&#xff0c;不能显示在3D空间中&#xff0c;UI Nodes(Canvas Node)是一个平面内的矩形…...

netstat是一个常用的网络工具,用于显示和分析网络连接、路由表以及网络接口等信息。

netstat 是一个常用的网络工具&#xff0c;用于显示和分析网络连接、路由表以及网络接口等信息。 它可以提供关于网络活动的实时统计数据&#xff0c;包括正在监听的端口、已建立的连接、网络接口的状态等。 使用 netstat 命令可以列出当前系统中的网络连接情况。以下是一些常…...

【Linux】linux权限

linux权限 一&#xff0c;Linux权限的概念二&#xff0c;Linux权限管理1.文件访问者分类2.文件类型和访问权限3.文件权限值的表示方法4.文件访问权限的设置 三&#xff0c;目录的权限四&#xff0c;粘滞位五&#xff0c;权限掩码1.什么是权限掩码2.权限掩码的计算 一&#xff0…...

XUbuntu22.04之如何创建、切换多个工作区(二百零九)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…...

网络安全之SSL证书加密

简介 SSL证书是一种数字证书&#xff0c;遵守SSL协议&#xff0c;由受信任的数字证书颁发机构&#xff08;CA&#xff09;验证服务器身份后颁发。它具有服务器身份验证和数据传输加密的功能&#xff0c;能够确保数据在传输过程中的安全性和完整性。 具体来说&#xff0c;SSL证…...

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…...

【位运算】消失的两个数字(hard)

消失的两个数字&#xff08;hard&#xff09; 题⽬描述&#xff1a;解法&#xff08;位运算&#xff09;&#xff1a;Java 算法代码&#xff1a;更简便代码 题⽬链接&#xff1a;⾯试题 17.19. 消失的两个数字 题⽬描述&#xff1a; 给定⼀个数组&#xff0c;包含从 1 到 N 所有…...

Spring数据访问模块设计

前面我们已经完成了IoC和web模块的设计&#xff0c;聪明的码友立马就知道了&#xff0c;该到数据访问模块了&#xff0c;要不就这俩玩个6啊&#xff0c;查库势在必行&#xff0c;至此&#xff0c;它来了。 一、核心设计理念 1、痛点在哪 应用离不开数据&#xff08;数据库、No…...

九天毕昇深度学习平台 | 如何安装库?

pip install 库名 -i https://pypi.tuna.tsinghua.edu.cn/simple --user 举个例子&#xff1a; 报错 ModuleNotFoundError: No module named torch 那么我需要安装 torch pip install torch -i https://pypi.tuna.tsinghua.edu.cn/simple --user pip install 库名&#x…...

React---day11

14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store&#xff1a; 我们在使用异步的时候理应是要使用中间件的&#xff0c;但是configureStore 已经自动集成了 redux-thunk&#xff0c;注意action里面要返回函数 import { configureS…...

C语言中提供的第三方库之哈希表实现

一. 简介 前面一篇文章简单学习了C语言中第三方库&#xff08;uthash库&#xff09;提供对哈希表的操作&#xff0c;文章如下&#xff1a; C语言中提供的第三方库uthash常用接口-CSDN博客 本文简单学习一下第三方库 uthash库对哈希表的操作。 二. uthash库哈希表操作示例 u…...

DBLP数据库是什么?

DBLP&#xff08;Digital Bibliography & Library Project&#xff09;Computer Science Bibliography是全球著名的计算机科学出版物的开放书目数据库。DBLP所收录的期刊和会议论文质量较高&#xff0c;数据库文献更新速度很快&#xff0c;很好地反映了国际计算机科学学术研…...

【堆垛策略】设计方法

堆垛策略的设计是积木堆叠系统的核心&#xff0c;直接影响堆叠的稳定性、效率和容错能力。以下是分层次的堆垛策略设计方法&#xff0c;涵盖基础规则、优化算法和容错机制&#xff1a; 1. 基础堆垛规则 (1) 物理稳定性优先 重心原则&#xff1a; 大尺寸/重量积木在下&#xf…...

【Post-process】【VBA】ETABS VBA FrameObj.GetNameList and write to EXCEL

ETABS API实战:导出框架元素数据到Excel 在结构工程师的日常工作中,经常需要从ETABS模型中提取框架元素信息进行后续分析。手动复制粘贴不仅耗时,还容易出错。今天我们来用简单的VBA代码实现自动化导出。 🎯 我们要实现什么? 一键点击,就能将ETABS中所有框架元素的基…...

ubuntu22.04 安装docker 和docker-compose

首先你要确保没有docker环境或者使用命令删掉docker sudo apt-get remove docker docker-engine docker.io containerd runc安装docker 更新软件环境 sudo apt update sudo apt upgrade下载docker依赖和GPG 密钥 # 依赖 apt-get install ca-certificates curl gnupg lsb-rel…...