Skip to content

Nest 二维码扫描

场景

  • PC 端生成二维码页面,移动端扫描二维码,PC 端获取扫描结果

流程如下

前端

对应两个页面

  • 一个是生成二维码页面

  • 一个是扫描二维码后跳转的页面

后端

对应五个接口

  • 生成二维码接口
  • 轮询查询二维码结果
  • 扫描二维码接口 等待结果
  • 扫描二维码后,点击了确认
  • 扫描二维码后,点击了取消

后端

安装包

bash
npm install qrcode @types/qrcode
npm install base62

控制器

ts
import { Controller, Get, Query } from "@nestjs/common";
import { QrcodeService } from "./qrcode.service";
import * as qrcode from "qrcode";
const base62 = require("base62/lib/ascii");
@Controller("qrcode")
export class QrcodeController {
  constructor(private readonly qrcodeService: QrcodeService) {}

  // 生成一个不会重复的字符串
  async setduanliancode(len) {
    console.log("进来了啊");
    console.log(len);
    let str = "";
    for (let i = 0; i < len; i++) {
      const num = Math.floor(Math.random() * 62);
      str += base62.encode(num);
    }
    return str;
  }
  // 生成二维码
  @Get("generate")
  async findAll() {
    const url = "https://www.uiyin.com"; // 你自己的跳转的网址
    // 生成一个不会重复的字符串
    let code = "";
    for (let i = 0; i < url.length; i++) {
      const num = Math.floor(Math.random() * 62);
      code += base62.encode(num);
    }
    console.log(code);
    const options: qrcode.QRCodeToDataURLOptions = {
      width: 300, // 二维码宽度
      margin: 2, // 二维码边距
      color: {
        dark: "#000000", // 二维码点的颜色
        light: "#FFFFFF", // 背景颜色
      },
      // errorCorrectionLevel: 'H', // 错误纠正级别 'L', 'M', 'Q', 'H'
    };
    // 结果
    const qrcodedata = await this.qrcodeService.setqrcode(url, code, options);

    return {
      data: {
        id: code, // 通过id 来轮询查询二维码状态
        img: qrcodedata,
      },
    };
  }

  // 增加一个检查接口,用于检查二维码状态
  @Get("check")
  async check(@Query() query: any) {
    console.log(query);
    // 我这里就没有验证了,你可以根据你的需求来验证
    const result = await this.qrcodeService.checkqrcode(query.id);
    console.log(result);
    return {
      data: {
        ...result, // 返回二维码状态
      },
    };
  }
  // 二维码确认
  // 在手机页面点击确认按钮 一定要传入qrcode_id 和token
  // 我这里为了测试就不写token了
  @Get("confirm")
  async confirm(@Query() query: any) {
    // 我这里就没有验证了,你可以根据你的需求来验证
    const result: any = await this.qrcodeService.confirmqrcode(
      query.id,
      query.token
    );
    return {
      data: {
        result, // 返回二维码状态
      },
    };
  }
  // 二维码取消
  // 在手机页面点击取消按钮 一定要传入qrcode_id 和token
  // 我这里为了测试就不写token了
  @Get("cancle")
  async cancle(@Query() query: any) {
    // 我这里就没有验证了,你可以根据你的需求来验证
    const result = await this.qrcodeService.cancelqrcode(query.id, query.token);
    console.log("控制器");
    console.log(result);
    return {
      data: {
        result, // 返回二维码状态
      },
    };
  }

  // 用户扫码后,跳转到页面仅仅跳转没点击确认和取消
  // 进入手机页面 一定要传入qrcode_id 和token
  // 我这里为了测试就不写token了
  @Get("waiting")
  async waiting(@Query() query: any) {
    // 我这里就没有验证了,你可以根据你的需求来验证
    const result = await this.qrcodeService.waitingqrcode(
      query.id,
      query.token
    );

    return {
      data: {
        result, // 返回二维码状态
      },
    };
  }
}

服务

ts
import { Inject, Injectable } from "@nestjs/common";
import * as qrcode from "qrcode";
import { RedisService } from "../redis/redis.service";
enum qrcodestatus {
  NOSCAN = "noscan", // 未扫描
  SCANWAITCONFIRM = "scanwaitconfirm", // 等待扫描
  SCANCONFIRM = "scanconfirm", // 已扫描
  SCANCANCEL = "scancancel", // 用户取消扫描
  EXPIRED = "expired", // 已过期
}
@Injectable()
export class QrcodeService {
  @Inject()
  private readonly redisService: RedisService;

