【全栈开发】基于Spring BootVueAndroid扫码授权登录

news2024/11/23 2:45:12

文章目录

    • 一、引言
    • 二、设计
      • 1、移动端(Android)
        • (1)库
        • (2)依赖
        • (3)使用
      • 2、前端(Vue)
        • (1)库
        • (2)使用
      • 3、后端(Spring Boot)
    • 三、附件

一、引言

  • 描述:如何通过移动设备向网页授权登录。
  • 难度:中级
  • 知识点:
    1、ZXing(Android库)
    2、QrCode(Vue库)
    3、Redis过期策略
    4、JWT令牌技术
  • 效果
    在这里插入图片描述

二、设计

1、移动端(Android)

zxing是谷歌推出的识别多种条形码的开源项目

(1)库

感谢开源者们的付出(地址:https://github.com/zxing/zxing)

(2)依赖

implementation 'com.google.zxing:core:3.3.0'

(3)使用

  • cv工程
    在这里插入图片描述
  • 配置权限
    <uses-permission android:name="android.permission.INTERNET" /> <!-- 网络权限 -->
    <uses-permission android:name="android.permission.VIBRATE" /> <!-- 震动权限 -->
    <uses-permission android:name="android.permission.CAMERA" /> <!-- 摄像头权限 -->
    <uses-feature android:name="android.hardware.camera.autofocus" /> <!-- 自动聚焦权限 -->
  • 判断权限
    private static final String DECODED_CONTENT_KEY = "codedContent";
    private static final String DECODED_BITMAP_KEY = "codedBitmap";
    private static final int REQUEST_CODE_SCAN = 0x0000;
    
	//动态权限申请
    if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CAMERA}, 1);
    } else {
        goScan();
    }
       
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode) {
            case 1:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    goScan();
                } else {
                    Toast.makeText(this, "你拒绝了权限申请,可能无法打开相机扫码哟!", Toast.LENGTH_SHORT).show();
                }
                break;
            default:
        }
    }
  • 跳转方法
	/**
     * 跳转到扫码界面扫码
     */
    private void goScan(){
        Intent intent = new Intent(MainActivity.this, CaptureActivity.class);
        startActivityForResult(intent, REQUEST_CODE_SCAN);
    }
  • 返回结果
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        // 扫描二维码/条码回传
        if (requestCode == REQUEST_CODE_SCAN && resultCode == RESULT_OK) {
            if (data != null) {
                //返回的文本内容
                String content = data.getStringExtra(DECODED_CONTENT_KEY);
                //返回的BitMap图像
                Bitmap bitmap = data.getParcelableExtra(DECODED_BITMAP_KEY);

                tv_scanResult.setText("你扫描到的内容是:" + content);
            }
        }
    }

2、前端(Vue)

关于router和store请自主学习

(1)库

  • npm下载
npm install qrcode --save

(2)使用

  • 模块:生成二维码(ValidateCode.vue)
<template>
    <canvas ref="canvas" @click="draw" width="140" height="40" style="cursor: pointer;"></canvas>
</template>
<script>
export default {
    data() {
        return {
            codes: [],
            ctx: "",
            colors: ["red", "yellow", "blue", "green", "pink", "black"],
            code_Len: 4
        };
    },
    mounted() {
        this.draw();
    },
    computed: {
        codeString() {
            let result = "";
            for (let i = 0; i < this.codes.length; i++) {
                result += this.codes[i];
            }
            return result.toUpperCase();
        }
    },
    watch: {
        codeString: function (newValue) {
            this.$emit("change", newValue);
        }
    },
    methods: {
        generateRandom() {
            this.codes = [];
            const chars = "abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789";
            const charsArr = chars.split("");

            for (let i = 0; i < this.code_Len; i++) {
                const num = Math.floor(Math.random() * charsArr.length);
                this.codes.push(charsArr[num]);
            }
        },
        draw() {
            this.generateRandom();
            this.drawText();
        },
        drawLine() {
            const lineNumber = 3; // 线条条数
            const lineX = 140;
            const lineY = 30; // 最大线条坐标
            for (let i = 0; i < lineNumber; i++) {
                this.ctx.strokeStyle = this.colors[Math.floor(Math.random() * 5)];
                this.ctx.beginPath();
                this.ctx.moveTo(
                    Math.floor(Math.random() * lineX),
                    Math.floor(Math.random() * lineY)
                );
                this.ctx.lineTo(
                    Math.floor(Math.random() * lineX),
                    Math.floor(Math.random() * lineY)
                );
                this.ctx.stroke();
            }
        },
        drawText() {
            const canvas = this.$refs["canvas"];
            this.ctx = canvas.getContext("2d");

            this.ctx.fillStyle = "#BFEFFF";
            this.ctx.fillRect(0, 0, 140, 40);
            this.ctx.font = "20px Verdana";

            let x = 15;

            for (let i = 0; i < this.code_Len; i++) {
                this.ctx.fillStyle = this.colors[Math.floor(Math.random() * 5)];
                this.ctx.fillText(this.codes[i], x, 25);
                x = x + 30;
            }

            this.drawLine();
        }
    }
};
</script>
  • UI界面
