node服务实现微信扫码登录和注册

# 实现思路

微信扫码登录和注册,这个功能需要微信提供接口,在我调研之后,发现需要微信的企业用户,才有开通这个接口的资格。所以这里我采用了曲线的方式,易登,这个平台提供了微信扫码的服务的回调的转发,所以我才可以实现这个功能

# 2024 年 06 月更新

易登也被微信禁止这样转发了,所以这个功能现在下线了

# 准备环节

这里我准备了一张表 qr_code_table 用来记录每次二维码生成的唯一 code,和这个 code 的当前状态,根据这个 code 和状态,从而接口进行不同的操作

# 生成二维码

调用微信(易登)的生成二维码的接口,拿到二维码后,插入一条数据 { code, status: 'init' } 到表 qr_code_table

// 生成 二维码 的接口
router.post('/generateQRCode', async (req, res) => {
  try {
    const ydUrl = `https://yd.jylt.cc/api/face/tempUserId?secret=102e2171&width=400`;
    // 生成二维码
    const { success, data, msg } = await fetchRequest(ydUrl, 'get', {});
    if (success) {
      // 在 qr_code_table 新增一条数据
      const qr_code = data.tempUserId;
      const insertSql = `insert into ${DB_NAME}.qr_code_table (qr_code, status) values ("${qr_code}", "init") `;
      await runSql(insertSql);
      res.send({ success: true, data });
    } else {
      res.send({ error: msg });
    }
  } catch (e) {
    console.error('e', e);
    res.send({ error: '二维码生成失败' });
  }
});

# 用户取消扫码

这个简单,记得要把表 qr_code_table 中对应的 code 的状态 status 置为 canceled

// 手动取消扫描二维码
router.post('/cancelReadQRCode', async (req, res) => {
  const { qrCode } = req.body;
  try {
    const updateQrCodeSql = `UPDATE ${DB_NAME}.qr_code_table SET status='canceled' WHERE qr_code="${qrCode}"`;
    await runSql(updateQrCodeSql);
    res.send({ success: true });
  } catch (e) {
    console.error('e', e);
    res.send({ error: '' });
  }
});

# 持续问询 前端的登录二维码 的 状态

正常的扫码流程,这个过程必定是持续的,所以这里采用长连接的方式,这里是前端持续的查询这个接口,所以这个接口需要持续提供 60 秒的数据传输

  • 什么时候停止呢? code 二维码的状态发生了变更,就会停止

  • 怎么停止呢? 其他接口更改了表 qr_code_table 中对应的 code 的状态 status,这个时候就会进行状态判断,从而告诉前端当前扫码登录进行的状态

  • 用户点击了确认登录:从用户表内取出当前的微信扫码登录的用户信息,生成 jwt,然后一起返回给前端,前端会拿着用户信息和 jwt 调用登录的接口,从而完成登录

  • 超时、取消等其他状态:修改表 qr_code_table 中对应的 code 的状态 status,结束长连接

// SSE 长连接接口 持续问询 前端的登录二维码 的 状态
router.get('/checkQRCodeStatus/:qrCode', async (req, res) => {
  const { qrCode } = req.params;
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  const checkStatus = async () => {
    try {
      const querySql = `select status, user_id from ${DB_NAME}.qr_code_table where qr_code = "${qrCode}"`;
      const result = await runSql(querySql);

      const { status, user_id } = result[0];

      if (result.length > 0) {
        // 确认登录
        if (status === 'confirmed') {
          const queryUserSql = `select user_name, user_type, user_id, deadline_time, score from ${DB_NAME}.user_table where user_id="${user_id}" `;
          const userInfo = await runSql(queryUserSql);
          const jwt = generateJWToken(userInfo[0]);
          res.write(
            `data: ${JSON.stringify({
              status,
              token: jwt,
              userInfo: userInfo[0],
            })} \n\n`,
          );
          res.end(); // 断开连接
        } else if (status === 'success') {
          // 扫描成功
          setTimeout(checkStatus, 1000); // 每1秒检查一次状态,不断开连接
        } else if (status === 'failed') {
          // 扫描失败
          res.write(`data: ${JSON.stringify({ status })} \n\n`);
          res.end(); // 断开连接
        } else if (status === 'canceled') {
          // 取消登录
          res.write(`data: ${JSON.stringify({ status })} \n\n`);
          res.end(); // 断开连接
        } else if (status === 'init') {
          // 未被扫描
          setTimeout(checkStatus, 1000); // 每1秒检查一次状态
        } else if (status === 'expired') {
          // 超时间了,不在轮询了
          res.write(`data: ${JSON.stringify({ status })} \n\n`);
          res.end(); // 断开连接
        } else {
          // 其他情况 也返回出去,但是断开连接
          res.write(`data: ${JSON.stringify({ status })} \n\n`);
          res.end(); // 断开连接
        }
      } else {
        res.end();
      }
    } catch (error) {
      console.error(error);
      res.end();
    }
  };

  checkStatus();

  // 60 秒后关闭连接
  setTimeout(() => {
    res.write(`data: ${JSON.stringify({ status: 'expired' })} \n\n`);
    res.end();
  }, 60000);
});

