字节流及存放本地文件上传和下载文件
前言
之前的文章有写过
vue+springboot使用文件流实现文件下载
实现如何通过
D:\file\文件名.文件格式
的形式进行下载文件
但是它对于很多业务场景相对适用性不是很广泛。
以及
elementUI加springboot实现上传excel文件给后端并读取excel
也只能是通过elementui的元素类型进行上传。
因此,本次文章将通过两种方式
存放本地文件和存放字节流两种方式教大家如何进行文件的上传和下载,适用绝大部分场景.
存放本地文件实现上传和下载
我们在开发完项目,并且将项目部署到服务器的时候,如果我们有实现上传的功能,实际上还是保存文件在部署项目的这个服务器上,下载也是从服务器下载,因此,我们就可以直接使用存放本地文件的方式实现上传和下载
存放本地文件-上传
我们都知道,在前端如果需要接收文件的传递给后端的话,可以使用
formData进行保存数据,那么对应前端代码如下:
前端
为演示方便,后续上传都使用原生的input进行获取文件
界面
<input type="file" id="upload" />
<button onclick="uploadFile">上传</button>
js逻辑
uploadFile() {// 获取上传图片let dom = document.getElementById("upload");// 定义接收文件的formData,并用"file"去接收数据let formDataInfo = new FormData();// 接收第一个文件,适用单文件formDataInfo.append("file",dom.files[0]);// 需要引入axiosthis.$axios({method: 'post', // 为post请求url: "/fileUplod"// 请求后端的路径,改成自己的就行,data: formDataInfo,// 传递参数responseType: 'json',// 响应类型}).then(res = > {// 响应数据})
以上是适用于单文件,如果需要上传多文件,参考如下代码
uploadFile() {// 获取上传图片let dom = document.getElementById("upload");// 定义接收文件的formData,并用"file"去接收数据let formDataInfo = new FormData();// 接收文件数量formDataInfo.append("length",dom.files.length)// 遍历文件加入formData,后端处理的时候获取length的长度进行遍历文件for (let i = 0;i<dom.files.length;i++) {formDataInfo.append("file"+i,dom.files[i]);}// 需要引入axiosthis.$axios({method: 'post', // 为post请求url: "/fileUplod"// 请求后端的路径,改成自己的就行,data: formDataInfo,// 传递参数responseType: 'json', // 响应类型}).then(res = > {// 响应数据})
}
后端
@POST@Path("fileUplod")public String fileUplod(FormDataMultiPart form) {try {// 需要保存文件路径String path = "D:/file";// 文件流InputStream inputStream= form.getField("file").getValueAs(InputStream.class);// 文件名String fileName = form.getField("file").getContentDisposition().getFileName();// 调用下面的保存本地文件方法uploadFileToLocal(inputStream,path,fileName);return "上传文件成功!";}catch(Exception e) {return "上传文件到本地磁盘失败" + e.getMessage();}}/*** @param inputStream 文件流* @param path 上传路径* @param fileName 文件名* @return 如果返回含义字符串【报错】上传失败,否则返回文件路径* @author ks027276* @description 上传文件到本地方法*/public String uploadFileToLocal(InputStream inputStream, String path, String fileName) throws Exception {try {File folder = new File(path);// 创建没有的文件夹if (!folder.isDirectory()) {folder.mkdirs();}//创建空文件FileOutputStream outputStream = new FileOutputStream(path + "/" + fileName);// 将文件流数据填充到空文件int index = 0;byte[] bytes = new byte[1024];while ((index = inputStream.read(bytes)) != -1) {outputStream.write(bytes, 0, index);outputStream.flush();}inputStream.close();outputStream.close();return path + "/" + fileName;} catch (Exception e) {return "【报错】" + e.getMessage();}}
假如需要获取文件名,使用代码:
String fileName = form.getField("file").getContentDisposition().getFileName();
值得一提的是,如果上传文件名为中文,那么这个方式上传会乱码,需要处理为如下:
String fileName = new String(form.getField("file").getContentDisposition().getFileName()
.getBytes("iso-8859-1"), "UTF-8");
上述皆为单文件上传的方式,假如使用我上方前端多文件上传,逻辑其实差不多,就是多了一个遍历而已,具体逻辑如下:
@POST@Path("fileUplod")public String fileUplod(FormDataMultiPart form) {try {// 需要保存文件路径String path = "D:/file";for (int i = 0;i <Integer.valueof(form.getField("length").getvalue());i++) {// 文件流InputStream inputStream= form.getField("file"+i).getValueAs(InputStream.class);// 文件名String fileName = form.getField("file"+i).getContentDisposition().getFileName();// 调用下面的保存本地文件方法uploadFileToLocal(inputStream,path,fileName);}return "上传文件成功!";}catch(Exception e) {return "上传文件到本地磁盘失败" + e.getMessage();}
}/*** @param inputStream 文件流* @param path 上传路径* @param fileName 文件名* @return 如果返回含义字符串【报错】上传失败,否则返回文件路径* @author ks027276* @description 上传文件到本地方法*/public String uploadFileToLocal(InputStream inputStream, String path, String fileName) throws Exception {try {File folder = new File(path);// 创建没有的文件夹if (!folder.isDirectory()) {folder.mkdirs();}//创建空文件FileOutputStream outputStream = new FileOutputStream(path + "/" + fileName);// 将文件流数据填充到空文件int index = 0;byte[] bytes = new byte[1024];while ((index = inputStream.read(bytes)) != -1) {outputStream.write(bytes, 0, index);outputStream.flush();}inputStream.close();outputStream.close();return path + "/" + fileName;} catch (Exception e) {return "【报错】" + e.getMessage();}}
保存结果
以下截图为我用上方功能实现保存到本地:

存放本地文件-下载
下载的话,既然上面已经接收到了路径,那么直接用路径去查找就好了
路径形式如下:
D:/file/1.jpg
前端代码
<button onclick="getFile">获取文件</button>
// path值形式为:D:/file/1.jpg
// name值形式为:1.jpg
getFile(path,name) {// 需要引入axiosthis.axios({method: 'get', // 为get请求url: "/downFile"// 请求后端的路径,改成自己的就行,data: {path: path,name: name},// 传递参数responseType: 'json',// 响应类型}).then(res => {var a = document.createElement('a');a.href = res.data // 这里需要根据自己实际项目作变更,可能你的数据在res,或res.data或res.data.datadocument.body.appendChild(a);a.click();document.body.removeChild(a);})}
后端
/*** 文件下载 根据路径*/
// response类型引用为:
import javax.ws.rs.core.Response;@GET@Path("/downFile")public Response downLoadPanorama(@QueryParam("path") String path,@QueryParam("name") String name) {try {FileInputStream inputStream = new FileInputStream(path);byte[] bytes = toByteArray(inputStream);return Response.ok(new StreamingOutput() {@Overridepublic void write(OutputStream output) throws IOException, WebApplicationException {try {output.write(bytes);} catch (Exception ex) {}}}).header("Content-disposition","attachment;filename=" + name).header("Cache-Control", "no-cache").build();} catch (Exception e) {e.printStackTrace();return Response.status(Response.Status.NOT_FOUND).build();}}
使用上面的代码会将文件下载下来
形式如:

存放字节流的形式上传和下载
一般来说,会更推荐使用字节流的方式进行实现文件的上传和下载,因为是文件流,所以我们可以更方便便捷的用它实现各种操作。
存字节流实际上是保存在数据库,因此对数据库的内存会占用更多。
存放字节流-上传
首先,数据库表的字段需要新建blob类型的字段接收数据

在java代码对象需要用byte数组类型接收文件转字节流的字段属性接收
@Data
public class FileObject {private byte[] file;
}
前端
界面
<input type="file" id="upload" />
<button onclick="uploadFile">上传</button>
js逻辑
uploadFile() {// 获取上传图片let dom = document.getElementById("upload");// 定义接收文件的formData,并用"file"去接收数据let formDataInfo = new FormData();// 接收第一个文件,适用单文件formDataInfo.append("file",dom.files[0]);// 需要引入axiosthis.$axios({method: 'post', // 为post请求url: "/fileUplod"// 请求后端的路径,改成自己的就行,data: formDataInfo,// 传递参数responseType: 'json',// 响应类型}).then(res = > {// 响应数据})
以上是适用于单文件,如果需要上传多文件,参考如下代码
uploadFile() {// 获取上传图片let dom = document.getElementById("upload");// 定义接收文件的formData,并用"file"去接收数据let formDataInfo = new FormData();// 接收文件数量formDataInfo.append("length",dom.files.length)// 遍历文件加入formData,后端处理的时候获取length的长度进行遍历文件for (let i = 0;i<dom.files.length;i++) {formDataInfo.append("file"+i,dom.files[i]);}// 需要引入axiosthis.$axios({method: 'post', // 为post请求url: "/fileUplod"// 请求后端的路径,改成自己的就行,data: formDataInfo,// 传递参数responseType: 'json', // 响应类型}).then(res = > {// 响应数据})
}
其实逻辑是一样的,和存放本地文件上传的数据一样
最主要的逻辑差距在后端
后端
@POST@Path("fileUplod")public String fileUplod(FormDataMultiPart form) {try {// 文件流InputStream inputStream= form.getField("file").getValueAs(InputStream.class);// 调用下面的保存本地文件方法byte[] fileBytes = getBytes(inputStream);// 调用新增数据库sql存到数据库// 这边就属于自己的功能逻辑了,我以下就不写了return "上传文件成功!";}catch(Exception e) {return "上传文件到数据库失败" + e.getMessage();}
}/*** 存字节流到数据库* @param inputStream 字节流* 有值说明获取成功,无值说明失败*/public byte[] getBytes(InputStream inputStream) throws Exception {try {BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();byte[] buffer;int len;byte[] buf = new byte[2048];while ((len = bufferedInputStream.read(buf)) != -1) {byteArrayOutputStream.write(buf, 0, len);}byteArrayOutputStream.flush();buffer = byteArrayOutputStream.toByteArray();inputStream.close();return buffer;} catch (Exception e) {return null;}}
上述皆为单文件上传的方式,假如使用我上方前端多文件上传,逻辑其实差不多,就是多了一个遍历而已,具体逻辑如下:
@POST@Path("fileUplod")public String fileUplod(FormDataMultiPart form) {try {for (int i = 0;i <Integer.valueof(form.getField("length").getvalue());i++) {// 文件流InputStream inputStream= form.getField("file"+i).getValueAs(InputStream.class);// 调用下面的保存本地文件方法byte[] "file"+i = getBytes(inputStream);// 调用新增数据库sql存到数据库// 这边就属于自己的功能逻辑了,我以下就不写了}return "上传文件成功!";}catch(Exception e) {return "上传文件到数据库失败" + e.getMessage();}
}/*** 存字节流到数据库* @param inputStream 字节流* 有值说明获取成功,无值说明失败*/public byte[] getBytes(InputStream inputStream) throws Exception {try {BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream);ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();byte[] buffer;int len;byte[] buf = new byte[2048];while ((len = bufferedInputStream.read(buf)) != -1) {byteArrayOutputStream.write(buf, 0, len);}byteArrayOutputStream.flush();buffer = byteArrayOutputStream.toByteArray();inputStream.close();return buffer;} catch (Exception e) {return null;}}
如果需要获取文件名,参照上面存放本地文件方式有说明
存到数据库结果为:

是一个BLOB数据
存放字节流-下载
通过sql查询语句我们查找到存放的字节流文件数据,展示到前端,结果为:

是一个Array数组的形式
后端逻辑就不写了,单纯就是一个sql查数据而已
如果是图片文件,可以使用一个img元素直接展示图片
<img :src="base64"/>
<button onclick="showImage">上传</button>
上面我们绑定了一个自定义属性:base64,js逻辑直接把它拿过来赋值即可
showImage(byte,type) {let binary = "";const bytes = new Uint8Array(byte);const len = bytes.byteLength;for (let i = 0; i < len; i++) {binary +=String.fromCharCode(bytes[i]);}// base64前缀let base64Before = "";if (type == 'jpg' || type == 'jpeg') {base64Before = 'data:image/jpeg;base64,';} else if (type == 'png') {base64Before = 'data:image/png;base64,';}this.base64 =base64Before +window.btoa(binary);
}
假如你需要拿到字节流下载文件的话,使用如下逻辑:
// byte,字节流,数据形式如: [11,-1,44,20,......]
// _type,数据形式如: jpg txt
// name,文件名,数据形式如 1.jpg 2.txt
downFile (byte,_type,name) {var eleLink = document.createElement('a');// 根据文件类型设定blob文件类型let fileType = this.extToMimes(_type);// 设定下载文件名eleLink.download = name;// 设置a标签不显示eleLink.style.display = 'none';// 将文件流转Uint8Arrayconst bytes = new Uint8Array(byte);// 将转换后的数据和获取的文件类型,创建blobvar blob = new Blob([bytes],{type: fileType});eleLink.href = URL.createObjectURL(blob);// 自动触发点击document.body.appendChild(eleLink);eleLink.click();// 然后移除document.body.removeChild(eleLink);
}
// 根据传入的类型设定blob文件类型extToMimes(ext) {let type = undefined;switch (ext) {case 'jpg':type = 'image/jpeg'case 'png':type = 'image/png'case 'jpeg':type = 'image/jpeg'break;case 'txt':type = 'text/plain'break;case 'xls':type = 'application/vnd.ms-excel'break;case 'doc':type = 'application/msword'break;case 'xlsx':type = 'application/vnd.ms-excel'break;default:}return type;}
下载结果
通过以上,可以下载文件,并且正确显示出内容

测试有效
结语
以上,为通过字节流和本地文件的方式进行上传和下载的方法。
相关文章:
字节流及存放本地文件上传和下载文件
前言 之前的文章有写过 vuespringboot使用文件流实现文件下载 实现如何通过 D:\file\文件名.文件格式的形式进行下载文件 但是它对于很多业务场景相对适用性不是很广泛。 以及 elementUI加springboot实现上传excel文件给后端并读取excel 也只能是通过elementui的元素类型进行…...
【翻译】下一步:Go 泛型
原文地址: The Next Step for Generics - The Go Blog https://blog.golang.org/generics-next-step 介绍 自从我们上次写下关于在Go中加入泛型的可能性的文章以来,已经快一年了。现在是该更新的时候了。 设计的更新 我们一直在继续完善泛型设计草案。…...
如何简单实现ELT?
在商业中,数据通常和业务、企业前景以及财务状况相关,有效的数据管理可以帮助决策者快速有效地从大量数据中分析出有价值的信息。数据集成(Data Integration)是整个数据管理流程中非常重要的一环,它是指将来自多个数据源的数据组合在一起&…...
细思极恐,第三方跟踪器正在获取你的数据,如何防范?
细思极恐,第三方跟踪器正在获取你的数据,如何防范? 当下,许多网站都存在一些Web表单,比如登录、注册、评论等操作需要表单。我们都知道,我们在冲浪时在网站上键入的数据会被第三方跟踪器收集。但是&#x…...
Java基础之==,equal的区别(温故而知新)-----点点滴滴的积累
1. 为运算符,equal 为String数据类型的比较方法;相同内容的对象地址不一定相同,但相相同地址的对象内容一定相同; 比较的是值是否相等,equal比较的是是否是同一个对象。 2.基本概念不同 1)对于,…...
SpringBoot项目使用切面编程实现数据权限管理
springBoot项目使用切面编程实现数据权限管理什么是数据权限管理如何实现数据权限管理什么是数据权限管理 不同用户在某页面看到数据不一致,实现每个用户之间数据隔离的效果。 如以下场景: ● 页面期望展示当前登录人所在部门的数据。 ● 页面期望展示当…...
亚马逊测评是做什么的,风险有哪些?
自养号测评顾名思义就是自己养国外的买家账号给自己店铺提升销量和评论,做过多年的跨境卖家都知道测评可以快速提高产品的排名、权重和销量,(国内某宝一样的逻辑)但随着测评需求日益增大,卖家在寻求真人测评时也很容易…...
安科瑞导轨式智能通讯管理机
安科瑞 李亚娜 一、概述 AWT200 数据通讯网关应用于各种终端设备的数据采集与数据分析。实现设备的监测、控制、计算,为系统与设备之间建立通讯纽带,实现双向的数据通讯。实时监测并及时发现异常数据,同时自身根据用户规则进行逻辑判断&…...
vs2010下 转换到 COFF 期间失败: 文件无效或损坏
因为同一个电脑上安装多个VS,有多个cvtres.exe。按照下面的操作如果还是不行就在C盘搜索cvtres.exe,然后挨个重命名,看看是调用的哪个,然后修改就可以了。 用VS2010编译C项目时出现这样的错误: LNK1123: 转换到 COFF …...
托福高频真词List19 // 附托福TPO阅读真题
目录 3.28单词 3.29真题 3.28单词 legitimately/properlyadv.正当地likewise/similarlyadv.同样地reveal/showv.揭示substantiate/confirmv.证实suppress/stop by forcev.镇压trend/tendencyn.趋势empirical/based on observationa.凭借经验的illuminate/li…...
Go语言项目标准结构应该如何组织的?
这里写自定义目录标题Go项目本身的目录结构Go语言项目典型目录结构GO语言项目最小标准目录结构可执行的Go语言项目目录结构库的Go语言项目目录结构关于internal目录总结参考文章每当我们写一个非hello world实用程序的Go程序或库时,我们都会在项目结构、代码风格和标…...
设计模式简介
设计模式简介 设计模式(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错…...
#详细介绍!!! 线程池的拒绝策略(经典面试题)
本篇单独讲解线程池的拒绝策略,介绍了当线程池任务满了之后,线程池会以什么样的方式来响应添加进来的任务 目录 一:理解线程池拒绝策略的触发情况代码理解 二:线程池的四种常见的拒绝策略 1.ThreadPoolExecutor.AbortPolicy 2…...
正则表达式作业
利用正则表达式完成下面的操作: 一、不定项选择题 能够完全匹配字符串"(010)-62661617"和字符串"01062661617"的正则表达式包括(A ) A. r"\(?\d{3}\)?-?\d{8}" B. r"[0-9()-]" C. r"[0-9(-)]*\d*&qu…...
《扬帆优配》交易拥挤度达历史极值 当前A股TMT板块性价比几何?
上周,A股商场企稳,但盘面风格分歧再度加深:很多资金涌入以ChatGPT、数字经济为代表的TMT板块,而新能源以及前期强势的“中字头”种类都呈现了回调。兴业证券计算显现,3月24日,TMT及电子板块的商场成交金额占…...
C/C++开发,无可避免的IO输入/输出(篇三).字符串流(内存流)IO处理
目录 一、字符串流 1.1 字符串流继承体系 1.2 字符串流本质-类模板std::basic_stringstream 1.3 字符串流缓冲-std::stringbuf 1.4 stringbuf与序列缓冲 1.5 字符串流的打开模式 二、字符串流的运用 2.1 格式转换是其拿手好戏 2.2 字符串流仅提供移动赋值 2.3 std::basic_str…...
什么是HTTP请求?【JavaWeb技术】
HTTP请求是指从客户端到服务器的请求消息,建立HTTP请求需要经历以下7个步骤才能请求成功。 (1)建立TCP连接 在HTTP开始工作前,Web浏览器需先通过网络和Web服务器连接,连接过程主要使用TCP/IP完成。 (2)Web浏览器向Web服务器发送请求命令 一旦…...
浅聊面试这件事
目录 哪个时间点适合跳槽 如何准备面试 面试原则 面试常见问题 哪个时间点适合跳槽 金三银四、金九银十,这些都📌标记为我们的最佳跳槽节点,但是这些节点真的是最佳的么,也需要因人而异。 如果公司年前不发年终奖,…...
【致敬未来的攻城狮计划】连续打卡第7天+瑞萨RA2E1点亮LED
开启攻城狮的成长之旅!这是我参与的由 CSDN博客专家 架构师李肯(http://yyds.recan-li.cn)和 瑞萨MCU (瑞萨电子 (Renesas Electronics Corporation) ) 联合发起的「 致敬未来的攻城狮计划 」的第 7 天,点击…...
Sam Altman专访:GPT-4没太让我惊讶,ChatGPT则让我喜出望外
导读ChatGPT、GPT-4 无疑是 2023 年年初人工智能界最大的「爆款」。3 月 26 日,OpenAI CEO、ChatGPT 之父 Sam Altman 接受了著名学者与科技播客、麻省理工大学研究员 Lex Fridman 的专访,Sam 分享了从OpenAI内部视角如何看待ChatGPT和GPT-4的里程碑式意…...
揭秘Windows热键失踪案:Hotkey Detective侦探手册
揭秘Windows热键失踪案:Hotkey Detective侦探手册 【免费下载链接】hotkey-detective A small program for investigating stolen hotkeys under Windows 8 项目地址: https://gitcode.com/gh_mirrors/ho/hotkey-detective 你是否曾在Windows系统中按下熟悉的…...
Phi-3-mini-4k-instruct-gguf实操手册:短问答/改写/摘要三大高频场景落地
Phi-3-mini-4k-instruct-gguf实操手册:短问答/改写/摘要三大高频场景落地 1. 模型简介与核心能力 Phi-3-mini-4k-instruct-gguf是微软推出的轻量级文本生成模型,基于Phi-3系列优化而来。这个GGUF版本特别适合处理短文本任务,具有以下特点&a…...
Kazumi:跨平台动漫资源整合解决方案,打造个性化追番体验
Kazumi:跨平台动漫资源整合解决方案,打造个性化追番体验 【免费下载链接】Kazumi 基于自定义规则的番剧采集APP,支持流媒体在线观看,支持弹幕。 项目地址: https://gitcode.com/gh_mirrors/ka/Kazumi 动漫爱好者常面临三大…...
Neeshck-Z-lmage_LYX_v2多场景落地:LoRA动态加载赋能数字人直播背景实时生成系统
Neeshck-Z-lmage_LYX_v2多场景落地:LoRA动态加载赋能数字人直播背景实时生成系统 1. 项目简介:一个专为本地绘画优化的轻量级工具 如果你对AI绘画感兴趣,特别是想体验国产的Z-Image文生图模型,但又被复杂的部署流程、繁琐的参数…...
医疗器械小白必看:B型、BF型、CF型设备到底怎么选?附真实医院案例解析
医疗器械采购指南:B型、BF型与CF型设备的实战选择策略 去年某三甲医院ICU因监护仪选型不当导致患者数据异常的事件,让医疗器械电气安全标准重新成为行业焦点。作为医疗设备采购人员,面对B型、BF型、CF型这些专业术语时,是否常感到…...
Pixel Epic效果展示:支持Markdown+LaTeX混合输出的学术论文初稿生成案例
Pixel Epic效果展示:支持MarkdownLaTeX混合输出的学术论文初稿生成案例 1. 像素史诗:科研写作的新范式 在传统学术写作工具普遍沉闷单调的背景下,Pixel Epic带来了一场视觉与功能双重革新的科研体验。这款基于AgentCPM-Report大模型的智能终…...
AEC-Q100到AEC-Q200:汽车电子组件认证标准差异与应用场景详解
AEC-Q100到AEC-Q200:汽车电子组件认证标准差异与应用场景详解 当一辆现代汽车驶过零下40度的北极圈,又穿越50度的沙漠高温,其电子系统仍需要保持毫秒级的响应精度——这种极端可靠性背后,是AEC-Q系列认证标准构筑的质量防线。作为…...
Java Web新手必看:EDUCODER头哥MVC用户登录实战(含JDBC连接避坑指南)
Java Web新手实战:EDUCODER平台MVC用户登录全流程解析 第一次接触Java Web开发时,最让人兴奋的莫过于亲手实现一个完整的用户登录系统。这不仅是对MVC架构的直观理解,更是打通前后端数据流的关键里程碑。在EDUCODER这样的实训平台上ÿ…...
Graph Node高级配置:环境变量与配置文件详解
Graph Node高级配置:环境变量与配置文件详解 【免费下载链接】graph-node Graph Node indexes data from blockchains such as Ethereum and serves it over GraphQL 项目地址: https://gitcode.com/gh_mirrors/gr/graph-node Graph Node 作为区块链数据索引…...
Qwen3.5-2B部署教程:Conda+Supervisor环境一键拉起,告别手动配置
Qwen3.5-2B部署教程:CondaSupervisor环境一键拉起,告别手动配置 1. 前言:认识Qwen3.5-2B轻量化模型 Qwen3.5-2B是Qwen3.5系列中的轻量化多模态基础模型,仅有20亿参数规模,专为低功耗、低门槛部署场景设计。这个模型特…...