<template>
  <div class="login">
    <el-button type="text" class="book" @click="book">功能介绍</el-button>
    <div class="main">
      <div class="login-title">
        {{ textLogin }}
      </div>
      <!-- 输入账号和密码 -->
      <div class="login_1" v-show="disLogin">
        <div class="input-data">
          <input type="text" required="" id="userName" v-model="email" />
          <div class="underlineN"></div>
          <label>E-mail</label>
        </div>
        <div class="input-data">
          <input type="password" required="" id="userPass" v-model="pass" />
          <div class="underlineP"></div>
          <label>Password</label>
        </div>
        <div class="login-yzm">
          <validate-code id="canvas" ref="ref_validateCode" @change="changeCode" />
          <div id="yzm-text">验证码:</div>
          <input type="text" id="yzm-input" v-model="inputVal">
        </div>
        <div class="login-btn">
          <button @click="login"><span>登录</span></button>
          <button @click="onRegView"><span>注册</span></button>
        </div>
      </div>
      <!-- 二维码登录 -->
      <div class="login_2" v-show="!disLogin">
        <!-- qrcode-vue -->
        <qrcode-vue :value="showCodeUrl" :size="size" level="H" @click="sx_qr" />
      </div>
      <div class="btn_qr" @click="login_qr">
        <img src="@/assets/saoyisao2.png" style="width: 20px; height: 20px;" alt="扫码登录">
      </div>
    </div>

  </div>
</template>

<script>
import ValidateCode from '@/components/part/ValidateCode.vue';
import QrcodeVue from 'qrcode.vue'

export default {
  name: 'LoginView',
  components: {
    ValidateCode,
    QrcodeVue
  },
  data() {
    return {
      // 验证码组件数据
      inputVal: "",
      checkCode: "",
      // 基本数据
      email: '',
      pass: '',
      // 切换登录方式
      textLogin: "用  户  登  录",
      disLogin: true,
      // 二维码
      codeHttpUrl: this.$store.state.webMapping.qr.findUuid,
      showCodeUrl: 'www.xpq.com',
      codeUrl: '',
      size: 180,
      // 定时器
      timer: null,
      qrHttpTimer: this.$store.state.webMapping.qr.findTime,
    }
  },
  methods: {
    changeCode(value) {     // Code
      this.checkCode = value;
    },
    onRegView() {          // router -> Reg
      this.$router.push('/reg')
    },
    async login() {       // email and pass Login
      if (this.email == '') {
        this.$open.openInfo('请输入邮箱!')
      } else if (this.pass == '') {
        this.$open.openInfo('请输入密码!')
      } else if (this.inputVal == '') {
        this.$open.openInfo('请输入验证码!')
      } else {
        if (this.inputVal.toUpperCase() === this.checkCode) {
          // this.$open.openOk('验证码正确!')
          var data = new FormData()
          data.append('email', this.email)
          data.append('pass', this.pass)
          const { data: res } = await this.$http.post('xpq/user/login', data)
          console.log(res)
          if (res.code == 0) {
            this.$open.openNo(res.msg)
            this.inputVal = "";
            this.$refs["ref_validateCode"].draw();
          } else {
            this.tzLogin(res.data)
          }
        } else {
          this.$open.openNo('验证码错误!')
          this.inputVal = "";
          this.$refs["ref_validateCode"].draw();
        }
      }
    },
    async login_qr() {    // btn qrCode Login
      // web-uuid
      const { data: res } = await this.$http.get("xpq/qr/sq")
      var string = JSON.stringify(res.data)
      console.log(string)
      this.disLogin = !this.disLogin
      if (!this.disLogin) {
        this.textLogin = "扫  码  登  录"
        // uuid -> qrcode
        if (string.length > 5) {
          this.showCodeUrl = string
          this.codeUrl = res.data.key
          // addtimer
          this.setTimer()
        }
      } else {
        this.textLogin = "用  户  登  录"
        // deltimer
        clearInterval(this.timer)
        this.timer = null
      }
    },
    async sx_qr() {       // 点击 qr 刷新
      const { data: res } = await this.$http.post(this.codeHttpUrl)
      var string = JSON.stringify(res)
      // uuid -> qrcode
      if (string.length > 5) {
        this.showCodeUrl = string
        this.codeUrl = res.key
      }
    },
    async setTimer() {    // 二维码定时器
      if (this.timer == null) {
        this.timer = setInterval(() => {
          // 循环运行
          this.getTimer()
        }, 1000)
      }
    },
    async getTimer() {     // 查看二维码是否激活
      var data = new FormData()
      data.append('key', this.codeUrl)
      console.log(this.codeUrl)
      const { data: res } = await this.$http.post("xpq/qr/login", data)
      if (res.code == 0) {
        if (res.msg.length == 1) {
          sx_qr()
        }
      } else {
        // 停止计时器
        clearInterval(this.timer)
        this.timer = null
        // 跳转界面
        this.tzLogin(res.data)
      }
    },
    tzLogin(res) {
      this.$open.openOk('欢迎回来!')
      // 将数据存储到sessionStorage中
      sessionStorage.setItem('id', res.id)
      sessionStorage.setItem('jwt', res.token)
      this.$router.push('/home')
    },
    book() {
      this.$router.push('/book')
    }
  },
  created: function () {     // 每次进入此界面,清除定时器,保证运行不受干扰
    clearInterval(this.timer)
    this.timer = null
  }
}
</script>

