一.可以使用的技术

  • 开发环境

    • idea,node.js
  • 技术
    • 虚拟机
    • Linux,docker
    • mysql,redis
    • jdbc,jsp,servlet
    • mybatis,mybatis-plus
    • springboot,springMVC,spring
    • tomcat8
    • postman 接口测试
    • jwt,token实现单点登录
    • 谷歌的验证码
    • Apache的加密规则
    • html,css,javascript
    • jQuery,ajax,json,
    • freeMark,thymeleaf,el-jstl
    • bootstrap,element-UI,vue
    • 阿里的图标库
    • 评分星星组件
    • Git

 二.环境搭建

  •  自动创建maven项目
  • 加入需要用到的依赖
    •   <!--整合mybatis-plus--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.49</version></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.0.5</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version></dependency><!--加密组件,apache--><dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId><version>1.3</version></dependency><!--redis依赖--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!--jwt--><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency>
      
  • 在主配置文件application.properties下配置好mysql,redis,tomcat等
    • 在这里,我们使用的mysql和redis都是docker容器当中部署的,相应的部署看docker那一篇
    • 映射到宿主机当中的端口号分别是:3307,6380,ip:192.168.8.171
    • spring.datasource.driver-class-name=com.mysql.jdbc.Driver
      spring.datasource.username=root
      spring.datasource.password=root
      spring.datasource.url=jdbc:mysql://192.168.8.171:3307/docker01?serverTimezone=UTC#mybatis日志
      mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
      #关闭驼峰命名法
      mybatis-plus.configuration.map-underscore-to-camel-case=false
      mybatis-plus.mapper-locations=classpath:mapper/*Mapper.xml
      mybatis-plus.type-aliases-package=com.pro.domain#Redis
      spring.redis.port=6380
      spring.redis.database=0
      spring.redis.host=192.168.8.171#secret
      jwt.secret=zyggg#端口号
      server.port=9000
      #项目的虚拟路径
      server.servlet.context-path=/LoginFriends
      
  • 数据库设计
    • 使用idea连接数据库自动生成实体类
      • 然后将启动类加上mapper扫描器注解
        • package com.pro;import org.mybatis.spring.annotation.MapperScan;
          import org.springframework.boot.SpringApplication;
          import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
          @MapperScan(value = "com.pro.mapper")
          public class LoginFriendsApplication {public static void main(String[] args) {SpringApplication.run(LoginFriendsApplication.class, args);}}
          
      • 写好三层架构,工具包,写出mapper测试类
        • 测试数据库查询成功
    • 现在开始实现加密注册功能
      • service

        • package com.pro.service;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
          import com.pro.domain.User;
          import com.pro.mapper.UserMapper;
          import com.pro.service.exception.ServiceException;
          import org.apache.commons.codec.digest.DigestUtils;
          import org.springframework.beans.factory.annotation.Autowired;
          import org.springframework.stereotype.Service;import java.util.List;
          import java.util.Random;@Service
          public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Overridepublic User creatUser(User user) {QueryWrapper queryWrapper = new QueryWrapper();queryWrapper.eq("username",user.getUsername());
          //        User user1 = userMapper.selectOne(queryWrapper);//不能这样写,如果你这样写,只是从数据库查出来的,并不是你造的对象,也就没有get,set方法,所以后面会抛异常List<User> list = userMapper.selectList(queryWrapper);if(list.size() >0){//不为空说明已存在该用户名,所以我们抛出自己建的service异常throw  new ServiceException("error01","用户名已存在");}User user1 = new User();//能执行到这,说明可以注册user1.setUsername(user.getUsername());//然后,我们来给密码进行加密Random random = new Random();int salt = random.nextInt(1000)+1000;//随机生成一个1000-1999的数user1.setSalt(salt);//给传进来的密码进行加密,需要到Apache的加密插件String pwd = DigestUtils.md5Hex(user.getPassword()+salt);user1.setPassword(pwd);//然后,将用户插入到数据库userMapper.insert(user1);return user1;}
          }
          
      • 因为我们需要在用户存在时,抛出一个service异常,那么我们需要写这个异常类
        • package com.pro.service.exception;public class ServiceException extends RuntimeException{private String code;private String msg;//要注意,项目开发阶段,code和msg这两个并不是想写什么就写什么//为了统一bug或运行时的消息内容,就需要固定的code和msg,这样团队开发人员就知道某个编码所对应的意思//1.要么定义一个接口,写上常量,用接口名+常量名的形式//2.定义枚举public ServiceException(String code, String msg) {super(code+":"+msg);this.code = code;this.msg = msg;}public String getCode() {return code;}public void setCode(String code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}@Overridepublic String toString() {return "ServiceException{" +"code='" + code + '\'' +", msg='" + msg + '\'' +'}';}
          }
          
      • 在控制层,我们写一个帮助类R来统一返回结果
      • package com.pro.util;public class R {private Integer code;private String msg;private Object data;public R() {}public R(Integer code, String msg, Object data) {this.code = code;this.msg = msg;this.data = data;}public R(int code, String msg) {this.code = code;this.msg = msg;}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getMsg() {return msg;}public void setMsg(String msg) {this.msg = msg;}public Object getData() {return data;}public void setData(Object data) {this.data = data;}
        }
        
      • 然后写控制层
        • package com.pro.controller;import com.pro.domain.User;
          import com.pro.service.UserService;
          import com.pro.util.R;
          import org.springframework.beans.factory.annotation.Autowired;
          import org.springframework.web.bind.annotation.PostMapping;
          import org.springframework.web.bind.annotation.RequestBody;
          import org.springframework.web.bind.annotation.RestController;@RestController
          public class UserController {@Autowiredprivate UserService userService;@PostMapping("/Register")//@RequestBody这里不能加这个注解public R Register(  User user){try {User user1 = userService.creatUser(user);return new R(200,"注册成功",user1);} catch (Exception e) {e.printStackTrace();return new R(300,"注册失败,该用户已存在");}}}
          
        • postman测试成功
    • 接下来,我们用jwt生成token,将token放到redis里面并设置存活时间,实现单点登录
    • 因为我们要实现一段时间免登录的功能,所以我们需要用到redis来设置存活时间,用jwt的目的,是为了更安全的加密生成token。
      • service:需要写登录

        • package com.pro.service;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
          import com.baomidou.mybatisplus.core.toolkit.StringUtils;
          import com.fasterxml.jackson.databind.ObjectMapper;
          import com.pro.domain.Post;
          import com.pro.domain.User;
          import com.pro.mapper.PostMapper;
          import com.pro.mapper.UserMapper;
          import com.pro.service.exception.ServiceException;
          import org.apache.commons.codec.digest.DigestUtils;
          import org.springframework.beans.factory.annotation.Autowired;
          import org.springframework.data.redis.core.RedisTemplate;
          import org.springframework.stereotype.Service;import java.io.IOException;
          import java.util.List;
          import java.util.Random;
          import java.util.concurrent.TimeUnit;@Service
          public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Autowiredprivate PostMapper postMapper;@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic User creatUser(User user) {QueryWrapper queryWrapper = new QueryWrapper();queryWrapper.eq("username", user.getUsername());
          //        User user1 = userMapper.selectOne(queryWrapper);//不能这样写,如果你这样写,只是从数据库查出来的,并不是你造的对象,也就没有get,set方法,所以后面会抛异常List<User> list = userMapper.selectList(queryWrapper);if (list.size() > 0) {//不为空说明已存在该用户名,所以我们抛出自己建的service异常throw new ServiceException("error01", "用户名已存在");}User user1 = new User();//能执行到这,说明可以注册user1.setUsername(user.getUsername());//然后,我们来给密码进行加密Random random = new Random();int salt = random.nextInt(1000) + 1000;//随机生成一个1000-1999的数user1.setSalt(salt);//给传进来的密码进行加密,需要到Apache的加密插件System.out.println(user.getPassword() + salt);String pwd = DigestUtils.md5Hex(user.getPassword() + salt);user1.setPassword(pwd);//然后,将用户插入到数据库userMapper.insert(user1);return user1;}/*** 登录** @param user* @return*/@Overridepublic User userLogin(User user) {QueryWrapper queryWrapper = new QueryWrapper();queryWrapper.eq("username", user.getUsername());User user1 = userMapper.selectOne(queryWrapper);if (user1 == null) {throw new ServiceException("error02", "用户名不存在");}//如果能执行到这,说明用户存在,现在需要比对密码是否正确,要注意密码是加过密的//将用户输入的密码加上盐值,再次加密,然后和数据库查出来的进行比对System.out.println(user1.getSalt() + user.getPassword());String pwd = DigestUtils.md5Hex(user.getPassword() + user1.getSalt());//前后位置要注意,自己在这里报了错,密码加盐值还是盐值加密码if (!user1.getPassword().equals(pwd)) {//不对,则抛出异常throw new ServiceException("error3", "密码错误");}//执行到这一步,则登录成功return user1;}/*** 查所有好友朋友圈,按时间倒序** @param userId* @return*/@Overridepublic List<Post> getFriendsPosts(Long userId) {User user = new User();user.setUserId(userId);List<Post> postList = postMapper.selectFriendsPost(user);if (postList.size() < 0) {throw new ServiceException("302", "没有好友发帖!");}return postList;}/*** 判断用户是否处于登录状态** @param token* @return*/@Overridepublic User queryLoginUserByToken(String token) {User loginUser = new User();//1.从Redis中取token//前端拿到的只是toekn,而我们存进redis的是在token前面拼接了一个token的,所以要String redisTokenKey = "token_" + token;String redisData = (String) redisTemplate.opsForValue().get(redisTokenKey);//如果没有,则表示没有登录或已经过期,不需要返回数据的时候,我们可以用Boolean/* if(StringUtils.isEmpty(redisData)){return false;}*///但是在这个案例当中,我们需要Redis当中的登录用户信息if(StringUtils.isEmpty(redisData)){throw new ServiceException("308","您未处于登录状态");}try {// 如果可以执行到这,则表示redis里面有,是登录用户//2.延长过期时间redisTemplate.expire(redisTokenKey, 1, TimeUnit.HOURS);//3.这一步可以选择要不要,看是否需要从token中解析出用户信息ObjectMapper objectMapper = new ObjectMapper();loginUser = objectMapper.readValue(redisData, User.class);} catch (IOException e) {e.printStackTrace();}//将解析好的用户信息传给调用者return loginUser;}}
          
        • 控制层:这里jwt生成token,存进Redis的思想很重要
        • 如果调用方法可以返回登录用户的信息,说明用户登录成功
                然后我们为了实现一段时间用户免登录的功能,我们需要把
                登录用户的信息存进redis,redis是键值数据库,我们可以用jwt更安全的生成token作为键
              用户信息作为值,存进redis
               每次访问后台时,都带上token
               作为后端,是否要保存,所以我们可以把token放到Redis里面key-value
                前端再来访问后端时,把放在头信息当中的token,组合一个key,再通过这个key去Redis里面去获取对应的value
                 如果这个key查出来的value为null,那说明这个用户没有登录
                  但是我们不希望Redis当中的key一直存活,所以我们需要给Redis设置存活时间
                  如果没操作,那就在存活时间过后就是未登陆状态
                  如果断断续续在操作,那么我们就延续存活时间
        •  /*** 登录,一段时间免登录* @param loginUser* @return*/@GetMapping("/Login")public R Login(User loginUser) throws JsonProcessingException {Map map = new HashMap();/**如果调用方法可以返回登录用户的信息,说明用户登录成功* 然后我们为了实现一段时间用户免登录的功能,我们需要把* 登录用户的信息存进redis,redis是键值数据库,我们可以用jwt更安全的生成token作为键* 用户信息作为值,存进redis* //每次访问后台时,都带上token* //作为后端,是否要保存,所以我们可以把token放到Redis里面key-value* //前端再来访问后端时,把放在头信息当中的token,组合一个key,再通过这个key去Redis里面去获取对应的value* //如果这个key查出来的value为null,那说明这个用户没有登录* //但是我们不希望Redis当中的key一直存活,所以我们需要给Redis设置存活时间* //如果没操作,那就在存活时间过后就是未登陆状态* //如果断断续续在操作,那么我们就延续存活时间*/try {User user1 = userService.userLogin(loginUser);if(user1 != null){//只要登录成功,用户信息和jwt生成的token串-->发到前端保存Map<String,Object> claims = new HashMap<>();claims.put("username",user1.getUsername());claims.put("password",user1.getPassword());//jwt生成token//secret是自己定义的加密规则,这个我们可以在配置文件当中自己写一个String token = Jwts.builder().setClaims(claims).signWith(SignatureAlgorithm.HS256, secret).compact();//以token作为键,序列化后的loginUser对象作为值,存入到redis,并设置存活时间String redisTokenKey = "token_" + token;//前面加个token方便自己清楚这个是tokenObjectMapper objectMapper = new ObjectMapper();//writeValueAsString:将传进来的对象序列化为json后返还给调用对象String tokenValue = objectMapper.writeValueAsString(loginUser);//然后,我们现在要将生成的token 和 value存进redis,要注入RedisTemplate,设置存活时间1小时redisTemplate.opsForValue().set(redisTokenKey,tokenValue, Duration.ofHours(1));//然后,我们将登录用户的信息以及token发送到前端,方便后续存到sessionStorage里面map.put("loginUser",loginUser);map.put("token",token);return  new R(200,"登录成功",map);}} catch (ServiceException e) {e.printStackTrace();//有异常,则登录失败return new R(300,e.getMsg());}return new R(301,"用户不存在,登录失败!");}
          
    • 写完登录,我们现在来写登录注册的前台页面

      • 导入需要用到的jar包

        • 页面中文乱码
        • 用bootstrap写的大致的框架
        • <!DOCTYPE html>
          <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
          <link href="css/bootstrap.css" rel="stylesheet">
          <script src="js/jquery-3.6.0.js"></script>
          <script src="js/popper.min.js"></script>
          <script src="js/bootstrap.js"></script>
          <html>
          <head><title>Title</title>
          </head>
          <style></style>
          </head>
          <script>$(function () {})</script>
          <body>
          <div class="container  col-12"></div>
          </body>
          <script></script>
          </html>
        • <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
          
      • 先写注册,注册,相对简单,我们主要是进行正则校验,然后到数据库当中进行重名校验(后台已经写好),注册成功之后跳转到登录页就好了
        • <!DOCTYPE html>
          <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
          <link href="css/bootstrap.css" rel="stylesheet">
          <script src="js/jquery-3.6.0.js"></script>
          <script src="js/popper.min.js"></script>
          <script src="js/bootstrap.js"></script>
          <html>
          <head><title>Title</title>
          </head>
          <style>body {background-color: #117a8b;}.container {padding: 0px;margin: 0px;background-color: white;}.loginbox {position: fixed;left: 50%;top: 50%;transform: translate(-50%, -50%);border: 2px solid pink;box-shadow: 0 0 10px lightgoldenrodyellow;}
          </style>
          </head>
          <script>$(function () {$('#imgVerifyCode').click(function () {reloadVerifyCode();})})function reloadVerifyCode() {//实现验证码点击刷新,我们是通过重新设置属性src的值继续为verify_code,但是为了防止使用缓存,我们在后面传递一个时间戳做为参数//这样可以保证每次传递的参数不一样,就不会用到缓存的数据,就相当于重新发了请求,达到了刷新验证码的目的$('#imgVerifyCode').attr("src", "verify_code?tp=" + new Date().getTime());}
          </script>
          <body>
          <div class="container loginbox col-5"><!--导航--><nav class="navbar navbar-light bg-white shadow"><ul class="nav"><li class="nav-item m-0 p-0"><!--                <img src="data:images/logo01.png" class=" " style="width: 150px;margin-left: -30px" alt="">--></li></ul></nav><!-- 表单 --><div class="container mt-2 p-2 m-0 col-12"><form id="frmLogin"><div class="passport bg-white"><h4 class="float-left pl-2">用户注册</h4><h6 class="float-right pt-2 pr-2"><a href="Login.html" style="color: #C40F25;font-size: 20px">用户登录</a></h6><div class="clearfix"></div><div class="alert d-none mt-2" id="tips"></div><div class="input-group mt-2"><input type="text" name="username" id="username" class="form-control p-4" placeholder="请输入用户名"style="border-radius: 10px"></div><div class="input-group mt-4" style="border-radius: 10px"><input type="password" name="password" id="password" class="form-control p-4" placeholder="请输入密码"style="border-radius: 10px"></div></div><a href="#" id="btnSubmit" class="btn btn-success btn-block mt-4 text-white pt-3 pb-3"style="background-color: #0f6674;border-radius: 15px">注册</a></form></div>
          </div>
          <!--注册失败的重名模式窗提示-->
          <div class="modal fade" id="creatFailure" tabindex="-1"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-body" id="tipFail"></div><div class="modal-footer"><a href="Register.html" type="button" class="btn btn-primary">重新注册</a></div></div></div>
          </div>
          </body>
          <script>$('#btnSubmit').click(function () {var username = $.trim($('#username').val());var password = $.trim($('#password').val());var reg = /^.{1,10}$/if (!reg.test(username)) {$('#tips').removeClass("d-none")$('#tips').hide();$('#tips').addClass("alert-danger");$('#tips').text("用户名格式应为(1-10位)");$('#tips').fadeIn(300);return;} else {$('#tips').text("");$('#tips').fadeOut(300);$('#tips').removeClass();$('#tips').addClass('alert');}if (!reg.test(password)) {$('#tips').removeClass("d-none")$('#tips').hide();$('#tips').addClass("alert-danger");$('#tips').text("密码格式应为(1-10位)");$('#tips').fadeIn(300);return;} else {$('#tips').text("");$('#tips').fadeOut(300);$('#tips').removeClass();$('#tips').addClass('alert');}//pwd正则处理同上,可以优化。。$(this).text("正在处理...");//alert('$.ajax...'+username+","+password)$.ajax({url: 'Register',type: 'post',data: $('#frmLogin').serialize(),dataType: 'json',success: function (res) {console.log(res)if (res.code == 200) {//将后端生成的token放到sessionStorage里面console.log(res)//跳转到登录页window.location = "Login.html";alert('注册成功')} else {/*    $('#tips').removeClass("d-none")$('#tips').hide();$('#tips').addClass("alert-danger");$('#tips').text(res.msg);$('#tips').fadeIn(1000);//用定时器让提示2s后消失setTimeout("yx()",2000);*///上面注掉,用模式窗提示console.log(res)$('#tipFail').text(res.msg);$('#creatFailure').modal('show')}}})})function yx() {$('#tips').text("");$('#tips').fadeOut(1000);$('#tips').removeClass();$('#tips').addClass('alert');}
          </script>
          </html>
      • 登录,我们要将后台传过来的数据当中的data.token放进sessionStorage里面,方便后续所有必须登录才能进行操作的方法用到。登录成功跳转到首页,在这个案例当中,我们是到发现页
      • 然后在以后的每次需要发请求调用后台方法的时候,将token放进请求头当中,到后台调用方法查询Redis当中是否有这个token对应的用户信息,如果有,就可以继续调用后面的方法操作,并且延长存活时间,没有,就需要弹出提示框重新登录
      • <!DOCTYPE html>
        <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
        <link href="css/bootstrap.css" rel="stylesheet">
        <script src="js/jquery-3.6.0.js"></script>
        <script src="js/popper.min.js"></script>
        <script src="js/bootstrap.js"></script>
        <html>
        <head><title>Title</title>
        </head>
        <style>body {background-color: #117a8b;}.container {padding: 0px;margin: 0px;background-color: white;}.loginbox {position: fixed;left: 50%;top: 50%;transform: translate(-50%, -50%);border: 2px solid pink;box-shadow: 0 0 10px lightgoldenrodyellow;}
        </style>
        </head>
        <script>$(function () {$('#imgVerifyCode').click(function () {reloadVerifyCode();})})function reloadVerifyCode() {//实现验证码点击刷新,我们是通过重新设置属性src的值继续为verify_code,但是为了防止使用缓存,我们在后面传递一个时间戳做为参数//这样可以保证每次传递的参数不一样,就不会用到缓存的数据,就相当于重新发了请求,达到了刷新验证码的目的$('#imgVerifyCode').attr("src", "verify_code?tp=" + new Date().getTime());}
        </script>
        <body>
        <div class="container loginbox col-5"><!--导航--><nav class="navbar navbar-light bg-white shadow"><ul class="nav"><li class="nav-item m-0 p-0">
        <!--                <img src="data:images/logo01.png" class=" " style="width: 150px;margin-left: -30px" alt="">--></li></ul></nav><!-- 表单 --><div class="container mt-2 p-2 m-0 col-12"><form id="frmLogin"><div class="passport bg-white"><h4 class="float-left pl-2">用户登录</h4><h6 class="float-right pt-2 pr-2"><a href="register.html" style="color: #C40F25;font-size: 20px">用户注册</a></h6><div class="clearfix"></div><div class="alert d-none mt-2" id="tips"></div><div class="input-group mt-2"><input type="text" name="username" id="username" class="form-control p-4" placeholder="请输入用户名"style="border-radius: 10px"></div><div class="input-group mt-4" style="border-radius: 10px"><input type="password" name="password" id="password" class="form-control p-4" placeholder="请输入密码"style="border-radius: 10px"></div></div><a href="#" id="btnSubmit" class="btn btn-success btn-block mt-4 text-white pt-3 pb-3"style="background-color: #0f6674;border-radius: 15px">登录</a></form></div>
        </div>
        <!--注册失败的重名模式窗提示-->
        <div class="modal fade" id="creatFailure" tabindex="-1"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-body" id="tipFail"></div><div class="modal-footer"><a href="login.html" type="button" class="btn btn-primary">重新登录</a></div></div></div>
        </div>
        </body>
        <script>$('#btnSubmit').click(function () {var username = $.trim($('#username').val());var password = $.trim($('#password').val());var reg = /^.{1,10}$/if (!reg.test(username)) {$('#tips').removeClass("d-none")$('#tips').hide();$('#tips').addClass("alert-danger");$('#tips').text("用户名格式应为(1-10位)");$('#tips').fadeIn(300);return;} else {$('#tips').text("");$('#tips').fadeOut(300);$('#tips').removeClass();$('#tips').addClass('alert');}if (!reg.test(password)) {$('#tips').removeClass("d-none")$('#tips').hide();$('#tips').addClass("alert-danger");$('#tips').text("密码格式应为(1-10位)");$('#tips').fadeIn(300);return;} else {$('#tips').text("");$('#tips').fadeOut(300);$('#tips').removeClass();$('#tips').addClass('alert');}//pwd正则处理同上,可以优化。。$(this).text("正在处理...");//alert('$.ajax...'+username+","+password)$.ajax({url: 'Login',type: 'get',data: $('#frmLogin').serialize(),dataType: 'json',success: function (res) {console.log(res)if (res.code == 200) {//将后端生成的token放到sessionStorage里面console.log(res)window.sessionStorage.setItem("token",res.data.token)//跳转到主页面window.location = "faXian.html";} else {/*    $('#tips').removeClass("d-none")$('#tips').hide();$('#tips').addClass("alert-danger");$('#tips').text(res.msg);$('#tips').fadeIn(1000);//用定时器让提示2s后消失setTimeout("yx()",2000);*///上面注掉,用模式窗提示console.log(res)$('#tipFail').text(res.msg);$('#creatFailure').modal('show')}}})})function yx() {$('#tips').text("");$('#tips').fadeOut(1000);$('#tips').removeClass();$('#tips').addClass('alert');}
        </script>
        </html>
      • 现在我们来写发现页前端页面,因为这个页面其实就是实现了一个点击朋友圈跳转到朋友圈页面的功能即可,到朋友圈页面之后,只需要我们将sessionStorage里面的token取出来放进请求头,发ajax请求去显示好友的朋友圈列表就可以了
        • 页面大致是这样,页面是静态页面
        • <!DOCTYPE html>
          <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
          <link href="css/bootstrap.css" rel="stylesheet">
          <script src="js/jquery-3.6.0.js"></script>
          <script src="js/popper.min.js"></script>
          <script src="js/bootstrap.js"></script>
          <html>
          <head><title>Title</title>
          </head>
          <style>body {background-color: #EDEDED;}.container {padding: 0px;margin: 0px;background-color: #EDEDED;width: 100%;height: 100% !important;font-weight: bold;font-family: "Microsoft JhengHei";}.loginbox {position: fixed;border: 2px solid pink;box-shadow: 0 0 10px lightgoldenrodyellow;}.navbar {background-color: #EDEDED !important;text-align: center;font-weight: bold;}div {/*background-color:white ;*/}.row {background-color: white;border-top: 1px solid gainsboro;}img {width: 23px;}.bottomNav{position: absolute;bottom: 0%;padding: 0px;margin: 0px;}.bottomNav li{border: 1px solid gainsboro;border-right: 0px;}.bottomNav ul{padding: 0px;margin: 0px;}a{color: black;font-weight: bold;font-family: "Microsoft JhengHei";}
          </style>
          </head>
          <script>$(function () {})</script>
          <body>
          <div class="container  col-12"><!--导航--><nav class="navbar navbar-light bg-white " style="height: 10%"><ul class="nav" style="width: 100%"><li class="nav-item m-0 p-0" style="width: 100%">发现</li></ul></nav><a href="friendsLife.html"><div class="row shadow" style="height: 8%"><div class="col-1 pt-3 pb-3 mr-2"><img src="data:images/icon01.png" alt=""></div><div class="col-3 pt-3 pb-3 h-100">朋友圈</div></div></a><div class="row shadow" style="height: 16%"><div class="col-1 pt-3 pb-3 mr-2"><img src="data:images/icon02.png" alt=""></div><div class="col-3 pt-3 pb-3 h-100">视频号</div><div class=" row col-12 " style="height: 8%"><div class="col-1 pt-3 pb-3 mr-2"><img src="data:images/icon03.png" alt=""></div><div class="col-3 pt-3 pb-3 h-100">直播</div></div></div><div class="row shadow" style="height: 16%"><div class="col-1 pt-3 pb-3 mr-2"><img src="data:images/icon04.png" alt=""></div><div class="col-3 pt-3 pb-3 h-100">扫一扫</div><div class=" row col-12 " style="height: 8%"><div class="col-1 pt-3 pb-3 mr-2"><img src="data:images/icon05.png" alt=""></div><div class="col-3 pt-3 pb-3 h-100">摇一摇</div></div></div><div class="row shadow" style="height: 8%"><div class="col-1 pt-3 pb-3 mr-2"><img src="data:images/icon06.png" alt=""></div><div class="col-3 pt-3 pb-3 h-100">看一看</div></div><div class="row shadow" style="height: 8%"><div class="col-1 pt-3 pb-3 mr-2"><img src="data:images/icon07.png" alt=""></div><div class="col-3 pt-3 pb-3 h-100">附近</div></div><div class="row " style="height: 8%"><div class="col-1 pt-3 pb-3 mr-2"><img src="data:images/icon08.png" alt=""></div><div class="col-3 pt-3 pb-3 h-100">小程序</div></div></div>
          <div class="row col-12 bottomNav" style="height: 8% " ><ul class="nav nav-pills nav-justified col-12"><li class="nav-item col-3"><a class="nav-link " href="#">微信</a></li><li class="nav-item col-3"><a class="nav-link p-0 pt-2 " href="#">通讯录</a></li><li class="nav-item col-3"><a class="nav-link" href="#">发现</a></li><li class="nav-item col-3"><a class="nav-link" href="#">我</a></li></ul>
          </div>
          </body>
          <script>$('#btnSubmit').click(function () {var username = $.trim($('#username').val());var password = $.trim($('#password').val());var reg = /^.{1,10}$/if (!reg.test(username)) {$('#tips').removeClass("d-none")$('#tips').hide();$('#tips').addClass("alert-danger");$('#tips').text("用户名格式应为(1-10位)");$('#tips').fadeIn(300);return;} else {$('#tips').text("");$('#tips').fadeOut(300);$('#tips').removeClass();$('#tips').addClass('alert');}if (!reg.test(password)) {$('#tips').removeClass("d-none")$('#tips').hide();$('#tips').addClass("alert-danger");$('#tips').text("密码格式应为(1-10位)");$('#tips').fadeIn(300);return;} else {$('#tips').text("");$('#tips').fadeOut(300);$('#tips').removeClass();$('#tips').addClass('alert');}//pwd正则处理同上,可以优化。。$(this).text("正在处理...");//alert('$.ajax...'+username+","+password)$.ajax({url: 'Register',type: 'post',data: $('#frmLogin').serialize(),dataType: 'json',success: function (res) {console.log(res)if (res.code == 200) {//将后端生成的token放到sessionStorage里面console.log(res)//跳转到登录页window.location = "Login.html";alert('注册成功')} else {/*    $('#tips').removeClass("d-none")$('#tips').hide();$('#tips').addClass("alert-danger");$('#tips').text(res.msg);$('#tips').fadeIn(1000);//用定时器让提示2s后消失setTimeout("yx()",2000);*///上面注掉,用模式窗提示console.log(res)$('#tipFail').text(res.msg);$('#creatFailure').modal('show')}}})})function yx() {$('#tips').text("");$('#tips').fadeOut(1000);$('#tips').removeClass();$('#tips').addClass('alert');}
          </script>
          </html>
      • 然后现在写朋友圈页面的静态原型(效果附上)
        • 注意在设置登录用户的头像的时候,用到了z-index(数值越大,浮在上层越高)
        • <!DOCTYPE html>
          <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
          <link href="css/bootstrap.css" rel="stylesheet">
          <script src="js/jquery-3.6.0.js"></script>
          <script src="js/popper.min.js"></script>
          <script src="js/bootstrap.js"></script>
          <html>
          <head><title>Title</title>
          </head>
          <style>.container {padding: 0px;margin: 0px;width: 100%;background-color: white;}img {width: 100%;}.userImage{position: absolute;right: 5%;bottom: 0;transform: translate(0px,30%);z-index: 9999;/*数值越大,浮在越上面*/}
          </style>
          </head>
          <script>$(function () {})</script>
          <body>
          <div class=" row col-12 p-0 m-0" style="height: 300px;background-color: pink"><img src="data:images/intro.png" alt="" style="height: 100%"><div class="col-3 p-0 m-0 userImage"><img class="p-0" src="data:images/tx01.jpg" alt="" style="width: 100% ;border-radius: 10%"></div>
          </div><div class="row col-12 p-0 m-0" style="height: 100px;background-color: white"></div><!--每个好友发的消息-->
          <div class="row col-12 p-0 m-0"><!--好友头像--><div class="col-2 p-0 m-0 "><img class="p-0" src="data:images/tx01.jpg" alt="" style="width: 100% ;border-radius: 10%"></div><div class="col-1 p-0"></div><!--好友信息和朋友圈内容--><div class="col-9 p-0 m-0"><div class="row "><span class="col-12 p-0" style="color: skyblue">拍呱呱哈哈哈哈哈哈</span></div><div class="row "><span class="col-12 p-0">在容器c1 中的data_container中建一个a.txt,然后在宿主机中修改这个a.txt,内容为world,然后再在容器c1的data_container 中查看a.txt是否有world</span></div><div class="row p-0"><img class="" src="data:images/mes01.png" alt="" style="width: 100%"></div></div>
          </div>
          <hr>
          <!--每个好友发的消息-->
          <div class="row col-12 p-0 m-0"><!--好友头像--><div class="col-2 p-0 m-0 "><img class="p-0" src="data:images/tx01.jpg" alt="" style="width: 100% ;border-radius: 10%"></div><div class="col-1 p-0"></div><!--好友信息和朋友圈内容--><div class="col-9 p-0 m-0"><div class="row "><span class="col-12 p-0" style="color: skyblue">拍呱呱哈哈哈哈哈哈</span></div><div class="row "><span class="col-12 p-0">在容器c1 中的data_container中建一个a.txt,然后在宿主机中修改这个a.txt,内容为world,然后再在容器c1的data_container 中查看a.txt是否有world</span></div><div class="row p-0"><img class="" src="data:images/mes01.png" alt="" style="width: 100%"></div></div>
          </div>
          <hr>
          <!--每个好友发的消息-->
          <div class="row col-12 p-0 m-0"><!--好友头像--><div class="col-2 p-0 m-0 "><img class="p-0" src="data:images/tx01.jpg" alt="" style="width: 100% ;border-radius: 10%"></div><div class="col-1 p-0"></div><!--好友信息和朋友圈内容--><div class="col-9 p-0 m-0"><div class="row "><span class="col-12 p-0" style="color: skyblue">拍呱呱哈哈哈哈哈哈</span></div><div class="row "><span class="col-12 p-0">在容器c1 中的data_container中建一个a.txt,然后在宿主机中修改这个a.txt,内容为world,然后再在容器c1的data_container 中查看a.txt是否有world</span></div><div class="row p-0"><img class="" src="data:images/mes01.png" alt="" style="width: 100%"></div></div>
          </div></body>
          <script></script>
          </html>
    • 现在我们根据静态原型,去写朋友圈需要的后端代码,这里我们需要去显示登录用户所有的好友发的朋友圈
    • 先在数据库当中写SQL语句,看是否可以查到我们需要的信息
    • select p.*,u.*
      from post p ,user u,friend f
      where select friendId from friend where userId=8
      -- 查询登录用户所有好友朋友圈发的帖子,并且根据时间倒序排序
      select p.*
      from post p
      where p.userId in (select friendId from friend where userId=8)
      order by p.creatTime desc-- 查询登录用户所有好友朋友圈发的帖子以及对应发帖好友的信息,并且根据时间倒序排序
      select u.* ,p.postId,p.content,p.image,p.creatTime
      from post p ,user u
      where p.userId in (select friendId from friend where userId=8) and p.userId=u.userId
      order by p.creatTime desc
    • SQL没有问题之后,我们在idea当中去写SQL,因为这里用到了子查询,mybatis-plus自带的查询已经满足不了,所以我们需要自己建一个PostMapper.xml文件去写SQL
    • PostMapper接口里写查询的方法
    • xml文件对应的SQL,多对一的关系
    • <?xml version="1.0" encoding="UTF-8"?>
      <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
      <mapper namespace="com.pro.mapper.PostMapper"><resultMap id="perMap" type="Post"><id property="postId" column="postId"/><result property="content" column="content"/><result property="image" column="image"/><result property="creatTime" column="creatTime"/><collection property="list" ofType="User" column="userId"><id property="userId" column="userId"/><result property="username" column="username"/><result property="password" column="password"/><result property="mobile" column="mobile"/><result property="email" column="email"/><result property="image" column="image"/><result property="salt" column="salt"/></collection></resultMap><select id="selectFriendsPost" resultMap="perMap" parameterType="User">select u.* ,p.postId,p.content,p.image,p.creatTimefrom post p ,user uwhere p.userId in (select friendId from friend where userId= #{userId}) and p.userId=u.userIdorder by p.creatTime desc</select>
      </mapper>
      
    • mapper测试成功
    • package com.pro.mapper;import com.pro.domain.Post;
      import com.pro.domain.User;
      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.junit4.SpringRunner;import java.util.List;import static org.junit.Assert.*;
      @RunWith(SpringRunner.class)
      @SpringBootTest
      public class PostMapperTest {@Autowiredprivate PostMapper postMapper;@Testpublic void selectFriendsPost() {User user = new User();user.setUserId(8);List<Post> postList = postMapper.selectFriendsPost(user);for (Post post : postList) {System.out.println(post);}}
      }
      
  • 都写到这一步了,那我们现在就只需要去UserSerice里面写业务就好了(因为是登陆用户去查看自己的朋友圈哪些好友发了贴子)getFriendsPosts,并且要写上根据token从Redis里面查是否有登录用户信息的queryLoginUserByToken这个方法,在Redis有登录用户信息的前提下,才可以调用查列表的方法。

  •  /*** 查所有好友朋友圈,按时间倒序** @param userId* @return*/@Overridepublic List<Post> getFriendsPosts(Long userId) {User user = new User();user.setUserId(userId);List<Post> postList = postMapper.selectFriendsPost(user);if (postList.size() < 0) {throw new ServiceException("302", "没有好友发帖!");}return postList;}/*** 判断用户是否处于登录状态** @param token* @return*/@Overridepublic User queryLoginUserByToken(String token) {User loginUser = new User();//1.从Redis中取token//前端拿到的只是toekn,而我们存进redis的是在token前面拼接了一个token的,所以要String redisTokenKey = "token_" + token;String redisData = (String) redisTemplate.opsForValue().get(redisTokenKey);//如果没有,则表示没有登录或已经过期,不需要返回数据的时候,我们可以用Boolean/* if(StringUtils.isEmpty(redisData)){return false;}*///但是在这个案例当中,我们需要Redis当中的登录用户信息if(StringUtils.isEmpty(redisData)){throw new ServiceException("308","您未处于登录状态");}try {// 如果可以执行到这,则表示redis里面有,是登录用户//2.延长过期时间redisTemplate.expire(redisTokenKey, 1, TimeUnit.HOURS);//3.这一步可以选择要不要,看是否需要从token中解析出用户信息ObjectMapper objectMapper = new ObjectMapper();loginUser = objectMapper.readValue(redisData, User.class);} catch (IOException e) {e.printStackTrace();}//将解析好的用户信息传给调用者return loginUser;}
    
  • 在控制层,我们要写一个获取朋友圈所有列表的方法,先调用查token的方法,再判断是否可以去访问朋友圈列表(这个案例我们以后会经常用到

  • /*** 获取所有朋友圈帖子列表** @param token* @return*/@GetMapping("/getFriendPosts")public R getFriendPosts(String token) {//根据前端发请求时请求头当中带来的token,做为键去redis当中去查,是否还有该登录用户//没有了,就直接返回不成功,,并提示未登录或长时间未操作,让用户重新登录//有,就可以直接调用查询方法返回查询结果User redisLoginUser = userService.queryLoginUserByToken(token);//如果是登录用户,则可以调用方法去查询帖子if (redisLoginUser == null) {return new R(600, "未登录或待机时间超过一小时,请登录");}try {List<Post> friendsPosts = userService.getFriendsPosts(redisLoginUser.getUserId());return new R(200, "查询帖子成功!", friendsPosts);} catch (ServiceException e) {e.printStackTrace();return new R(309, e.getMsg());}}
  • 然后我们用postman测试接口是否可用
  • 先登录,确认登录后生成的token写入了Redis,在将登录生成的token作为参数发请求调用查列表的方法,看是否可以查询
  • 然后查朋友圈就成功了,那么接口就无误了
  • 在postman测试朋友圈列表的时候,我们发现,虽然登录成功,但是并没有数据,通过debug,发现是在登录的时候,我们存进Redis的值只有登录用户的用户名和密码,但是我们在获取列表的时候,用到了Redis里登录用户的ID,可是我们没存,所以,要将登录接口当中存Redis时的loginUser改为user1
  • 到这一步了,我们就可以继续完成前台的页面的显示,动态的显示

  • 主要是请求头,控制层代码要改一下
  • <!DOCTYPE html>
    <meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
    <link href="css/bootstrap.css" rel="stylesheet">
    <script src="js/jquery-3.6.0.js"></script>
    <script src="js/popper.min.js"></script>
    <script src="js/bootstrap.js"></script>
    <script src="js/art-template.js"></script>
    <html>
    <head><title>Title</title>
    </head>
    <style>.container {padding: 0px;margin: 0px;width: 100%;background-color: white;}img {width: 100%;}.userImage {position: absolute;right: 5%;bottom: 0;transform: translate(0px, 30%);z-index: 9999; /*数值越大,浮在越上面*/}</style>
    </head>
    <script>$(function () {/*从sessionStorage取token*/var token = window.sessionStorage.getItem("token");alert(token)$.ajax({url: 'getFriendPosts',headers:{token:token},type: 'get',//与接口对应dataType: 'json',success: function (res) {if(res.code == 200){var friendsPostsList = res.datafor (var i = 0; i < friendsPostsList.length; i++) {var post = friendsPostsList[i];//连接模板与后台响应的结果bookvar html = template("tpl", post);//将结果拼接在模板上之后,追加到div(bookList)中console.log(post);$('#postList').append(html);}}else {console.log(res.msg);$('#tipFail').text(res.msg);$('#creatFailure').modal('show')}}})})
    </script>
    <!--图片对应数据的模板-->
    <script type="text/html" id="tpl"><div class="row col-12 p-0 m-0" style="height: 30px;background-color: white"></div><!--每个好友发的消息--><div class="row col-12 p-0 m-0"><!--好友头像--><div class="col-2 p-0 m-0 "><img class="p-0" src="{{list[0].image}}" alt="" style="width: 100% ;border-radius: 10%"></div><div class="col-1 p-0"></div><!--好友信息和朋友圈内容--><div class="col-9 p-0 m-0"><div class="row "><span class="col-12 p-0" style="color: darkblue;font-weight: bold">{{list[0].username}} &nbsp;{{creatTime}}</span></div><div class="row "><span class="col-12 p-0">{{content}}</span></div><div class="row p-0"><img class="" src="{{image}}" alt="" style="width: 100%;border-radius: 10%"></div></div></div><hr style="color: #4e555b">
    </script>
    <body>
    <div class=" row col-12 p-0 m-0" style="height: 300px;background-color: pink" id="postList"><img src="data:images/intro.png" alt="" style="height: 100%"><div class="col-3 p-0 m-0 userImage"><img class="p-0" src="data:images/tx01.jpg" alt="" style="width: 100% ;border-radius: 10%"></div>
    </div><!--每个好友发的消息-->
    <div class="row col-12 p-0 m-0"></div><!--注册失败的重名模式窗提示-->
    <div class="modal fade" id="creatFailure" tabindex="-1"><div class="modal-dialog modal-dialog-centered"><div class="modal-content"><div class="modal-body" id="tipFail"></div><div class="modal-footer"><a href="login.html" type="button" class="btn btn-primary">重新登录</a></div></div></div>
    </div>
    </body>
    <script></script>
    </html>
  • 效果:
  • 但是在写的时候遇到了几个问题,总结一下:
    • 引擎模板template使用的时候,要用的数据层级关系,可以先打印数据到控制台,看是什么样的层级,如果要用到数组遍历,不会的话,去百度查,一般是list[]
    • 还有,刚查出来的时候,发现发帖好友显示出来的头像和帖子当中的图片是一样的,而且debug之后,观察到,后台返回过来的数据集合当中,就已经是一样的了,说明错误出在SQL上面:最终是因为两个image查询的时候名字一样,需要起别名
  • 最后做一下整体的总结:

    • 在这个微信朋友圈案例当中,
    • 我全部使用到的技术点:
      • idea:开发环境
      • docker容器技术部署了MySQL和Redis
      • Redis数据库用来存储登录用户的信息,用于实现未登录不能进行一些操作
      • MySQL数据库存储了表结构和数据
      • mybatis-plus框架
      • springboot框架
      • html超文本标记语言
      • css层叠样式表
      • JavaScript脚本语言
      • jQuery(JavaScript库)
      • boostrap响应式前端框架
      • template引擎模板
      • jwt开放标准
    • 遇到的难点:
      • idea使用Redis,设置存活时间
      • 请求头与token
      • 复杂SQL对应xml文件,还不太熟练
      • template模板
    • 收获即难点
    • 案例后续可扩展
      • 点赞,评论,转发
      • 发朋友圈
      • 查看好友个人信息
      • 聊天界面及功能,好友聊天,短消息

总结学过的技术,实现加密注册,登录及过期不能访问,微信朋友圈功能,文章比较长,但是比较详细。相关推荐

  1. 微信朋友圈技术之道:三个人的后台团队与每日十亿的发布量

    概述 截止到2015年7月,微信每月活跃用户约5.49亿,朋友圈每天的发表量(包括赞和评论)超过10亿,浏览量超过100亿.得益于4G网络的发展,以上数据仍有很快的增长,而且相对于PC互联网时代,移动 ...

  2. mfc让图片与按钮一起_微信朋友圈发图片还能添加语音,简单两步就能搞定!今天学到了...

    大家好,我是分享科技小达人~ 今天跟大家探讨的问题是:[微信朋友圈发图片添加语音的方法]. 日常生活中,我们都喜欢发朋友圈,今天就来教你如何在微信朋友圈,发送带语音的图片,方法非常简单,一起来学习一下 ...

  3. 微信朋友圈技术实现设想

    前提 微信朋友圈是我们每天都在用的功能, 但是如果让你来实现一个微信朋友圈, 你会如何做呢? 我来简单设想一下. 实现功能 发朋友圈 评论动态 查看朋友圈(只能查看好友的) 查看评论(只能查看共同好友 ...

  4. IM开发技术学习:揭秘微信朋友圈这种信息推流背后的系统设计

    本文由徐宁发表于腾讯大讲堂,原题"程序员如何把你关注的内容推送到你眼前?揭秘信息流推荐背后的系统设计",有改动和修订. 1.引言 信息推流(以下简称"Feed流" ...

  5. Android 仿微信朋友圈拍照原理解读,技术分析

    在日常开发中,我们的APP中经常会有拍照的功能,很多的产品经理会要求把拍照和拍照后编辑的体验弄成和微信一样. 先来看看微信的拍照界面和编辑界面 微信拍照的两个优点 :1.响应速度快,从点击拍照按钮,到 ...

  6. 学Linux云计算技术有意义吗?linux学习入门

    近年来,云计算技术发展迅速.毫无疑问,Linux云计算技术的发展前景受到很多公司和个人的青睐.因此,越来越多的人打算学习云计算技术,进入it行业.那学Linux云计算技术有意义吗?云计算发展前景如何? ...

  7. 三分钟快速开发手机号注册登录功能

    手机号注册登录如今已经是大多数APP必备的功能,通过验证码的核实,我们获取了用户的手机号码,从而在以下几个方面确保安全和产生价值: 符合国家信息安全监管的需要,避免个别的用户违反监管要求,而我们无法追 ...

  8. 用 Blink 打造你的技术朋友圈

    有多少小伙伴至今都还不知道Blink呢? Blink上有一个话题叫:来了,奇怪的冷知识,最大的冷知识就是:千万级CSDN 用户,只有几十万人在使用Blink! 所以接下来,一起来看看你为什么要发Bli ...

  9. plsql developer无监听程序_微信小程序支持分享到朋友圈啦!技术解读跟我来

    千呼万唤始出来!微信小程序页面分享到朋友圈的功能,终于在安卓系统灰度测试了!目前只在安卓系统!只在安卓系统!只在安卓系统!iOS系统还没有办法体验. 首先,我们看一下官方文档的描述,解读一下小程序分享 ...

最新文章

  1. 腾讯优图开源首个医疗AI ML预训练模型
  2. 2019年人工智能行业现状与发展趋势报告
  3. UA MATH523A 实分析1 集合论基础1 基本概念复习
  4. 使用 Spring Boot 快速构建 Spring 框架应用--转
  5. Java并发编程(6):Runnable和Thread实现多线程的区别(含代码)
  6. 单链表反转的原理和python代码实现
  7. CodeForces - 1220B Multiplication Table(思维)
  8. C#调用C++的dll文件方法
  9. python操作excel_使用Python操作Excel时必学的3个库
  10. 关于django的模板
  11. linux系统优化篇之---top
  12. 解决读取数据库里面中文字符乱码的问题
  13. @程序员,这门编程语言不输 C/C++!
  14. 使用systemd来构建你的服务
  15. sql server Developer Edition版本的下载安装
  16. 桌面虚拟化:软件为先
  17. 常见Http响应头部 responses header
  18. JS临时死区(TDZ)
  19. Centos 8 搭建samba文件共享服务(超详细)
  20. C++ string 简单截取字符串使用

热门文章

  1. 当我说转行大数据工程师时,众人笑我太疯癫,直到四个月后......
  2. 《高效的秘密》第五,六章读后感
  3. cati服务器授权信息无效,cati安装
  4. 亚马逊封号潮不断,亚马逊封号最新进展,亚马逊账号关联要怎么解决?怎么使用vmlogin浏览做到账号防关联
  5. Win10系统安装3dsmax2014常见问题及解决方案
  6. PEP8 -- Python代码样式指南(中文版)
  7. 新手上路注意事项及驾车技巧
  8. 带通滤波器c5000汇编语言,基于SIW技术的高选择性带通滤波器的设计与实现
  9. 日本杂货连锁店Loft首家海外直营店于上海开业
  10. Python爬虫:老兵不死,用数据纪念2019男篮世界杯