# 用户扫码的回调

在这个接口里,记录了用户扫码的各种回调

  • 取消扫码,就直接更新表 qr_code_table 中对应的 code 的状态 status 为 'canceled' 即可,后续的操作在持续问询的长连接接口里面会进行处理的

  • 扫码失败,就直接更新表 qr_code_table 中对应的 code 的状态 status 为 'failed' 即可,后续的操作在持续问询的长连接接口里面会进行处理的

  • 扫码成功,需要根据用户是否是新注册的用户来区分,进行不同的工作

    • 用户是已经注册用户,更新表 qr_code_table 中对应的 code 的状态 status 为 'confirmed' ,后续的操作在持续问询的长连接接口里面会进行处理的

    • 用户不是已经注册用户,用当前的微信信息注册一个用户,然后再更新表 qr_code_table 中对应的 code 的状态 status 为 'confirmed' ,后续的操作在持续问询的长连接接口里面会进行处理的;从而实现了扫码注册并登录的操作

// 扫码的回调
router.get('/qrCodeCallback', async (req, res) => {
  const {
    scanSuccess,
    tempUserId: qr_code,
    cancelLogin,
    wxMaUserInfo: wxMaUserInfoInParam,
  } = req.query;
  try {
    // 判断是否取消登录
    if (cancelLogin) {
      // 扫码失败,状态更新
      const updateQrCodeSql = `UPDATE ${DB_NAME}.qr_code_table SET status='canceled' WHERE qr_code="${qr_code}"`;
      await runSql(updateQrCodeSql);
      res.send({ code: 1, msg: '取消登录' });
      return;
    }
    if (!scanSuccess) {
      // 扫码失败,状态更新
      const updateQrCodeSql = `UPDATE ${DB_NAME}.qr_code_table SET status='failed' WHERE qr_code="${qr_code}"`;
      await runSql(updateQrCodeSql);
      res.send({ code: 1, msg: '扫码失败' });
      return;
    }
    if (scanSuccess) {
      // 扫码成功
      const wxMaUserInfo = JSON.parse(wxMaUserInfoInParam) || {};
      const { openId, nickName } = wxMaUserInfo;
      // 先根据 当前微信用户信息查询用户表,有没有这个人,如果有,就登录;没有就注册并登录
      const queryUserSql = `select * from ${DB_NAME}.user_table where wechat_id='${openId}' `;
      const userList = await runSql(queryUserSql);

      if (userList.length > 0) {
        // 更新二维码状态和用户信息,说明是已有用户,直接登录即可
        const updateQrCodeSql = `UPDATE ${DB_NAME}.qr_code_table SET status='confirmed', user_id="${userList[0].user_id}" WHERE qr_code="${qr_code}"`;
        await runSql(updateQrCodeSql);
        res.send({ code: 0, msg: '登录成功' });
      } else {
        // 用当前微信信息注册一个用户,虽然是使用者 但是积分只有0
        const values = {
          user_name: nickName,
          user_id: openId,
          password: openId,
          wechat_id: openId,
          user_type: 'user',
          disabled: 'false',
          create_time: dayjs().format('YYYYMMDD'),
          deadline_time: '20990101',
          score: '0',
        };
        const fields = Object.keys(values).join(',');
        const fieldsAddValue = Object.values(values)
          .map((v) => `'${v || ''}'`)
          .join(',');
        const addUserSql = `insert into ${DB_NAME}.user_table (${fields}) values (${fieldsAddValue}) `;
        await runSql(addUserSql);
        // 注册成功之后 登录
        const updateQrCodeSql = `UPDATE ${DB_NAME}.qr_code_table SET status='confirmed', user_id="${openId}" WHERE qr_code="${qr_code}"`;
        await runSql(updateQrCodeSql);
        res.send({ code: 0, msg: '注册并登录成功' });
      }
    }
    res.send({ code: 1, msg: '未知错误' });
  } catch (e) {
    console.error(e);
    res.send({ code: 1, msg: e });
  }
});

# 用户扫码绑定微信

这个接口整体类似于用户扫码登录的操作,唯一的区别在于不再与登录产生关联,直接扫码然后将微信 id 和 userid 关联在一起就可以了,但是易登被禁用了,这里就没有再继续开发了