<style scoped>
/* 设置自适应屏幕大小 */

.login {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 100vh;
    background: linear-gradient(-135deg, #50c8c2, #4158d0);
}


/* 标题设计 */

.login-title {
    text-align: center;
    font-size: 20px;
    padding-bottom: 20px;
}


/* 标题设计 */

.login-title {
    text-align: center;
    font-size: 20px;
    padding-bottom: 20px;
}


/* 输入框设计 */

.main {
    width: 450px;
    background-color: #fff;
    padding: 30px;
    box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
}


/* 模块规格 */


/* 输入框、验证码 */

.main .input-data,
.login-yzm {
    width: 100%;
    height: 40px;
    margin: 10px;
    position: relative;
}


/* 输入框规格设计 */

.main .input-data input {
    width: 100%;
    height: 100%;
    border: none;
    border-bottom: 2px solid silver;
    font-size: 17px;
}


/* 动画效果 */

.input-data input:focus~label,
.input-data input:valid~label {
    transform: translateY(-20px);
    font-size: 15px;
    color: #4158D0;
}


/* 输入框文本提醒动画 */

.main .input-data label {
    position: absolute;
    bottom: 10px;
    left: 0;
    color: grey;
    pointer-events: none;
    transition: all 0.3s ease;
}


/* 动画设计 */

.main .input-data .underlineN {
    position: absolute;
    bottom: 0px;
    height: 2px;
    width: 100%;
}

.input-data .underlineN::before {
    /* margin-left: -50%; */
    background: #4158D0;
    position: absolute;
    content: "";
    height: 100%;
    width: 100%;
    transform: scaleX(0);
    transition: transform 0.3s ease;
}

.input-data input:focus~.underlineN:before,
.input-data input:valid~.underlineN:before {
    transform: scaleX(1);
}

.main .input-data .underlineP {
    position: absolute;
    bottom: 0px;
    height: 2px;
    width: 100%;
}

.input-data .underlineP::before {
    /* margin-left: -50%; */
    background: #4158D0;
    position: absolute;
    content: "";
    height: 100%;
    width: 100%;
    transform: scaleX(0);
    transition: transform 0.3s ease;
}

.input-data input:focus~.underlineP:before,
.input-data input:valid~.underlineP:before {
    transform: scaleX(1);
}


/* 按钮设计 */

.login-btn {
    text-align: center;
}

.login-btn button {
    background: radial-gradient(circle, rgba(247, 150, 192, 1) 0%, rgba(118, 174, 241, 1) 100%);
    border: none;
    color: #fff;
    font-family: 'Lato', sans-serif;
    border-radius: 10px;
    cursor: pointer;
    padding: 10px 30px;
    margin: 10px;
    position: relative;
    top: 20px;
}


/* 按钮触摸和移出 */

.login-btn button:hover {
    background: transparent;
    color: #76aef1;
}

.login-btn button::before,
.login-btn button::after {
    content: '';
    position: absolute;
    width: 1px;
    height: 1px;
    box-shadow: -1px -1px 20px 0px rgba(255, 255, 255, 1), -4px -4px 5px 0px rgba(255, 255, 255, 1), 10px 10px 20px 0px rgba(0, 0, 0, .4), 6px 6px 5px 0px rgba(0, 0, 0, .3);
    transition: all 0.8s ease;
    padding: 0;
}

.login-btn button::before {
    top: 0;
    right: 0;
}

.login-btn button::after {
    bottom: 0;
    left: 0;
}

.login-btn button:hover::before,
.login-btn button:hover::after {
    height: 100%;
}

.login-btn button span::before,
.login-btn button span::after {
    position: absolute;
    content: '';
    width: 0px;
    box-shadow: -1px -1px 20px 0px rgba(255, 255, 255, 1), -4px -4px 5px 0px rgba(255, 255, 255, 1), 10px 10px 20px 0px rgba(0, 0, 0, .4), 6px 6px 5px 0px rgba(0, 0, 0, .3);
    transition: all 0.8s ease;
}

.login-btn button span::before {
    top: 0;
    left: 0;
}

.login-btn button span::after {
    bottom: 0;
    right: 0;
}

.login-btn button span:hover::before,
.login-btn button span:hover::after {
    width: 100%;
}


/* 验证码 */


/* 布局 */

.login-yzm #canvas,
#yzm-text {
    float: left;
}

