最新要闻
- 【天天速看料】一条视频带你回忆高三这一年 网友感动:祝愿每位考生忙而不茫
- 《暗黑破坏神4》正式发售:标准版终于能玩了
- 苹果Vision Pro头戴支持近视用户:需额外掏钱买插片-世界观焦点
- 牛骨头汤有营养吗 牛骨头汤是否有营养
- 苹果XR头显Vision Pro王炸登场:3499美元起售,2024年开卖-环球观天下
- 经常挖鼻孔会影响颜值 严重可诱发颅内感染
- 天天观热点:小米王腾分享苹果Vision Pro看法:令人赞叹、但普及需要时间
- QQ空间18周岁 第一代美女网红鼻祖露面:16年没变样
- 压力给到合资品牌 上汽大众永久关停第一工厂:部分产线搬迁|全球要闻
- 全国爱眼日,王贾桥小学倡导学子保护视力、珍爱光明
- 抄底!百度网盘超级会员年卡178元:送优酷+喜马拉雅 世界实时
- 防止别人蹭热点 苹果Vision Pro完全不提元宇宙三个字
- 16.99万起!零重力座椅、无框车门、三种动力选择 长安深蓝S7即将上市
- 制作成本16.5亿!《封神三部曲》第一部定档:7月20日上映 焦点速读
- 天天消息!孙俪代言!超能双离子洗衣粉骨折价大促:39.9元11斤
- 国内卫浴二线品牌有哪些_国内卫浴二线品牌-最新
广告
手机
iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- iphone11大小尺寸是多少?苹果iPhone11和iPhone13的区别是什么?
- 警方通报辅警执法直播中被撞飞:犯罪嫌疑人已投案
- 男子被关545天申国赔:获赔18万多 驳回精神抚慰金
- 3天内26名本土感染者,辽宁确诊人数已超安徽
- 广西柳州一男子因纠纷杀害三人后自首
- 洱海坠机4名机组人员被批准为烈士 数千干部群众悼念
家电
环球报道:花果山博客
- 前言
- 项目介绍
- 统一返回结果
- 登录功能实现
- 发布博客界面实现5.1 集成markdown编辑器5.2 创建Maven聚合工程5.2.1 遇到的坑点5.3 集成阿里云OSS实现图片上传
- 实现用户聊天功能6.1 数据库实现6.2 后端代码实现6.3 前端代码实现遇到的坑点
前言
这个博客只记录在开发过程中遇到的不会的知识点
简单介绍一个写这个博客的目的。因为之前学开发都是学完所需的知识点再去做项目,但是这时候在做项目的过程中发现以前学过的全忘了,所以为了减少这种情况,我打算以后通过项目学习技术,说的直接点就是,项目中需要用到哪些技术,那我就去学哪些技术,并用到此项目中。
项目介绍
这是一个SpringBoot + Vue 的分布式前后端项目,具体技术边学边用,因此等此项目完结了,这个项目介绍也就完结了。目前用到的技术:数据库:MySql前端: Vue, Element-ui, LocalStorge,后端:Spring, SpringMvc, Mybatis-Plus, SptingBoot, SpringSecurity其他:阿里云OSS, MavonEditor, WebSocket
统一返回结果
package com.monkey.monkeybackend.utils.result;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;@Data@NoArgsConstructor@AllArgsConstructorpublic class ResultVO { private int code; private String msg; private Object data;}
package com.monkey.monkeybackend.utils.result;public class ResultStatus { public static final int OK=10000; public static final int NO=10001; // 添加购物车失败}
登录功能我选则的是Jwt_Token实现,将生成的Token存到本地游览器LocalStorge中下面介绍Token实现的流程几个实现登录的工具类先引入依赖
org.springframework.boot spring-boot-starter-security 2.7.5 io.jsonwebtoken jjwt-api 0.11.5 io.jsonwebtoken jjwt-impl 0.11.5 runtime io.jsonwebtoken jjwt-jackson 0.11.5 runtime
1:检测Token是否过期工具类
package com.monkey.monkeybackend.config.SpringSecurity;import com.monkey.monkeybackend.Mapper.User.UserMapper;import com.monkey.monkeybackend.Pojo.user.User;import io.jsonwebtoken.Claims;import org.jetbrains.annotations.NotNull;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;import org.springframework.security.core.context.SecurityContextHolder;import org.springframework.stereotype.Component;import org.springframework.util.StringUtils;import org.springframework.web.filter.OncePerRequestFilter;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;@Componentpublic class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private UserMapper userMapper; @Override protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException { // 从哪里读取token String token = request.getHeader("Authorization"); // token以Bearer开头 if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) { filterChain.doFilter(request, response); return; } token = token.substring(7); String userid; try { Claims claims = JwtUtil.parseJWT(token); userid = claims.getSubject(); } catch (Exception e) { throw new RuntimeException(e); } User user = userMapper.selectById(Integer.parseInt(userid)); if (user == null) { throw new RuntimeException("用户名未登录"); } UserDetailsImpl loginUser = new UserDetailsImpl(user); UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request, response); }}
2: 生成Token类
package com.monkey.monkeybackend.config.SpringSecurity;import io.jsonwebtoken.Claims;import io.jsonwebtoken.JwtBuilder;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import org.springframework.stereotype.Component;import javax.crypto.SecretKey;import javax.crypto.spec.SecretKeySpec;import java.util.Base64;import java.util.Date;import java.util.UUID;@Componentpublic class JwtUtil { public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14; // 有效期14天 public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsfds121232131afasdfac"; public static String getUUID() { return UUID.randomUUID().toString().replaceAll("-", ""); } public static String createJWT(String subject) { JwtBuilder builder = getJwtBuilder(subject, null, getUUID()); return builder.compact(); } private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; SecretKey secretKey = generalKey(); long nowMillis = System.currentTimeMillis(); Date now = new Date(nowMillis); if (ttlMillis == null) { ttlMillis = JwtUtil.JWT_TTL; } long expMillis = nowMillis + ttlMillis; Date expDate = new Date(expMillis); return Jwts.builder() .setId(uuid) .setSubject(subject) .setIssuer("sg") .setIssuedAt(now) .signWith(signatureAlgorithm, secretKey) .setExpiration(expDate); } public static SecretKey generalKey() { byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256"); } public static Claims parseJWT(String jwt) throws Exception { SecretKey secretKey = generalKey(); return Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(jwt) .getBody(); }}
3:路径拦截器
package com.monkey.monkeybackend.config.SpringSecurity;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.http.HttpMethod;import org.springframework.security.authentication.AuthenticationManager;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.config.http.SessionCreationPolicy;import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration@EnableWebSecuritypublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/user/login", "/user/register", "/user/getUserInfoBytoken").permitAll() .antMatchers("/blog/article/getArticleContentByLabelId", "/blog/article/pagination", "/blog/article/fireRecently").permitAll() .antMatchers("/blog/label/getLabelList").permitAll() .antMatchers(HttpMethod.OPTIONS).permitAll() .anyRequest().authenticated(); http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); }}
4:得到用户信息工具类
package com.monkey.monkeybackend.config.SpringSecurity;import com.monkey.monkeybackend.Pojo.user.User;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import java.util.Collection;/* * 通过从数据库中查到的用户名和密码判断该用户是否合格 * */@Data@AllArgsConstructor@NoArgsConstructorpublic class UserDetailsImpl implements UserDetails { private User user; @Override public Collection extends GrantedAuthority> getAuthorities() { return null; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; }}
5:
package com.monkey.monkeybackend.config.SpringSecurity;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import com.monkey.monkeybackend.Mapper.User.UserMapper;import com.monkey.monkeybackend.Pojo.user.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.security.core.userdetails.UserDetails;import org.springframework.security.core.userdetails.UserDetailsService;import org.springframework.security.core.userdetails.UsernameNotFoundException;import org.springframework.stereotype.Service;@Servicepublic class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { QueryWrapper userQueryWrapper = new QueryWrapper<>(); userQueryWrapper.eq("username", username); User user = userMapper.selectOne(userQueryWrapper); if (user == null) { throw new RuntimeException("用户不存在"); } return new UserDetailsImpl(user); }}
用户注册实现
// 用户注册 @Override public ResultVO userRegister(Map userInfo) { String username = userInfo.get("username"); String password = userInfo.get("password"); String confirePassword = userInfo.get("confirePassword"); username = username.trim(); // 删除首位空白字符 if (username.length() == 0) { return new ResultVO(ResultStatus.NO, "用户名不能为空", null); } if (password == null || password.length() == 0) { return new ResultVO(ResultStatus.NO, "密码不能为空", null); } if (username.length() > 20) { return new ResultVO(ResultStatus.NO, "用户名长度不能大于20", null); } if (password.length() > 20) { return new ResultVO(ResultStatus.NO, "密码长度不能大于20", null); } if (!password.equals(confirePassword)) { return new ResultVO(ResultStatus.NO, "两次密码不一致", null); } QueryWrapper userQueryWrapper = new QueryWrapper<>(); userQueryWrapper.eq("username", username); Long selectCount = userMapper.selectCount(userQueryWrapper); if (selectCount > 0) { return new ResultVO(ResultStatus.NO, "该用户名已存在,请重新输入", null); } String encode = passwordEncoder.encode(password); String photo = "https://cdn.acwing.com/media/user/profile/photo/246711_md_08990849f1.png"; User user = new User(); user.setPassword(encode); user.setPhoto(photo); user.setUsername(username); user.setRegisterTime(new Date()); int insert = userMapper.insert(user); if (insert > 0) { return new ResultVO(ResultStatus.OK, "注册成功", null); } return new ResultVO(ResultStatus.OK, "注册失败", null); }
用户登录实现
// 用户登录 @Override public ResultVO userLogin(Map userInfo) { String username = userInfo.get("username"); String password = userInfo.get("password"); UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = // 将用户名与密码封装成一个加密之后的字符串 new UsernamePasswordAuthenticationToken(username, password); Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken); // 登录失败自动处理 UserDetailsImpl userDetails = (UserDetailsImpl) authenticate.getPrincipal(); User user = userDetails.getUser(); String token = JwtUtil.createJWT(user.getId().toString()); return new ResultVO(ResultStatus.OK, "登录成功", token); }
用户登录前端实现
LoginViews.vue
loginUser() { const vue = this; store.dispatch("login", { username: this.userInformation.username, password: this.userInformation.password, success() { store.dispatch("getUserInfoBytoken", { success() { vue.$modal.msgSuccess("登录成功"); router.push({ name: "home", }); }, error() { vue.$modal.msgError("登录失败"); } }) }, error(response) { vue.$modal.msgError(response.msg) } }) }
store.user.js
login(context, data) { $.ajax({ url: "http://localhost:4000/user/login", type: "post", data: { username: data.username, password: data.password }, success(response) { if (response.code == "10000") { localStorage.setItem("token", response.data); context.commit("updateToken", response.data); data.success(response); } else { data.error(response); } }, error(response) { data.error(response); } }) }, // 通过token得到用户信息 getUserInfoBytoken(context, data) { $.ajax({ url: "http://localhost:4000/user/getUserInfoBytoken", type: "get", headers: { Authorization: "Bearer " + context.state.token, }, success(response) { console.log(response) if (response.code == "10000") { context.commit("updateUserInfo", { ...response.data, is_login: true, }); if (data != null) data.success(response) } else { if (data != null) data.error(response); } }, errror(response) { data.error(response); } }) },
5. 发布博客界面实现
### 5.1 集成markdown编辑器1:安装mavonEditor包npm install mavon-editor --s
2:导入并使用mavonEditor在需要使用Markdown的Vue组件导入mavonEditorimport { mavonEditor } from "mavon-editor"import "mavon-editor/dist/css/index.css"
3:使用组件components: { mavonEditor },
4: 使用组件
5: 效果如下
5.2 创建maven聚合工程
坑点
1: 所有的maven聚合工程的父项目pom文件中的packing属性必需是pom, 子模块中的packing是jar类型的,不然在target中找不到application.properties配置类
1: 阿里云OSS的作用:
两个用户之间不能直接访问到对方电脑中的图片,文件,视频,以及前端默认不能访问本地图片,文件,视频,所以我们可以把这些信息都存到一个公共的地方存储,便于用户共同访问。
2: 阿里云OSS的使用
后端实现
1: 创建service-oss maven类型模块
2: 编写配置文件
#服务端口server.port=5000#服务名spring.application.name=service-oss#环境设置:dev、test、prodspring.profiles.active=dev#阿里云 OSS#不同的服务器,地址不同aliyun.oss.file.endpoint=oss-cn-beijing.aliyuncs.comaliyun.oss.file.keyid=你的keyid // 注意这几个字段前面后面都不能存在空格aliyun.oss.file.keysecret=你的keysecret#bucket可以在控制台创建,也可以使用java代码创建aliyun.oss.file.bucketname=monkey-blog
3: 编写启动类
4:增加一个工具类,读取配置文件中的内容
package com.monkey.monkeyoss.utils;import org.springframework.beans.factory.InitializingBean;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;// 常量属性读取的配置类@Componentpublic class ConstantPropertiesUtlis implements InitializingBean { // 读取配置文件内容 @Value("${aliyun.oss.file.endpoint}") private String endPoint; @Value("${aliyun.oss.file.keyid}") private String keyId; @Value("${aliyun.oss.file.keysecret}") private String keySecret; @Value("${aliyun.oss.file.bucketname}") private String bucketName; // 定义公开静态常量,方便外面的类调用 public static String END_POINT; public static String KEY_ID; public static String KER_SECRET; public static String BUCKET_NAME; /* 在初始化的时候执行这个方法 因为属性类型是private,所以当项目启动,Spring加载之后,执行接口的一个方法,读取字段值 */ @Override public void afterPropertiesSet() throws Exception { END_POINT = endPoint; KEY_ID = keyId; KER_SECRET = keySecret; BUCKET_NAME = bucketName; }}
Controller
package com.monkey.monkeyoss.controller;import com.monkey.monkeyUtils.result.ResultStatus;import com.monkey.monkeyUtils.result.ResultVO;import com.monkey.monkeyoss.service.OssService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import org.springframework.web.multipart.MultipartFile;@CrossOrigin@RestController@RequestMapping("/monkeyoss")public class OssController { @Autowired private OssService ossService; // 上传文章封面的方法 @PostMapping("/upload") public ResultVO uploadOssFile(@RequestParam("file") MultipartFile picture, @RequestParam("module") String module) { // 获取图片 // 返回上传到阿里云oss的路径 String url = ossService.uploadFile(picture, module); return new ResultVO(ResultStatus.OK, null, url); } // 删除文件的方法 @DeleteMapping("/remove") public ResultVO removeFile(@RequestParam("fileUrl") String fileUrl) { return ossService.removeFile(fileUrl); }}
ServiceImpl代码
package com.monkey.monkeyoss.service.impl;import com.aliyun.oss.ClientException;import com.aliyun.oss.OSS;import com.aliyun.oss.OSSClientBuilder;import com.aliyun.oss.OSSException;import com.aliyun.oss.model.PutObjectRequest;import com.aliyun.oss.model.PutObjectResult;import com.monkey.monkeyUtils.result.ResultStatus;import com.monkey.monkeyUtils.result.ResultVO;import com.monkey.monkeyoss.service.OssService;import com.monkey.monkeyoss.utils.ConstantPropertiesUtlis;import org.joda.time.DateTime;import org.springframework.stereotype.Service;import org.springframework.web.multipart.MultipartFile;import javax.print.DocFlavor;import java.io.IOException;import java.io.InputStream;import java.net.MalformedURLException;import java.net.URL;import java.util.UUID;@Servicepublic class OssServiceImpl implements OssService { // 上传文件到阿里云oss, 并返回阿里云图片存储 @Override public String uploadFile(MultipartFile file, String module) { // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。 String endpoint = ConstantPropertiesUtlis.END_POINT; // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。 String accessKeyId = ConstantPropertiesUtlis.KEY_ID; String accessKeySecret = ConstantPropertiesUtlis.KER_SECRET; // 填写Bucket名称,例如examplebucket。 String bucketName = ConstantPropertiesUtlis.BUCKET_NAME; // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。 String objectName = "exampledir/exampleobject.txt"; // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。 // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。 String filePath= "D:\\localpath\\examplefile.txt"; // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); try { // 获取上传文件的输入流 InputStream inputStream = file.getInputStream(); // 获取文件名称 String filename = file.getOriginalFilename(); String name = file.getName(); // 保证文件名不重复 String uuid = UUID.randomUUID().toString().replaceAll("-", ""); // 1: 通过 uuid生成随机值 filename = uuid + filename; // 2: 通过日期生成路径 // 获取当前日期 String dataPath = new DateTime().toString("yyyy/MM/dd"); filename = module + dataPath + "/" + filename; // 创建PutObjectRequest对象。 /* * 第一个参数: Bucket名称 * 第二个参数:上传到OSS文件的路径或文件名称 * 第三个参数:上传文件的输入流 * */ PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, filename, inputStream); // 创建PutObject请求。 PutObjectResult result = ossClient.putObject(putObjectRequest); // 返回上传到阿里云OSS文件的路径// https://monkey-blog.oss-cn-beijing.aliyuncs.com/article/relax.jpg String fileUrl = " https://" + bucketName + "." + endpoint + "/" + filename; return fileUrl; } catch (OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason."); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); } catch (ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network."); System.out.println("Error Message:" + ce.getMessage()); } catch (IOException e) { throw new RuntimeException(e); } finally { if (ossClient != null) { ossClient.shutdown(); } } return null; } // 删除阿里云文件 @Override public ResultVO removeFile(String fileUrl) { System.err.println(fileUrl); // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。 String endpoint = ConstantPropertiesUtlis.END_POINT; // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。 String accessKeyId = ConstantPropertiesUtlis.KEY_ID; String accessKeySecret = ConstantPropertiesUtlis.KER_SECRET; // 填写Bucket名称,例如examplebucket。 String bucketName = ConstantPropertiesUtlis.BUCKET_NAME; // 填写Object完整路径,例如exampledir/exampleobject.txt。Object完整路径中不能包含Bucket名称。 String objectName = fileUrl; try { // 原图片地址https://monkey-blog.oss-cn-beijing.aliyuncs.com/articlePicture/2023/05/29/c873d24883b44e6ab47b22eb92eaef0d04.png // 需要图片地址articlePicture/2023/05/29/02bb26a36e004b19b7a57f5a348f312e03.jpg objectName = new URL(objectName).getPath().substring(1); objectName = objectName.substring(1); } catch (MalformedURLException e) { throw new RuntimeException(e); } // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); try { // 删除文件。 ossClient.deleteObject(bucketName, objectName); return new ResultVO(ResultStatus.OK, null, null); } catch (OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason."); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); return new ResultVO(ResultStatus.NO, null, null); } catch (ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network."); System.out.println("Error Message:" + ce.getMessage()); return new ResultVO(ResultStatus.NO, null, null); } finally { if (ossClient != null) { ossClient.shutdown(); } } }}
前端代码
Method方法
// 删除阿里云的文件 onUploadRemove(file) { const vue = this; console.log(vue) console.log(store.state.user.token); $.ajax({ url: "http://localhost:5000/monkeyoss/remove", type: "delete", headers: { Authorization: "Bearer " + store.state.user.token }, data: { fileUrl: file.response.data }, success(response) { if (response.code == "10000") { vue.$modal.msgSuccess("删除后图片成功"); vue.ruleForm.photo = ""; } else { vue.$modal.msgError("删除图片失败"); } }, error() { vue.$modal.msgError("删除图片失败") } }) }, // 上传成功之后判断上传的图片是否成功 onUploadSuccess(response) { if (response.code == "10000") { this.$modal.msgSuccess("上传成功"); this.ruleForm.photo = response.data; // 得到阿里云中的地址 } else { this.$modal.msgError("上传失败"); } }, beforeAvatarUpload(file) { const isJPG = file.type === "image/jpeg"; const isPng = file.type === "image/png" const isLt2M = file.size / 1024 / 1024 < 2; if (!isJPG && !isPng) { this.$message.error("上传头像图片只能是 JPG/PNG 格式!"); } if (!isLt2M) { this.$message.error("上传头像图片大小不能超过 2MB!"); } return isJPG && isLt2M; }, submitForm(formName) { this.$refs[formName].validate((valid) => { if (valid) { alert("submit!"); } else { console.log("error submit!!"); return false; } }); }, resetForm(formName) { this.$refs[formName].resetFields(); } } }
## 遇到的 坑点1: componenet注解会默认覆盖SpringBoot注解。2: 扫描的mapperScan范围不能包括@Service注解,不然会导致mybatis-plus不能执行BaseMapper中的Sql语句
最终效果
# 数据库后端代码
1:VO类
package com.monkey.monkeyblog.pojo.Vo;import com.fasterxml.jackson.annotation.JsonFormat;import lombok.AllArgsConstructor;import lombok.Data;import lombok.NoArgsConstructor;import java.util.Date;// 聊天用户信息列表@Data@AllArgsConstructor@NoArgsConstructorpublic class UserChatVo { private Long id; private Long senderId; // 回复人id private String senderPhoto; // 聊天人头像 private String senderName; // 聊天人姓名 private String senderBrief; // 聊天人简介 private String receiverName; // 回复人姓名 private Long receiverId; // 回复者id private String receiverPhoto; // 回复人头像 private String receiverBrief; // 回复人简介 private String lastContent; // 最后聊天内容 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") private Date LastCreateTime; // 最后聊天时间 private Long isLike; // 当前用户是否关注回复人(0表示未关注,1表示已关注) private String direction; // 聊天人的方向 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") private Date createTime; // 每条记录聊天时间 private String content; // 每条聊天内容}
后端逻辑类
package com.monkey.monkeyblog.service.Impl.chat;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import com.monkey.monkeyUtils.result.ResultStatus;import com.monkey.monkeyUtils.result.ResultVO;import com.monkey.monkeyblog.mapper.ChatHistoryMapper;import com.monkey.monkeyblog.mapper.user.UserFansMapper;import com.monkey.monkeyblog.pojo.ChatHistory;import com.monkey.monkeyblog.pojo.Vo.UserChatVo;import com.monkey.monkeyblog.pojo.user.UserFans;import com.monkey.monkeyblog.service.chat.WebSocketChatService;import com.monkey.spring_security.mapper.user.UserMapper;import com.monkey.spring_security.pojo.user.User;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.*;@Servicepublic class WebSocketChatServiceImpl implements WebSocketChatService { @Autowired private ChatHistoryMapper chatHistoryMapper; @Autowired private UserMapper userMapper; @Autowired private UserFansMapper userFansMapper; //通过当前登录用户登录id得到该用户聊天信息列表(左边) @Override public ResultVO getReplyUserListByUserId(Map data) { long userId = Long.parseLong(data.get("userId")); QueryWrapper chatHistoryQueryWrapper = new QueryWrapper<>(); chatHistoryQueryWrapper.eq("sender_id", userId).or().eq("receiver_id", userId); chatHistoryQueryWrapper.orderByDesc("create_time"); List chatHistoryList = chatHistoryMapper.selectList(chatHistoryQueryWrapper); // 1: 将sender_id, receiver_id交换排序去重后再按时间降序取出最晚消息发出的时间 // 1.1 将 sender_id, receiver_id按sender_id在前,receiver_id在后 Map isSwap = new HashMap<>(); // 判断(sender_id, receiver_id)是否交换过 Long len = Long.parseLong(String.valueOf(chatHistoryList.size())); for (long i = 0; i < len; i ++ ) { ChatHistory chatHistory = chatHistoryList.get((int)i); Long senderId = chatHistory.getSenderId(); Long receiverId = chatHistory.getReceiverId(); if (senderId > receiverId) { chatHistory.setSenderId(receiverId); chatHistory.setReceiverId(senderId); chatHistoryList.set((int) i, chatHistory); isSwap.put(i, true); } } // 找出时间最晚的一条记录,将多余的数据去除. List chatHistories = new ArrayList<>(); Map, Boolean> resList = new HashMap<>(); for (int i = 0; i < len; i ++ ) { ChatHistory chatHistory = chatHistoryList.get(i); Long receiverId = chatHistory.getReceiverId(); Long senderId = chatHistory.getSenderId(); Map.Entry key1 = new AbstractMap.SimpleEntry<>(senderId, receiverId); if (resList.get(key1) == null || !resList.get(key1)) { // 判断之前是否交换过sender_id, receiver_id if (isSwap.get(i) != null && isSwap.get(i)) { chatHistories.set(i, chatHistory); } chatHistories.add(chatHistory); resList.put(key1, true); } } List userChatVoList = new ArrayList<>(); // 若当前用户没有向该作者发送过消息,则在数据库填入一个空字段以便在列表左边显示 long statrReceiverId = Long.parseLong( data.get("receiverId")); QueryWrapper chatHistoryQueryWrapper1 = new QueryWrapper<>(); chatHistoryQueryWrapper1.eq("sender_id", userId).eq("receiver_id", statrReceiverId) .or().eq("receiver_id", userId).eq("sender_id", statrReceiverId); Long count = chatHistoryMapper.selectCount(chatHistoryQueryWrapper1); if (count <= 0) { ChatHistory chatHistory = new ChatHistory(); chatHistory.setSenderId(userId); chatHistory.setReceiverId(statrReceiverId); chatHistory.setCreateTime(new Date()); chatHistory.setContent("快开始聊天吧。"); chatHistoryMapper.insert(chatHistory); ChatHistory chatHistory1 = chatHistoryMapper.selectById(chatHistory.getId()); UserChatVo userChatVo = new UserChatVo(); Long senderId = chatHistory1.getSenderId(); Long receiverId = chatHistory1.getReceiverId(); userChatVo.setId(chatHistory1.getId()); userChatVo.setLastCreateTime(chatHistory1.getCreateTime()); userChatVo.setLastContent(chatHistory1.getContent()); // 通过发送者id得到发送者信息 User sendUser = userMapper.selectById(senderId); userChatVo.setSenderId(senderId); userChatVo.setSenderName(sendUser.getUsername()); userChatVo.setSenderPhoto(sendUser.getPhoto()); userChatVo.setSenderBrief(sendUser.getBrief()); // 通过接收者id得到接收者信息 User receiverUser = userMapper.selectById(receiverId); userChatVo.setReceiverName(receiverUser.getUsername()); userChatVo.setReceiverPhoto(receiverUser.getPhoto()); userChatVo.setReceiverBrief(receiverUser.getBrief()); userChatVo.setReceiverId(receiverUser.getId()); // 判断发送者是否关注了接收者 QueryWrapper userFansQueryWrapper = new QueryWrapper<>(); userFansQueryWrapper.eq("fans_id", senderId); userFansQueryWrapper.eq("user_id", receiverId); Long selectCount = userFansMapper.selectCount(userFansQueryWrapper); userChatVo.setIsLike(selectCount); userChatVoList.add(userChatVo); } for (int i = 0; i < chatHistories.size(); i ++ ) { UserChatVo userChatVo = new UserChatVo(); ChatHistory chatHistory = chatHistories.get(i); Long senderId = chatHistory.getSenderId(); Long receiverId = chatHistory.getReceiverId(); userChatVo.setId(chatHistory.getId()); userChatVo.setLastCreateTime(chatHistory.getCreateTime()); userChatVo.setLastContent(chatHistory.getContent()); // 通过发送者id得到发送者信息 User sendUser = userMapper.selectById(senderId); userChatVo.setSenderId(senderId); userChatVo.setSenderName(sendUser.getUsername()); userChatVo.setSenderPhoto(sendUser.getPhoto()); userChatVo.setSenderBrief(sendUser.getBrief()); // 通过接收者id得到接收者信息 User receiverUser = userMapper.selectById(receiverId); userChatVo.setReceiverName(receiverUser.getUsername()); userChatVo.setReceiverPhoto(receiverUser.getPhoto()); userChatVo.setReceiverBrief(receiverUser.getBrief()); userChatVo.setReceiverId(receiverUser.getId()); // 判断发送者是否关注了接收者 QueryWrapper userFansQueryWrapper = new QueryWrapper<>(); userFansQueryWrapper.eq("fans_id", senderId); userFansQueryWrapper.eq("user_id", receiverId); Long selectCount = userFansMapper.selectCount(userFansQueryWrapper); userChatVo.setIsLike(selectCount); userChatVoList.add(userChatVo); } return new ResultVO(ResultStatus.OK, null, userChatVoList); } // 得到聊天对话框信息(右边) @Override public ResultVO showChatInformation(Map data) { long senderId = Long.parseLong(data.get("senderId")); long receiverId = Long.parseLong(data.get("receiverId")); QueryWrapper chatHistoryQueryWrapper = new QueryWrapper<>(); chatHistoryQueryWrapper.orderByAsc("create_time"); // 得到双方聊天记录 chatHistoryQueryWrapper.eq("sender_id", senderId).eq("receiver_id", receiverId) .or().eq("receiver_id", senderId).eq("sender_id", receiverId); List chatHistoryList = chatHistoryMapper.selectList(chatHistoryQueryWrapper); List userChatVoList = new ArrayList<>(); for (ChatHistory chatHistory : chatHistoryList) { UserChatVo userChatVo = new UserChatVo(); userChatVo.setId(chatHistory.getId()); Long receiverId1 = chatHistory.getReceiverId(); Long senderId1 = chatHistory.getSenderId(); // 通过senderId, receiverId得到对应信息 User senderUser = userMapper.selectById(senderId1); userChatVo.setSenderBrief(senderUser.getBrief()); userChatVo.setSenderPhoto(senderUser.getPhoto()); userChatVo.setSenderId(senderUser.getId()); userChatVo.setSenderName(senderUser.getUsername()); User receiverUser = userMapper.selectById(receiverId1); userChatVo.setReceiverId(receiverUser.getId()); userChatVo.setReceiverName(receiverUser.getUsername()); userChatVo.setReceiverBrief(receiverUser.getBrief()); userChatVo.setReceiverPhoto(receiverUser.getPhoto()); userChatVo.setContent(chatHistory.getContent()); userChatVo.setCreateTime(chatHistory.getCreateTime()); userChatVoList.add(userChatVo); } // 因为websocket一次发送的消息有允许发送的消息有大小限制所以每次确定最多发送十条消息 if (userChatVoList.size() < 10) { return new ResultVO(ResultStatus.OK, null, userChatVoList); } else { List res = new ArrayList<>(); for (int i = userChatVoList.size() - 10; i < userChatVoList.size(); i ++ ) { res.add(userChatVoList.get(i)); } return new ResultVO(ResultStatus.OK, null, res); } }}
后端webSocket类
package com.monkey.monkeyblog.service.Impl.chat;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONArray;import com.alibaba.fastjson.JSONObject;import com.monkey.monkeyblog.mapper.ChatHistoryMapper;import com.monkey.monkeyblog.pojo.ChatHistory;import com.monkey.monkeyblog.pojo.Vo.UserChatVo;import com.monkey.spring_security.JwtUtil;import com.monkey.spring_security.mapper.user.UserMapper;import com.monkey.spring_security.pojo.user.User;import io.jsonwebtoken.Claims;import org.springframework.beans.BeanUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Component;import javax.websocket.*;import javax.websocket.server.PathParam;import javax.websocket.server.ServerEndpoint;import java.io.IOException;import java.util.ArrayList;import java.util.Date;import java.util.List;import java.util.concurrent.ConcurrentHashMap;@Component@ServerEndpoint("/websocket/chat/{token}") // 注意不要以"/"结尾public class WebSocketChatServer { // 将用户id 映射到每个websocket实例,以便通过用户id找到其对应的websocket // static 所有实例访问同一个哈希表 public static ConcurrentHashMap userList = new ConcurrentHashMap<>(); // 用户从后端向前端发送消息 private Session session = null; private User user; private static UserMapper userMapper; private static ChatHistoryMapper chatHistoryMapper; @Autowired public void setChatHistoryMapper(ChatHistoryMapper chatHistoryMapper) { WebSocketChatServer.chatHistoryMapper = chatHistoryMapper; } @Autowired public void setUserMapper(UserMapper userMapper) { WebSocketChatServer.userMapper = userMapper; } @OnOpen public void onOpen(Session session, @PathParam("token") String token) { // 建立连接 System.err.println("chat Connect !! "); this.session = session; Long senderId = this.getUserIdBytoken(token); this.user = userMapper.selectById(senderId); if (this.user != null) { userList.put(senderId, this); } else { try { this.session.close(); } catch (IOException e) { throw new RuntimeException(e); } } System.err.println(this.user); } @OnMessage public void onMessage(String message, Session session) { // 从Client接收消息 System.err.println("chat messsage!!"); JSONObject data = JSONObject.parseObject(message); String event = (String)data.get("event"); if ("start_chat".equals(event)) { JSONArray messageArray = JSONArray.parseArray(data.getString("message")); List userChatVoList = this.getListByJSON(messageArray, UserChatVo.class); // 判断聊天者方向 List chatVoList = this.judgeDirection(userChatVoList); JSONObject jsonObject = new JSONObject(); jsonObject.put("event", event); jsonObject.put("messageList", chatVoList); this.sendMessage(jsonObject.toJSONString()); } else if ("send_message".equals(event)) { String dataString = data.getString("message"); Long receiverId = data.getLong("receiverId"); // 向发送者和接收者传递消息 this.sendMessageDeleiverMessage(dataString, receiverId); } } // 通过接收者id发送消息给接收者 private void sendMessageDeleiverMessage(String dataString, Long receiverId) { // 将该消息加入数据库 ChatHistory chatHistory = new ChatHistory(); chatHistory.setContent(dataString); chatHistory.setCreateTime(new Date()); chatHistory.setReceiverId(receiverId); chatHistory.setSenderId(user.getId()); chatHistoryMapper.insert(chatHistory); ChatHistory history = chatHistoryMapper.selectById(chatHistory.getId()); // 将消息返回前端 JSONObject jsonObjectReceiver = new JSONObject(); jsonObjectReceiver.put("event", "receive_message"); User receiver = userMapper.selectById(receiverId); UserChatVo userChatVoReceiver = new UserChatVo(); userChatVoReceiver.setContent(dataString); userChatVoReceiver.setCreateTime(history.getCreateTime()); userChatVoReceiver.setDirection("左"); userChatVoReceiver.setReceiverName(receiver.getUsername()); userChatVoReceiver.setReceiverPhoto(receiver.getPhoto()); jsonObjectReceiver.put("information", userChatVoReceiver); WebSocketChatServer webSocketChatServer = userList.get(receiverId); System.err.println(webSocketChatServer); if (webSocketChatServer != null) { webSocketChatServer.sendMessage(jsonObjectReceiver.toJSONString()); } JSONObject jsonObjectSender = new JSONObject(); jsonObjectSender.put("event", "send_message"); UserChatVo userChatVoSender = new UserChatVo(); userChatVoSender.setSenderName(this.user.getUsername()); userChatVoSender.setSenderPhoto(this.user.getPhoto()); userChatVoSender.setCreateTime(history.getCreateTime()); userChatVoSender.setDirection("右"); userChatVoSender.setContent(dataString); jsonObjectSender.put("information", userChatVoSender); userList.get(this.user.getId()).sendMessage(jsonObjectSender.toJSONString()); } @OnClose public void onClose() { // 关闭链接 System.err.println("chat Close !! "); // 关闭连接时删除该用户 if (this.user != null) { userList.remove(this.user.getId()); } } @OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } // 后端像前端发送单个信息 public void sendMessage(String message) { System.err.println("返回消息"); try { this.session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); } } // 通过token得到当前用户id public Long getUserIdBytoken(String token) { Long userId = -1L; try { Claims claims = JwtUtil.parseJWT(token); userId = Long.parseLong(claims.getSubject()); } catch (Exception e) { throw new RuntimeException(e); } return userId; } // 将JSON集合转化成JAVA中的实体类 public List getListByJSON(JSONArray jsonArray, Class clazz) { List list = new ArrayList<>(); for (int i = 0; i < jsonArray.size(); i++) { JSONObject jsonObject = jsonArray.getJSONObject(i); T object = JSONObject.toJavaObject(jsonObject, clazz); list.add(object); } return list; } private List judgeDirection(List userChatVoList) { for (UserChatVo userChat : userChatVoList) { Long senderId = userChat.getSenderId(); Long receiverId = userChat.getReceiverId(); if (senderId.equals(this.user.getId())) { userChat.setDirection("右"); } else if (receiverId.equals(this.user.getId())) { userChat.setDirection("左"); } } return userChatVoList; }}
前端代码
{{ $store.state.user.username }} {{ charUserInformation.receiverName }} 未关注 已关注 {{ charUserInformation.lastCreateTime | formatDate }} {{ charUserInformation.lastContent }} {{ charUserInformation.senderName }} 未关注 已关注 {{ charUserInformation.lastCreateTime | formatDate }} {{ charUserInformation.lastContent }} {{ $store.state.user.username }}
{{ message.createTime }} {{ message.senderName }} {{ message.content }} {{ message.senderName }} {{ message.createTime }} {{ message.content }} 请选择需要聊天的用户 <script>import $ from "jquery"import store from "@/store";export default { name: "WebSocketChat", data() { return { // 是否点击了左边框 isChoice: false, //聊天用户信息 chatUserInformationList: [], // 右边框展示信息 showInformation: { receiverName: "", receiverBrief: "", }, socketUrl: `ws://localhost:4000/websocket/chat/${store.state.user.token}`, // 聊天消息 messageList: {}, socket: null, // 是否选中该行 isSelected: null, // 聊天框发送消息 message: "", // 接收人id receiverId: "", //消息种类,sendMessage表示发送消息. receiveMessage 表示接收消息 messageKind: "", } }, // 每次点击人之后自动跳到页面最底部 updated(){ let scrollContainer = document.querySelector(".chatBox") scrollContainer.scrollTop = scrollContainer.scrollHeight }, filters: { formatDate: value => { if (!value) return ""; // 转换成 Date 对象 const date = new Date(value); // 格式化输出 const year = date.getFullYear(); const month = ("0" + (date.getMonth() + 1)).slice(-2); const day = ("0" + date.getDate()).slice(-2); return `${year}-${month}-${day}`; } }, created() { this.initWebSocket(); }, mounted() { setTimeout(() => { this.getReplyUserListByUserId(store.state.user.id); }, 100) }, unmounted() { this.socket.close(); }, methods: { handleKeyDown(e) { if (e.keyCode === 13 && !e.ctrlKey) { // Enter,发送消息 this.sendMessages(this.message, this.receiverId); e.preventDefault(); } else if (e.keyCode === 13 && e.ctrlKey) { // Ctrl+Enter,换行 this.message += "\n"; } }, // 发送聊天消息 sendMessages(message, receiverId) { // 发送消息的逻辑 this.socket.send(JSON.stringify({ event: "send_message", message, receiverId })); this.message = ""; }, initWebSocket() { // 创建WebSocket this.socket = new WebSocket(this.socketUrl); this.socket.onopen = () => { console.log("chat connect !!"); }; this.socket.onmessage = (message) => { console.log("chat onmessage!!"); let data = JSON.parse(message.data); console.log(data) if (data.event == "start_chat") { this.messageList = data.messageList; } else if (data.event == "send_message") { if (data.information != null) this.messageList.push(data.information) this.getReplyUserListByUserId(store.state.user.id); } else if (data.event == "receive_message") { if (data.information != null) this.messageList.push(data.information) this.getReplyUserListByUserId(store.state.user.id); } }, this.socket.onclose = () => { console.log("chat onclose !! "); } }, // 展示所选行 showRow(index) { this.isSelected = index; }, // 得到聊天对话框信息 showChatInformation(receiverId, senderId, index) { this.isChoice = true; const vue = this; this.isSelected = index this.receiverId = receiverId; $.ajax({ url: "http://localhost:4000/webSocketChat/showChatInformation", type: "get", headers: { Authorization: "Bearer " + store.state.user.token }, data: { senderId, receiverId, }, success(response) { if (response.code == "10000") { vue.socket.send(JSON.stringify({ event: "start_chat", message: response.data })); } else { vue.$modal.msgError("发生未知错误,加载信息失败"); } }, error() { vue.$modal.msgError("认证失败,无法访问系统资源"); } }) }, // 通过当前登录用户登录id得到该用户聊天列表 getReplyUserListByUserId(userId) { const vue = this; $.ajax({ url: "http://localhost:4000/webSocketChat/getReplyUserListByUserId", type: "get", data: { userId }, headers: { Authorization: "Bearer " + store.state.user.token }, success(response) { if (response.code == "10000") { vue.chatUserInformationList = response.data; } else { vue.$modal.msgError("发生未知错误") } }, error() { vue.$modal.msgError("认证失败,无法访问系统资源"); } }) } }}</script>
关键词:
饶派杯 XCTF 车联网挑战赛 mqttsvr 复现|当前观点
【天天速看料】一条视频带你回忆高三这一年 网友感动:祝愿每位考生忙而不茫
《暗黑破坏神4》正式发售:标准版终于能玩了
苹果Vision Pro头戴支持近视用户:需额外掏钱买插片-世界观焦点
牛骨头汤有营养吗 牛骨头汤是否有营养
读改变未来的九大算法笔记05_数字签名
Git hooks与自动化部署-全球新消息
每日聚焦:直播平台源码画面质量功能的实现
云图说|ModelArts开发环境,让AI开发、探索、教学更简单 世界新要闻
苹果XR头显Vision Pro王炸登场:3499美元起售,2024年开卖-环球观天下
经常挖鼻孔会影响颜值 严重可诱发颅内感染
天天观热点:小米王腾分享苹果Vision Pro看法:令人赞叹、但普及需要时间
QQ空间18周岁 第一代美女网红鼻祖露面:16年没变样
压力给到合资品牌 上汽大众永久关停第一工厂:部分产线搬迁|全球要闻
全国爱眼日,王贾桥小学倡导学子保护视力、珍爱光明
linux tar解压命令总结
速讯:848中国鱼类资料图谱大全ACCESS\EXCEL数据库
顶象无感验证码助力京客隆提升数字化运营能力
MySQL用户与权限管理_每日热门
全球快播:电池级碳酸锂价格重回30万元/吨 锂盐厂商二季度业绩可期
抄底!百度网盘超级会员年卡178元:送优酷+喜马拉雅 世界实时
防止别人蹭热点 苹果Vision Pro完全不提元宇宙三个字
16.99万起!零重力座椅、无框车门、三种动力选择 长安深蓝S7即将上市
制作成本16.5亿!《封神三部曲》第一部定档:7月20日上映 焦点速读
天天消息!孙俪代言!超能双离子洗衣粉骨折价大促:39.9元11斤
国内卫浴二线品牌有哪些_国内卫浴二线品牌-最新
低利率环境与发行优势助推 商业银行绿色金融债发行火热
抽检合格率97.26%!江西加强中高考期间校园食品安全监管
阿根廷中国行 花30万可让梅西敬酒、合影?官方回应
续航2小时、仅售2万5!苹果首款AR眼镜开卖:还有半年时间攒钱
天天观天下!《暗黑4》德鲁伊玩家发声:不要随便攻击熊熊!
移动SSD也白菜价了!梵想PS2000 1TB只要279元 不到小米一半
天天日报丨天猫京东红包加码!最高23888元 每天最多领3次
阿根廷中国行 花30万可让梅西敬酒、合影?官方回应
在 Linux 中使用 sFTP 上传或下载文件与文件夹
日本工人的实际工资在4月份继续下降,尽管已经反映了在年度薪资谈判中所取得的加薪,这给考虑召集选举的首相岸田文雄带来挑战|天天时讯
环球速递!76核核显GPU足以毁天灭地 苹果Mac Pro不支持任何独显
今亮点!AMD确认锐龙8000明年问世:Zen5 CPU、亮机核显大升级
iPhone不用再说“嘿 Siri”了!可能不好用:用户担忧误唤醒_每日快讯
烤串巨无霸!广西烧烤竹签长1米8:网友直呼开眼 一串吃饱|热点评
精彩看点:李宁获摩根大通增持1117.05万股 最新持股比例为5.07%
全国中成药集采分组等多项信息公布 96家药企参与申报 多个品种竞争激烈
国际金融市场早知道:6月6日
取代iPhone?苹果发布首款MR头显Vision Pro:卖到2.5万天价
资讯:实用性看齐安卓?这就是iOS/iPad OS 17 苹果已推送测试版:iPhone 8、X被弃
天天简讯:《小美人鱼》全球票房突破3亿美元 黑美人鱼欢呼:大家还是爱看我的
独立在轨33天 状态良好:天舟五号与空间站组合体再次对接 时讯
7月14日上映!《碟中谍7》发布新剧照:59岁阿汤哥骑摩托冲下悬崖 头条焦点
龙岗生态司法修复基地揭牌 天天动态
24800元的苹果Vision Pro发布 一文看懂WWDC23有多硬!
母亲病危家人让高考孩子见最后一面 网友争议:高考高于亲情?|天天微速讯
天弘中证沪港深线上消费主题交易型开放式指数证券投资基金基金产品资料概要(更新)
环球热点!SD协议-时序
JAVA学习笔记_基础篇01
基于 log4j2 插件实现统一日志脱敏,性能远超正则替换-焦点信息
焦点观察:MRST绘制三维网格图
每日焦点!生态河湖 诗画江城
2.4万元!苹果首款MR头显Vision Pro发布:单眼像素超越4K电视
真颠覆XR!苹果Vision Pro技术、体验几乎完美 但是一根线毁所有|世界快报
苹果Mac三大新品国行价格汇总:巅峰顶配9.7万元-环球速看
8450米的珠峰上舍命救人 女子承诺1万美元只肯给4000美元:贵吗?
1100万年后 决定性别的Y染色体将消失:人类会怎么样?
旅游景区清洁工人工作情况影像记录带定位时间时间水印相机
全球动态:父母去世了,为子女买的保险能用遗产续费吗?
天天讯息:高中教材选修和必修有什么区别_高中必修是什么意思 选修呢
油毡纸多少钱一卷(油毡纸) 环球播资讯
当前短讯!不负如来不负卿txt下载完整版_不负如来不负卿txt下载
UFOU首获投资,升降桌售价上万元有人望而却步,孟振亮曾连续创业
每日短讯:管理能力有哪些方面_能力有哪些方面
阿里云宣布举办首届AI黑客马拉松挑战赛 最终获胜团队将获得3万奖金
现实题材电影《八角笼中》发布一“骗”真心版预告 由王宝强与肖央主演
人造电子皮肤首次亮相 每层主干均为长分子链组成
美国一男子连吃100天麦当劳后减重53斤 网友困惑垃圾食品是否需重新定义
巴奴火锅反向推出“天价土豆”套餐 套餐几乎囊括巴奴所有热门菜品
日本地下偶像女子团体成员灌男粉丝喝洗洁精 看其吐泡泡后捧腹大笑
在线访问量直线飙升 OpenAI已跻身全球20家流量最高网站榜单
2023年夏日游戏节发布预热宣传片 将于6月9日进行直播
比亚迪上调部分车型的保养费 幅度达到50%左右
清华大学成立秀钟书院:今年开始招生 目标宏大_当前热文
语音系统用上全自研算法!李想:95%以上控制张嘴就能搞定-天天亮点
浙江大学学生被录用为“场务及驱鸟员” 网友惊呼太卷了
创美药业(02289)将于7月31日派发末期股息每股0.45元
hackthebox sniper medium
致乘风破浪的青春|胡千行:求是创新,多维发展-消息
网上出现假冒北京市人社局的网站!官方发布风险提示_头条焦点
警惕!身上有这7个特征的人 容易得肺癌
男子嘴贴保鲜膜忍不住亲亲宝宝引热议 医生科普:被老人、家人亲吻会遭殃
太卷了!浙大学霸应聘机场驱鸟员:要求英语四级425分 环球微头条
文心一言 VS 讯飞星火 VS chatgpt (32)-- 算法导论5.2 4题_全球看点
环球微动态丨【解决方法】网络设备使用CLI命令行模式进入Telnet登录,如交换机,路由器
厦门大学有哪些专业 厦门大学专业排名
天天热头条丨信泰航空拟对全资子公司信泰克增资1000万
5月国内汽车召回数量排名:美系特斯拉遥遥领先 全球滚动
珠峰为救遇险女子放弃登顶当事人发声:停止网暴_环球视讯
OpenAI跻身全球前20大网站:月活用户近9亿
购物追剧两不误!腾讯视频会员年卡+京东PLUS年卡降价:到手138元
排海计划坚持不变!福岛核电站鱼体放射性物质超标180倍 环球头条
当前动态:2023年南疆冬小麦成熟期及复播玉米播种期预报金十期货6月5日讯,根据目前南疆冬小麦发育进程和6月天气气候趋势预测,预计2023年南疆冬小麦成熟期大部
全国一体化算力算网调度平台正式发布
比iPhone 14更薄的机身塞进4600mAh!vivo S17本周开卖:2499起