18.将文件上传至云服务器 + 优化网站的性能

目录

1.将文件上传至云服务器

1.1 处理上传头像逻辑

1.1.1 客户端上传

1.1.2 服务器直传

2.优化网站的性能

2.1 本地缓存优化查询方法

2.2 压力测试


1.将文件上传至云服务器

  • 客户端上传:客户端将数据提交给云服务器,并等待其响应;用户上传头像时,将表单数据提交给服务器
  • 服务器直传:应用服务器将数据直接提交给云服务器,并等待其响应;分享时,服务端将自动生成的图片,直接提交给云服务器

使用七牛云服务器

导入依赖:

<!-- https://mvnrepository.com/artifact/com.qiniu/qiniu-java-sdk -->
<dependency><groupId>com.qiniu</groupId><artifactId>qiniu-java-sdk</artifactId><version>7.12.1</version>
</dependency>

在 application.properties 中配置:

# qiniu
qiniu.key.access=NHzlA2MRle10vB0wNeO54aGS-tMjEpO5BiIrflz9
qiniu.key.secret=qicgWpPOslm5_dFu_j_94r5gUcKm_UekhT2MMLPf
qiniu.bucket.header.name=communityheader123321
quniu.bucket.header.url=http://s6sc3za8f.hb-bkt.clouddn.com
qiniu.bucket.share.name=communityshare123321
qiniu.bucket.share.url=http://s6scgzhqs.hb-bkt.clouddn.com

1.1 处理上传头像逻辑

1.1.1 客户端上传

打开 UserController 类:

  • 注入上述的两个 key
  • 注入有关头像的空间内容
  • 废弃原来上传头像方法:上传头像因为有表单,所以在客户端上传,表单直接提交给七牛云,废弃此方法
  • 上传文件同样废弃
  • 在打开用户设置的页面时需要生成凭证(设置上传文件名称,响应信息),将凭证写到表单中,当打开表单时,表单中应该有凭证
  • 最后传给模板
  • 还需要添加一个方法:在表单中将数据提交给七牛云,七牛云返回一个消息,然后将 User 表中的 headurl 做一个更新,更新为七牛云的路径
@Value("${qiniu.key.access}")private String accessKey;@Value("${qiniu.key.secret}")private String secretKey;@Value("${qiniu.bucket.header.name}")private String headerBucketName;@Value("${quniu.bucket.header.url}")private String headerBucketUrl;@LoginRequired@RequestMapping(path = "/setting", method = RequestMethod.GET)//添加方法使得浏览器通过方法访问到设置的页面public String getSettingPage(Model model) {// 上传文件名称String fileName = CommunityUtil.generateUUID();// 设置响应信息StringMap policy = new StringMap();policy.put("returnBody", CommunityUtil.getJSONString(0));// 生成上传凭证Auth auth = Auth.create(accessKey, secretKey);String uploadToken = auth.uploadToken(headerBucketName, fileName, 3600, policy);model.addAttribute("uploadToken", uploadToken);model.addAttribute("fileName", fileName);return "/site/setting";}// 更新头像路径@RequestMapping(path = "/header/url", method = RequestMethod.POST)@ResponseBodypublic String updateHeaderUrl(String fileName) {if (StringUtils.isBlank(fileName)) {return CommunityUtil.getJSONString(1, "文件名不能为空!");}String url = headerBucketUrl + "/" + fileName;userService.updateHeader(hostHolder.getUser().getId(), url);return CommunityUtil.getJSONString(0);}