.login-yzm #yzm-text {
    position: relative;
    left: 25px;
    top: 10px;
}


/* 输入框样式 */

.login-yzm #yzm-input {
    position: relative;
    top: 7px;
    left: 18px;
    height: 30px;
    width: 160px;
}


/* qrCode button */

.btn_qr {
    width: 100%;
    text-align: right;
}

.login_2 {
    width: 100%;
    text-align: center;
}

.book {
  position: fixed;
  top: 50px;
  right: 0;
  margin-right: 40px;
  color: #ffffff;
  font-size: 15px;
}
</style>

3、后端(Spring Boot)

其实最优解应该是Redis+Spring Task,但这里我没打算使用Spring Task,对初学者多少有点不友好,我之后会专门出一篇专门讲Spring Task

  • 关于JWT令牌技术,博客地址:http://t.csdn.cn/fSa0y
  • 关于Redis的使用,博客地址:http://t.csdn.cn/7x6ls
@RestController
@RequestMapping("/xpq/qr")
@Slf4j
public class QrController {

    @Resource
    RedisTemplate redisTemplate;

    @Resource
    UserService userService;

    @Resource
    private JwtProperties jwtProperties;

    @GetMapping("{id}")
    public Result<QrInfoVo> qrInfo(@PathVariable Long id) {
        String key = UUID.randomUUID().toString();

        QrInfoVo qrInfoVo = new QrInfoVo();
        qrInfoVo.setId(id);
        qrInfoVo.setKey(key);
        log.info("二维码信息:{}", qrInfoVo);

        // 存入Rides中,设置过期时间为 1分钟
        redisTemplate.opsForValue().set(key, "0",1, TimeUnit.MINUTES);

        return Result.success(qrInfoVo);
    }

    @PostMapping("appLogin")
    public Result appLogin(QrInfoVo qrInfoVo) {
        log.info("获取到app用户信息:{}", qrInfoVo);

        redisTemplate.opsForValue().set(qrInfoVo.getKey(), String.valueOf(qrInfoVo.getId()),1, TimeUnit.MINUTES);
        return Result.success();
    }