  // 生成二维码
  async setqrcode(
    url: string,
    code: string,
    options?: qrcode.QRCodeToDataURLOptions
  ) {
    // 使用 toDataURL 生成 base64 格式的图片,然后转换为 Buffer
    // toDataURL 的 options 类型就是 QRCode.QRCodeToDataURLOptions
    const dataUrl = await qrcode.toDataURL(url, options);
    // 保存redis 状态
    let redisdata = { status: qrcodestatus.NOSCAN, token: "" };
    this.redisService.sethash(code, redisdata);
    return dataUrl;
  }

  // 检查二维码
  async checkqrcode(text: string) {
    console.log(text);
    const status: any = await this.redisService.gethash(text);
    if (status === null) {
      return {
        status: "初始化",
        token: "",
      };
    } else {
      if (status.status === qrcodestatus.SCANCONFIRM) {
        return {
          status: status.status,
          token: status.token,
        };
      } else {
        return {
          status: status.status,
          token: "",
        };
      }
    }
  }

  // 确认二维码
  async confirmqrcode(qrcode_id: string, token: string) {
    // 这里获取到token,知道是谁了。然后进行业务处理
    // 我这里为了业务测试 就不写token了
    console.log(token);
    const status: any = await this.redisService.gethash(qrcode_id);
    if (!status) {
      return "二维码有问题";
    }
    // 设置二维码状态
    let redisdata = {
      status: qrcodestatus.SCANCONFIRM,
      token: token,
    };
    this.redisService.sethash(qrcode_id, redisdata);
    return "success";
  }

  // 取消二维码
  async cancelqrcode(qrcode_id: string, token: string) {
    // 这里获取到token,知道是谁了。然后进行业务处理
    // 我这里为了业务测试 就不写token了
    console.log(token);
    const status: any = await this.redisService.gethash(qrcode_id);
    if (!status) {
      return "二维码有问题";
    }
    // 设置二维码状态
    let redisdata = {
      status: qrcodestatus.SCANCANCEL,
      token: token,
    };
    this.redisService.sethash(qrcode_id, redisdata);
    return "cancle";
  }
  // 等待确认二维码
  async waitingqrcode(qrcode_id: string, token: string) {
    // 这里获取到token,知道是谁了。然后进行业务处理
    // 我这里为了业务测试 就不写token了
    console.log(token);
    const status: any = await this.redisService.gethash(qrcode_id);
    if (!status) {
      return "二维码有问题";
    }
    // 设置二维码状态
    // 设置二维码状态
    let redisdata = {
      status: qrcodestatus.SCANWAITCONFIRM,
      token: token,
    };
    this.redisService.sethash(qrcode_id, redisdata);
    return "waiting";
  }
}

前端

  • 扫描后的页面我就不写了.调用那三个接口即可

  • 这个我写的是获取二维码的页面

  1. 获取二维码

  2. 轮询查询二维码状态

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>扫码登录</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://unpkg.com/axios@1.5.0/dist/axios.min.js"></script>
  </head>
  <body>
    <img id="img" src="" alt="" />
    <div id="info"></div>
    <script>
      let id = "";
      axios.get("http://localhost:5000/qrcode/generate").then((res) => {
        console.log(res.data.data);
        document.getElementById("img").src = res.data.data.img;
        id = res.data.data.id;
      });
      // 循环查询状态
      function queryStatus(id) {
        axios.get("http://localhost:5000/qrcode/check?id=" + id).then((res) => {
          const result = res.data.data;
          let content = "";
          switch (result.status) {
            case "noscan":
              content = "未扫码";
              break;
            case "scanwaitconfirm":
              content = "已扫码,等待确认";
              break;
            case "scanconfirm":
              content = "已确认";
              console.log("获取到返回的token");
              console.log(result.token);
              console.log("就应该跳转到登录页");
              break;
            case "scancancel":
              content = "已取消";
              break;
          }
          document.getElementById("info").textContent = content;
        });
      }

      setInterval(() => {
        queryStatus(id);
      }, 3000);
    </script>
  </body>
</html>