再处理表单 setting.html:

                <!-- 上传头像 --><h6 class="text-left text-info border-bottom pb-2">上传头像</h6><!--上传到本地--><!--<form class="mt-5" method="post" enctype="multipart/form-data" th:action="@{/user/upload}"><div class="form-group row mt-4"><label for="head-image" class="col-sm-2 col-form-label text-right">选择头像:</label><div class="col-sm-10"><div class="custom-file"><input type="file" th:class="|custom-file-input ${error!=null?'is-invalid':''}|"id="head-image" name="headerImage" lang="es" required=""><label class="custom-file-label" for="head-image" data-browse="文件">选择一张图片</label><div class="invalid-feedback" th:text="${error}">该账号不存在!</div></div></div></div><div class="form-group row mt-4"><div class="col-sm-2"></div><div class="col-sm-10 text-center"><button type="submit" class="btn btn-info text-white form-control">立即上传</button></div></div></form>--><!--上传到七牛云--><form class="mt-5" id="uploadForm"><div class="form-group row mt-4"><label for="head-image" class="col-sm-2 col-form-label text-right">选择头像:</label><div class="col-sm-10"><div class="custom-file"><input type="hidden" name="token" th:value="${uploadToken}"><input type="hidden" name="key" th:value="${fileName}"><input type="file" class="custom-file-input" id="head-image" name="file" lang="es" required=""><label class="custom-file-label" for="head-image" data-browse="文件">选择一张图片</label><div class="invalid-feedback">该账号不存在!</div></div></div></div><div class="form-group row mt-4"><div class="col-sm-2"></div><div class="col-sm-10 text-center"><button type="submit" class="btn btn-info text-white form-control">立即上传</button></div></div></form>

在 static 包下 js 包新建 setting.js: 