    @PostMapping("/login")
    public Result<UserLoginVO> login(String key) {
        log.info("获取key:{}", key);

        String string = (String) redisTemplate.opsForValue().get(key);
        if (string.equals("0")) {
            return Result.error("");
        } else if(string.equals(null)) {
            return Result.error("0");
        } else {
            log.info("得到key:{}", key);
            Long keys = Long.valueOf(string).longValue();
            log.info("用户登录:{}", keys);
            UserInfoVo u = userService.queryById(keys);

            // 扫码成功后,生成jwt令牌
            Map<String, Object> claims = new HashMap<>();
            claims.put(JwtClaimsConstant.EMP_ID, u.getId());
            String token = JwtUtil.createJWT(
                    jwtProperties.getAdminSecretKey(),
                    jwtProperties.getAdminTtl(),
                    claims);
            log.info("jwt令牌:{}", token);

            UserLoginVO userLoginVO = UserLoginVO.builder()
                    .id(u.getId())
                    .userName(u.getName())
                    .image(u.getImage())
                    .token(token)
                    .build();

            return Result.success(userLoginVO);
        }
    }

}

三、附件

git代码地址之后会更新出来,我得确定一下项目开源机制

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

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

相关文章

Home Assistant-开源智能家居系统

Home Assistant&#xff08;以下简称HA&#xff09; 它是个开源的智能家居平台&#xff0c;一个系统平台软件&#xff0c;像TB 1.它把家中的智能家居设备整合到HA中&#xff0c;它能够接入的设备非常的多比如小米、博联、易微联、飞利浦、特斯拉…&#xff0c;也可以接入软件&…

Python远程连接Ubuntu20.4下的Mariadb数据库进行操作

文章目录 前言一、ubuntu20.4安装mariadb10.51、更换数据源2、安装mariadb3、设置密码4、设置管理用户5、设置远程登录6、修改端口 二、mariadb10.5建库建表创建数据库2.建表 三、Python代码及环境准备1、Python2、环境 四、总结五、参考资料 前言 环境&#xff1a; 1、Ubuntu2…

Chromium浏览器渗透测试工具EvilSelenium简单入门

EvilSelenium是一款基于Selenium的渗透测试工具&#xff0c;该工具基于武器化的Selenium实现其功能&#xff0c;可以帮助广大研究人员针对基于Chromium的浏览器进行安全分析和渗透测试。 功能介绍 1、通过autofill获取存储的凭证信息&#xff1b; 2、获取Cookie数据&#xf…

高考答题卡怎么被机器识别?基于OpenCV答题卡识别模拟-米尔ARM+FPGA异构开发板

本篇测评由优秀测评者“筑梦者与梦同行”提供。 01. 前言MYD-JX8MMA7SDK发布说明 根据下图文件内容可以知道myir-image-full系统支持的功能&#xff0c;其支持OpenCV&#xff0c;也就不用在格外安装相关驱动包等&#xff0c;省了很多事情。 02. MYD-JX8MMA7软件评估指南 本文…

Java中Object类常用的11个方法

Java中Object类常用的11个方法 先看下 Object 的类结构&#xff08;快捷键&#xff1a;alt7&#xff09;&#xff1a; 1. getClass 方法&#xff08;获取类的class对象。&#xff09; public final native Class<?> getClass();final 方法、获取对象的运行时 class …

学生成绩管理系统(PowerDesigner+MyEclipse+SQL Server)

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;69学生 获取完整源码源文件论文报告数据库表等 系统中用户共有管理员、教师和学生三种&#xff0c;分别对应不同的权限。 管理员 &#xff08;1&#xff09;院系的开设&#xff1b; &#xff08;2&#xff09;教师基本信息…

VScode的插件和.json文件和快捷键

文章目录 1. 插件了解插件的配置的修改Remote DevelopmentFilter LineC/C 和 C intellisense&#xff08;弃用&#xff09;cpp-check-lint 2. VScode中的.json文件2.1 tasks.jsontasks.json文件的格式tasks.json文件中任务的配置arg参数选择 案例&#xff1a; 2.2 lauch.json参…

数字逻辑复习重点总结

文章目录 前言第一章第二章第三章第四章第五章第六章第七章&#xff1a;第八章总结 前言 因为要期末考试了所以就将知识点进行了总结&#xff0c;把期末要考的知识点分章节进行划分&#xff0c;以至于我能取得一个好成绩。 第一章 进制转换 8421码、2421码、余3码、格雷码&am…

