实现 扫一扫拥有自己的微信机器人

577 阅读2分钟

大体思路:

  • web 登录页从服务端获取一个带有 scene 的小程序码。
  • 微信扫码,进入小程序,获得唯一参数 scene,小程序进行授权获取用户信息(头像、昵称)。
  • 授权登录,把 scene, opendid 和用户信息一起保存到数据库。
    • 先根据 openid 查询用户列表,如果存在则更新用户信息 和 scene, 如果不存在则添加用户。
  • 登录页轮询或 websocket 请求后端获取扫码状态,扫码成功后登录进去。
    • 带着 scene 值轮询,如果查询到用户信息,则根据用户信息,secret, 生成 token 返回给 web登录页。
    • web 登录页,拿到 token, 保存 token, 并跳转到首页, 以后每次请求请求头带上 token。
  • 获取机器人登录的二维码,扫码登录
server 端的代码

token.js

async function getAccessToken() {
  const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${secret}`;
  try {
    const currentTime = Date.now();
    var contentText = fs.readFileSync(
      path.resolve(__dirname, "./access_token.json"),
      "utf-8"
    );
    accessTokenJson = JSON.parse(contentText);
    if (
      !accessTokenJson.access_token ||
      accessTokenJson.expires_time < currentTime
    ) {
    // token 过期了,则重新请求生成
      let { access_token, expires_in } = await superagent.req({
        url,
        method: "GET",
      });

      fs.writeFileSync(
        path.resolve(__dirname, "./access_token.json"),
        JSON.stringify({
          access_token,
          expires_time: Date.now() + (expires_in - 200) * 1000,
        }),
        "utf-8"
      );

      return access_token;
    } else {
    // token 未过期,直接用
      return accessTokenJson.access_token;
    }
  } catch (err) {
    console.log("获取接口失败", err);
  }
}

qrCode.js

function getQrCode() {
  return new Promise(async function (resolve, reject) {
    const access_token = await getAccessToken();
    const createQrUrl = `https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${access_token}`;
    const scene = Date.now();
    ajax
      .post(createQrUrl)
      .send({
        path: "page/index/index", // 小程序扫码后展示的页面
        width: 430,
        scene, // 唯一性的参数
        is_hyaline: true,
      })
      .set("X-API-Key", "foobar")
      .set("accept", "json")
      .end(function (err, res) {
        var base64 = res.body.toString("base64");
        resolve({
          base64,
          scene,
        });
      });
  });
  // return qrData;
}

server.js

// 通过该请求,登录页获取小程序码
router.post("/createQrCode", async (ctx, next) => {
  const { base64, scene } = await getQrCode();
  const base64Str = `data:image/png;base64,${base64}`;
  ctx.body = {
    code: 20000,
    base64Str,
    scene,
  };
});
小程序端代码

index.js

 onLogin(scene) {
    wx.login({
      success: (res) => {
        if (res.code) {
          wx.request({
            url: "https://www.moluoyingxiong.tech/bot-server/getOpenId",
            data: {
              code: res.code,
            },
            success: (user) => {
              const { openid } = user.data;
              const alreadyInfo = this.data;
              this.setData({
                userInfo: {
                  ...alreadyInfo,
                  openid,
                  scene,
                },
              });
            },
          });
        } else {
          console.log("登录失败!" + res.errMsg);
        }
      },
    });
  },
  
  saveUserInfo(userInfo) {
    wx.request({
      url: "https://www.moluoyingxiong.tech/bot-server/saveUserInfo",
      method: "post",
      data: userInfo,
      success(res) {},
    });
  },
  
    getUserProfile() {
    // 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认,开发者妥善保管用户快速填写的头像昵称,避免重复弹窗
    wx.getUserProfile({
      desc: "展示用户信息", // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
      success: (res) => {
        const { userInfo } = res;
        const alreadyInfo = this.data.userInfo;
        this.setData({
          avatarUrl: userInfo.avatarUrl,
          userInfo: {
            ...alreadyInfo,
            ...userInfo,
          },
        });
        this.saveUserInfo({
          userInfo: { ...alreadyInfo, ...userInfo },
        });
      },
      fail(e) {
        console.log(e, "======");
      },
    });
  },

web 端代码 完整代码

login.vue


mounted() {
    this.getQrcode(); // 登录页初始化出小程序码
},
methods: {
    getQrcode() {
      getQrcode().then((res) => {
        const { base64Str, scene } = res;
        this.qrcode = base64Str;
        this.checkIsScan(scene); // 获取小程序码后开始轮询检查是否扫码
      });
    },
    checkIsScan(scene) {
      const startTime = Date.now();
      if (this.hasScan) {
        clearInterval(this.timer);
        this.timer = null;
      } else {
        this.timer = setInterval(() => {
          checkScene({ scene }).then((res) => {
            const curTime = Date.now();
            if (curTime - startTime > 30 * 1000) { // 半分钟后停止检查
              clearInterval(this.timer);
              this.timer = null;
            }
            const { hasScan, token } = res;
            if (hasScan) { // 已经扫码,则保存 token ,跳转到首页或 redirect 页
              clearInterval(this.timer);
              this.timer = null;
              this.$store.commit("user/SET_TOKEN", token); 
              setToken(token);
              this.$router.push({ path: this.redirect || "/" });
            }
            this.hasScan = hasScan;
          });
        }, 3000);
      }
    },
}

微信小助手 - 娜美 这个项目可以在终端显示登陆二维码,为了实现多个小助手,其实就是在 server 端生成多个机器人实例。

bot.js

function initBot() {
  return new Promise(function (resolve) {
    const bot = new Wechaty({
      name: "nami",
    });
    bot.on("scan", (qrcode, status) => {
      // require('qrcode-terminal').generate(qrcode); // 在console端显示二维码
      const qrcodeImageUrl = [
        "https://api.qrserver.com/v1/create-qr-code/?data=",
        encodeURIComponent(qrcode),
      ].join("");
      resolve({ qrcodeImageUrl }); // 把二维码返回给前端,直接扫描登录
    });
    bot.on("login", (user) => {
      onLogin(user, bot);
    });
    bot.on("logout", () => {
      bot.logout()
      bot.stop()
    });
    bot.on("message", (msg) => {
      onMessage(msg, bot);
    });
    bot
      .start()
      .then(() => {
        console.log("开始登陆微信");
      })
      .catch((e) => console.error(e));
  });
}

server.js

// koa server
const Koa = require("koa");
const Router = require("@koa/router");
const app = new Koa();
const router = new Router();

router.get("/bot/generate", async (ctx, next) => {
  deleteMemory();
  const { qrcodeImageUrl } = await bot.botInit();
  ctx.body = {
    code: 20000,
    data: { qrcodeImageUrl }, // 生成二维码,扫码登录机器人
  };
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(3000, () => {
  console.log("listening localhost:3000");
});