$(function(){$("#uploadForm").submit(upload);
});function upload() {$.ajax({url: "http://upload-z1.qiniup.com",method: "post",processData: false,contentType: false,data: new FormData($("#uploadForm")[0]),success: function(data) {if(data && data.code == 0) {// 更新头像访问路径$.post(CONTEXT_PATH + "/user/header/url",{"fileName":$("input[name='key']").val()},function(data) {data = $.parseJSON(data);if(data.code == 0) {window.location.reload();} else {alert(data.msg);}});} else {alert("上传失败!");}}});return false;
}

1.1.2 服务器直传

需要重构分享相关的功能,打开 ShareController

  • 注入分享空间的 url
  • 修改返回浏览器的路径:七牛云空间路径 = 空间 url + / + 图片名字
  • 废弃从本地获取图片传给客户端的方法:传给七牛云之后,通过七牛云获取图片
    @Value("${qiniu.bucket.share.url}")private String shareBucketUrl;@RequestMapping(path = "/share", method = RequestMethod.GET)@ResponseBodypublic String share(String htmlUrl) {// 文件名String fileName = CommunityUtil.generateUUID();// 异步生成长图Event event = new Event().setTopic(TOPIC_SHARE).setData("htmlUrl", htmlUrl).setData("fileName", fileName).setData("suffix", ".png");//触发事件eventProducer.fireEvent(event);// 返回访问路径Map<String, Object> map = new HashMap<>();// map.put("shareUrl", domain + contextPath + "/share/image/" + fileName);map.put("shareUrl", shareBucketUrl + "/" + fileName);return CommunityUtil.getJSONString(0, null, map);}// 废弃// 获取长图@RequestMapping(path = "/share/image/{fileName}", method = RequestMethod.GET)public void getShareImage(@PathVariable("fileName") String fileName, HttpServletResponse response) {if (StringUtils.isBlank(fileName)) {throw new IllegalArgumentException("文件名不能为空!");}response.setContentType("image/png");File file = new File(wkImageStorage + "/" + fileName + ".png");try {OutputStream os = response.getOutputStream();FileInputStream fis = new FileInputStream(file);byte[] buffer = new byte[1024];int b = 0;while ((b = fis.read(buffer)) != -1) {os.write(buffer, 0, b);}} catch (IOException e) {logger.error("获取长图失败: " + e.getMessage());}}

修改 EventConsumer,逻辑是在消费者中体现

  • 在消费者中消费事件,传入两个 key 和上传空间的 name
  • 注入 ThreadPoolTaskScheduler
  • 在消费分享事件中,在执行生成图片时,启用定时器,监视该图片,一旦生成,则上传至七牛云.
    // 消费分享事件@KafkaListener(topics = TOPIC_SHARE)public void handleShareMessage(ConsumerRecord record) {if (record == null || record.value() == null) {logger.error("消息的内容为空!");return;}Event event = JSONObject.parseObject(record.value().toString(), Event.class);if (event == null) {logger.error("消息格式错误!");return;}//获取 html、文件名、后缀String htmlUrl = (String) event.getData().get("htmlUrl");String fileName = (String) event.getData().get("fileName");String suffix = (String) event.getData().get("suffix");//拼接参数String cmd = wkImageCommand + " --quality 75 "+ htmlUrl + " " + wkImageStorage + "/" + fileName + suffix;//执行命令try {Runtime.getRuntime().exec(cmd);logger.info("生成长图成功: " + cmd);} catch (IOException e) {logger.error("生成长图失败: " + e.getMessage());}// 启用定时器,监视该图片,一旦生成了,则上传至七牛云.UploadTask task = new UploadTask(fileName, suffix);//触发定时器执行Future future = taskScheduler.scheduleAtFixedRate(task, 500);//完成任务后定时器关闭task.setFuture(future);}//相当于线程体class UploadTask implements Runnable {// 文件名称private String fileName;// 文件后缀private String suffix;// 启动任务的返回值private Future future;// 开始时间private long startTime;// 上传次数private int uploadTimes;public UploadTask(String fileName, String suffix) {this.fileName = fileName;this.suffix = suffix;this.startTime = System.currentTimeMillis();}public void setFuture(Future future) {this.future = future;}@Overridepublic void run() {// 生成失败if (System.currentTimeMillis() - startTime > 30000) {logger.error("执行时间过长,终止任务:" + fileName);future.cancel(true);return;}// 上传失败if (uploadTimes >= 3) {logger.error("上传次数过多,终止任务:" + fileName);future.cancel(true);return;}//没有上述情况继续执行相关逻辑//从本地中寻找文件String path = wkImageStorage + "/" + fileName + suffix;//本地路径File file = new File(path);if (file.exists()) {logger.info(String.format("开始第%d次上传[%s].", ++uploadTimes, fileName));// 设置响应信息StringMap policy = new StringMap();// 成功返回0policy.put("returnBody", CommunityUtil.getJSONString(0));// 生成上传凭证Auth auth = Auth.create(accessKey, secretKey);String uploadToken = auth.uploadToken(shareBucketName, fileName, 3600, policy);// 指定上传机房UploadManager manager = new UploadManager(new Configuration(Zone.zone1()));try {// 开始上传图片Response response = manager.put(path, fileName, uploadToken, null, "image/" + suffix, false);// 处理响应结果JSONObject json = JSONObject.parseObject(response.bodyString());if (json == null || json.get("code") == null || !json.get("code").toString().equals("0")) {logger.info(String.format("第%d次上传失败[%s].", uploadTimes, fileName));} else {logger.info(String.format("第%d次上传成功[%s].", uploadTimes, fileName));future.cancel(true);}} catch (QiniuException e) {logger.info(String.format("第%d次上传失败[%s].", uploadTimes, fileName));}} else {logger.info("等待图片生成[" + fileName + "].");}}}

2.优化网站的性能

本地缓存:将数据缓存在应用服务器上,性能最好;常用的缓存工具:Ehcache、Guava、Caffeine等

分布式缓存:将数据缓存在 NoSQL 数据库上,跨服务器;常用缓存工具:MemCache、Redis等

多级缓存:一级缓存 > 二级缓存 > DB;避免缓存雪崩(缓存失效,大量请求直达DB),提高系统的可用性

2.1 本地缓存优化查询方法

本地缓存主要使用 Caffeine 工具,导入依赖:

        <dependency><groupId>com.github.ben-manes.caffeine</groupId><artifactId>caffeine</artifactId><version>2.7.0</version></dependency>

设置自定义参数:

# caffeine
caffeine.posts.max-size=15
caffeine.posts.expire-seconds=180

优化查询方法(discussPostService):

  • 初始化 Logger,记录日志
  • 注入上述申明参数
  • Caffeine核心接口: Cache, LoadingCache(同步缓存:多个线程同时访问缓存中的数据,但是缓存中没有数据,让多个线程排队等待,然后缓存去数据库中取), AsyncLoadingCache(异步缓存:支持并发同时取数据)
  • 声明帖子列表缓存,使用 LoadingCache<String, List<DiscussPost>>(使用 key 缓存 value)
  • 声明帖子总数缓存,使用 LoadingCache<Integer, Integer>
  • 在服务启动或者首次调用 DiscussPostService,初始化一次上述两个缓存即可
  • 给当前类新增初始化方法,添加注解 @PostConstruct,初始化帖子列表缓存和帖子总数缓存
  • 在查询某一页的方法中需要启动缓存的条件:缓存热门帖子(orderMode == 1)并且缓存首页(访问首页的时候,userId 是不传为0),缓存的是一页的数据(key 为 offset 和 limit 组合);否则访问数据库,在访问之前记录一下日志(load post list from DB.)
  • 在查询总数的方法中:用户查看自己帖子的时候不需要缓存,但是当 userId == 0 为首页查询,需要缓存帖子总数
    private static final Logger logger = LoggerFactory.getLogger(DiscussPostService.class);@Value("${caffeine.posts.max-size}")private int maxSize;@Value("${caffeine.posts.expire-seconds}")private int expireSeconds;// Caffeine核心接口: Cache, LoadingCache, AsyncLoadingCache// 帖子列表缓存private LoadingCache<String, List<DiscussPost>> postListCache;// 帖子总数缓存private LoadingCache<Integer, Integer> postRowsCache;@PostConstructpublic void init() {// 初始化帖子列表缓存postListCache = Caffeine.newBuilder().maximumSize(maxSize)//缓存最大数据量.expireAfterWrite(expireSeconds, TimeUnit.SECONDS)//把缓存写入缓存空间多长时间自动过期.build(new CacheLoader<String, List<DiscussPost>>() {//当尝试从缓存中取数据,caffeine会看缓存是否有数据://没有,需要提供一个查询的方法,load就是实现这个查询方法@Nullable@Overridepublic List<DiscussPost> load(@NonNull String key) throws Exception {//实现访问数据库查数据if (key == null || key.length() == 0) {throw new IllegalArgumentException("参数错误!");}//不为空,解析 key(offset + ":" + limit),:进行切割String[] params = key.split(":");if (params == null || params.length != 2) {throw new IllegalArgumentException("参数错误!");}int offset = Integer.valueOf(params[0]);int limit = Integer.valueOf(params[1]);// 二级缓存: Redis -> mysqllogger.debug("load post list from DB.");//调用discussPostMapper查询数据(userId = 0   orderMode = 1)return discussPostMapper.selectDiscussPosts(0, offset, limit, 1);}});// 初始化帖子总数缓存postRowsCache = Caffeine.newBuilder().maximumSize(maxSize).expireAfterWrite(expireSeconds, TimeUnit.SECONDS).build(new CacheLoader<Integer, Integer>() {@Nullable@Overridepublic Integer load(@NonNull Integer key) throws Exception {logger.debug("load post rows from DB.");return discussPostMapper.selectDiscussPostRows(key);}});}//声明一个业务方法:查询某一页的方法,返回类型是集合public List<DiscussPost> findDiscussPosts(int userId, int offset, int limit, int orderMode) {//缓存热门帖子(orderMode == 1)并且缓存首页(访问首页的时候,userId 是不传为0)// 缓存的是一页的数据(key 为 offset 和 limit 组合)if (userId == 0 && orderMode == 1) {return postListCache.get(offset + ":" + limit);}//否则访问数据库,在访问之前记录一下日志logger.debug("load post list from DB.");return discussPostMapper.selectDiscussPosts(userId, offset, limit, orderMode);}//查询行数的方法public int findDiscussPostRows(int userId) {if (userId == 0) {return postRowsCache.get(userId);}logger.debug("load post rows from DB.");return discussPostMapper.selectDiscussPostRows(userId);}

测试类:

package com.example.demo;import com.example.demo.entity.DiscussPost;
import com.example.demo.service.DiscussPostService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;import java.util.Date;@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = DemoApplication.class)
public class CaffeineTests {@Autowiredprivate DiscussPostService postService;@Testpublic void initDataForTest() {for (int i = 0; i < 300000; i++) {DiscussPost post = new DiscussPost();post.setUserId(111);post.setTitle("互联网求职暖春计划");post.setContent("今年的就业形势,确实不容乐观");post.setCreateTime(new Date());post.setScore(Math.random() * 2000);postService.addDiscussPost(post);}}@Testpublic void testCache() {System.out.println(postService.findDiscussPosts(0, 0, 10, 1));System.out.println(postService.findDiscussPosts(0, 0, 10, 1));System.out.println(postService.findDiscussPosts(0, 0, 10, 1));System.out.println(postService.findDiscussPosts(0, 0, 10, 0));}}

2.2 压力测试

使用 jmeter 工具测试:

未添加缓存:

添加缓存:

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

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

相关文章

软件工程造价师证书有用吗?难不难考?

&#x1f3af;软件工程造价师证书是有用的&#xff0c;它证明了持有人具备评估和估算软件开发cheng本、进度和资源规划的能力。✔️在IT行业中&#xff0c;受高度重视&#xff0c;特别是在软件开发和项目管理领域。 &#x1f469;软件工程造价师考试难易程度因人而异。该证书需…

影响代理IP稳定性的因素有哪些?

代理IP作为一种网络服务&#xff0c;在生活中扮演着各种各样的角色。它们可以用于保护隐私、突破访问限制、提高网络安全性等。代理IP的稳定性受到多种因素的影响&#xff0c;下面和大家探讨一下影响代理IP稳定性的因素。 1、网络环境&#xff1a;代理IP所处的网络环境对它的稳…

腾讯云新用户的定义与权益

腾讯云作为国内领先的云计算服务提供商&#xff0c;吸引了越来越多的用户。对于新用户来说&#xff0c;了解腾讯云的新用户定义和相关权益非常重要&#xff0c;因为它关系到用户能否享受到更多的优惠和服务。 一、腾讯云新用户的定义 腾讯云新用户是指首次注册腾讯云账号并且没…

【数据库】mysql事务

一、事务的基本概念 1、事务的定义 事务可由一条非常简单的SQL语句组成&#xff0c;也可以由一组复杂的SQL语句组成。。 在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。事务处理可以用来维护数据库的完整性&#xff0c;保证成批的 SQL 语句要么全部执行&…

Java技术专题:「入门到精通系列」深入探索常用的六种加密技术和实现

文章目录 1. 引言2. 对称加密3. 非对称加密4. 哈希算法5. 消息摘要6. 数字签名7. 数字证书8. 拓展功能与未来展望 &#x1f389;欢迎来到Java学习路线专栏~探索Java中的静态变量与实例变量 ☆* o(≧▽≦)o *☆嗨~我是IT陈寒&#x1f379;✨博客主页&#xff1a;IT陈寒的博客&am…

产品经理如何选择城市?

年底&#xff0c;全国性的人口大迁徙即将开始。选择城市&#xff0c;堪称年轻人的“二次投胎”&#xff0c;族望留原籍&#xff0c;家贫走他乡。 古人在选择城市时&#xff0c;主要的考量因素是家族势力&#xff0c;这一点放在当代&#xff0c;大致也成立&#xff0c;如果在老…

如何在CentOS安装SQL Server数据库并通过内网穿透工具实现公网访问

文章目录 前言1. 安装sql server2. 局域网测试连接3. 安装cpolar内网穿透4. 将sqlserver映射到公网5. 公网远程连接6.固定连接公网地址7.使用固定公网地址连接 前言 简单几步实现在Linux centos环境下安装部署sql server数据库&#xff0c;并结合cpolar内网穿透工具&#xff0…

C# 反射的终点:Type,MethodInfo,PropertyInfo,ParameterInfo,Summry

文章目录 前言反射是什么&#xff1f;常用类型操作SummryPropertyInfoMethodInfo无参函数运行 有参函数运行,获取paramterInfo 总结 前言 我之前写了一篇Attribute特性的介绍&#xff0c;成功拿到了Attribute的属性&#xff0c;但是如果把Attribute玩的溜&#xff0c;那就要彻…

什么是企业数字化转型?数字化的价值体现在哪里?

从2015年接触平安的数字化转型&#xff0c;到2021年承接阿里云的服务数字化项目&#xff0c;再到2023年主导大大小小10来个数字化项目&#xff0c;8年的时间&#xff0c;数字化对我而言已经从一个“新词”变成了一个“旧词”。 8年过去&#xff0c;数字化也从一道企业的“选做…

迎接人工智能的下一个时代:ChatGPT的技术实现原理、行业实践以及商业变现途径

课程背景 2023年&#xff0c;以ChatGPT为代表的接近人类水平的对话机器人&#xff0c;AIGC不断刷爆网络&#xff0c;其强大的内容生成能力给人们带来了巨大的震撼。学术界和产业界也都形成共识&#xff1a;AIGC绝非昙花一现&#xff0c;其底层技术和产业生态已经形成了新的格局…

[Vulnhub靶机] DriftingBlues: 2

[Vulnhub靶机] DriftingBlues: 2靶机渗透思路及方法&#xff08;个人分享&#xff09; 靶机下载地址&#xff1a; https://download.vulnhub.com/driftingblues/driftingblues2.ova 靶机地址&#xff1a;192.168.67.21 攻击机地址&#xff1a;192.168.67.3 一、信息收集 1.…

led手电筒照明线性恒流驱动芯片推荐:SM2123EGL双通道可调光

LED手电筒照明线性恒流驱动芯片是一种专门用于LED手电筒的照明系统的关键组件。它采用了线性恒流驱动技术&#xff0c;可以确保LED手电筒在不同电池电压和温度变化下&#xff0c;保持恒定的亮度输出&#xff0c;提高了LED手电筒的稳定性和可靠性。 LED手电筒照明线性恒流驱动芯…

VScode右键没有go to definition选项

1. 背景 1.1. 项目代码在远程服务器上&#xff1b; 1.2. win重装系统&#xff0c;重新安装vscode出现问题&#xff0c;没重装系统之前是没问题的&#xff1b; 2. 问题 打开vscode&#xff0c;通过ssh链接远程服务器中的项目代码后&#xff0c;选中函数右键没有go to defini…

大连理工大学软件学院2022年秋季学期《矩阵与数值分析》上机作业

文章目录 《计算机科学计算》第二版162页第12题&#xff08;1&#xff09;162页第16题216页第12题 《数值分析方法与应用》一、基础知识部分1、5、 二、线性方程组求解2、6、 三、非线性方程组求解1、4、 四、插值与逼近1、5、7、 五、数值积分2、 六、微分方程数值解法1、 《计…

机房自动化监控手把手分享给你 - 番外1:声光报警实现

本文章是一个机房自动化监控实际项目系列文章的番外篇&#xff0c;有个朋友问能否补充一个声光报警的实现&#xff0c;我仔细一想&#xff1a;虽然我不在这个项目中实现声光报警&#xff0c;但我在其他项目用过&#xff0c;使用的设备器件成本很低。那就以这个项目为背景&#…

视频转为序列图的软件,让视频批量转为序列图

你是否曾经遇到过这样的困境&#xff1a;需要将一段视频转为一系列的图片&#xff0c;但却没有合适的工具来完成&#xff1f;或许你曾经手动截图&#xff0c;或者用其他方式&#xff0c;但结果往往不尽如人意&#xff0c;图片质量差、色彩失真、画面不清晰。现在&#xff0c;让…

C语言动态内存管理

我们目前知道的开辟内存空间的方法有&#xff1a; 1.创建变量 2.创建数组&#xff1b; 但是这2种方法开辟的空间大小都是固定的&#xff0c;如果是数组的话确认了大小之后是无法改变的&#xff1b; int a10;//在栈区空间上开辟4个字节的空间&#xff1b;int arr[10];//在栈…

C++ 模板进阶

目录 一、非类型模板参数 二、模板的特化 1、函数模板特化 2、类模板特化 全特化 偏特化 3、例题 三、模板分离编译 1、定义 2、解决方法 3、模板总结 一、非类型模板参数 模板参数分类类型形参与非类型形参。 类型形参即&#xff1a;出现在模板参数列表中&#xf…

10本审稿及出版效率均较好的科普期刊参数分享!

医、药、护、技及医学工程等相关的人员&#xff0c;进行卫生高级职称评审时&#xff0c;需要在专业期刊上公开发表本专业学术论文&#xff0c;论文的方向、内容质量以及发表的刊物都至关重要。今天常笑医学给大家整理了10本审稿及出版效率均较好的科普期刊&#xff01;参数分享…

【开源GPT项目 - 在问】让知识无界,智能触手可及

Chatanywhere: chatAnywhere 在问 | 让知识无界&#xff0c;智能触手可及 项目简介 这是一个免费的在线聊天工具&#xff0c;旨在让用户更方便地享受科技带来的便利。用户可以使用我们的工具来获取答案、寻求建议、进行翻译和计算等等。这是由一位个人开发者创建的&#xff…