Creating Serial Numbers (C#)

此示例展示如何使用Visual C#编写的Add-ins为文件数据卡生成序列号。 注意事项&#xff1a; SOLIDWORKS PDM Professional无法强制重新加载用.NET编写的Add-ins&#xff0c;必须重新启动所有客户端计算机&#xff0c;以确保使用Add-ins的最新版本。 SOLIDWORKS PDM Professio…

购买一套WMS仓储管理系统要多少钱

随着电商行业的快速发展&#xff0c;仓储物流行业也逐渐成为了人们关注的焦点。WMS仓储管理系统作为物流管理领域的重要工具&#xff0c;在提高仓库管理效率、降低运营成本方面具有重要作用。那么&#xff0c;购买一套WMS仓储管理系统要多少钱呢&#xff1f; 首先&#xff0c;我…

Vue开发实战(03)-组件化开发

对组件功能的封装&#xff0c;可以像搭积木一样开发网页。 Vue官方的示例图对组件化开发的形象展示。左边是一个网页&#xff0c;可以按照功能模块抽象成很多组件&#xff0c;这些组件就像积木一样拼接成网页。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直…

Lecture 21 Summarisation

目录 Extractive: Single-DocExtractive: Multi-DocAbstractive: Single-DocEvaluationConclusion summarisation Distill the most important information from a text to produce shortened or abridged versionExamples outlines of a documentabstracts of a scientific ar…

MapReduce【小文件的优化-Sequence文件】

在实际开发中&#xff0c;我们肯定希望提高MapReduce的工作效率&#xff0c;其实提高MapReduce的效率&#xff0c;无非就是提高Map阶段和Reduce阶段的效率。 Map阶段优化之小文件问题 我们知道Map阶段中的MapTask个数是与InputSplit的个数有关的&#xff0c;一般一个InputSpl…

《微服务实战》 第二十八章 分布式锁框架-Redisson

前言 Redisson 在基于 NIO 的 Netty 框架上&#xff0c;充分的利⽤了 Redis 键值数据库提供的⼀系列优势&#xff0c;在Java 实⽤⼯具包中常⽤接⼝的基础上&#xff0c;为使⽤者提供了⼀系列具有分布式特性的常⽤⼯具类。使得原本作为协调单机多线程并发程序的⼯具包获得了协调…

VR全景营销颠覆传统营销模式,让商企博“出圈”

在激烈的市场竞争中&#xff0c;营销成为了商企博“出圈”的重要课题&#xff0c;随着5G的到来&#xff0c;VR全景迈入了快速发展时期&#xff0c;随着VR全景的普及应用&#xff0c;商业领域也逐渐引入了VR全景营销。 时下&#xff0c;商企的营销是越发困难&#xff0c;传统的营…

币圈下半年重点之一:以太坊坎昆升级,将带来哪些实质性利好?

近期BRC-20大火&#xff0c;主打价值存储的比特币竟然生态比以太坊还热&#xff0c;但要论生态&#xff0c;以太坊才是真正的王者&#xff0c;因为其正在悄悄酝酿下一个重大升级——坎昆&#xff08;Dencun&#xff09;升级。 最新消息&#xff0c;以太坊开发者已经就Dencun升级…

【MySQL高级篇笔记-数据库的设计规范(中) 】

此笔记为尚硅谷MySQL高级篇部分内容 目录 一、为什么要数据库设计 二、范式 1、范式简介 2、范式都包括哪些 3、键和相关属性的概念 4、第一范式(1st NF) 5、第二范式(2nd NF) 6、第三范式(3rd NF) 7、小结 三、反范式化 1、概述 2、 应用举例 3、反范式的新问…

逆向分析高薪就业:学习Android逆向开发,拥抱行业机会!

简述 Android 逆向开发是指利用各种技术手段对安卓应用程序进行逆向分析和研究&#xff0c;以了解应用程序的内部机制&#xff0c;发现应用程序中的漏洞、脆弱性或者安全问题&#xff0c;并提供相关的解决方案。逆向开发技术可以帮助开发人员更好地了解应用程序的构成、运行机…

Django实现接口自动化平台(六)httprunner(2.x)基本使用【持续更新中】

上一章&#xff1a; Django实现接口自动化平台&#xff08;五&#xff09;httprunner&#xff08;2.x&#xff09;基本使用【持续更新中】_做测试的喵酱的博客-CSDN博客 下一章&#xff1a; 一、 api 文件夹&#xff08;没有任何数据依赖的场景&#xff09; api 文件夹&…

一键生成代码

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…