nodejs從0到1搭建後臺服務

来源:https://www.cnblogs.com/loveverse/archive/2023/08/28/17661367.html
-Advertisement-
Play Games

# 項目主體搭建 - 前端:`vue3`、`element-plus`、`ts`、`axios`、`vue-router`、`pinia` - 後端:`nodejs`、`koa`、`koa-router`、`koa-body`、`jsonwebtoken` - 部署:`nginx`、`pm2`、`x ...


項目主體搭建

  • 前端:vue3element-plustsaxiosvue-routerpinia
  • 後端:nodejskoakoa-routerkoa-bodyjsonwebtoken
  • 部署:nginxpm2xshell、騰訊雲自帶寶塔面板
  • 資料庫:mysqlredis
  • 開發軟體:vs codeAnother Redis Desktop ManagerNavicat Premium 15

後端主要使用的依賴包

  • dotenv: 將環境變數中的變數從 .env 文件載入到 process.env
  • jsonwebtoken: 頒發token,不會緩存到mysqlredis
  • koa: 快速搭建後臺服務的框架
  • koa-body: 解析前端傳來的參數,並將參數掛到ctx.request.body
  • koa-router: 路由中間件,處理不同url路徑的請求
  • koa2-cors: 處理跨域請求的中間件
  • mysql2: 在nodejs中連接和操作mysql資料庫,mysql也可以,不過要自己封裝連接資料庫
  • redis: 在nodejs中操作redis的庫,通常用作持久化token、點贊等功能
  • sequelize: 基於promise的orm(對象關係映射)庫,不用寫sql語句,更方便的操作資料庫
  • nodemon: 自動重啟服務
  • sequelize-automete: 自動化為sequelize生成模型

文件劃分

image.png

常量文件配置

  1. 創建.env.developmentconfig文件夾,配置如下
# 資料庫ip地址
APP_HOST = 1.15.42.9
# 服務監聽埠
APP_PORT = 40001
# 資料庫名
APP_DATA_BASE = test
# 用戶名
APP_USERNAME = test
# 密碼
APP_PASSWORD = 123456
# redis地址
APP_REDIS_HOST = 1.15.42.9
# redis埠
APP_REDIS_PORT = 6379
# redis密碼
APP_REDIS_PASSWORD = 123456
# redis倉庫
APP_REDIS_DB = 15
# websocket尾碼
APP_BASE_PATH = /
# token標識
APP_JWT_SECRET = LOVE-TOKEN
# 保存文件的絕對路徑
APP_FILE_PATH = ""
# 網路url地址
APP_NETWORK_PATH = blob:http://192.168.10.20:40001/

然後在config文件中將.env的配置暴露出去

const dotenv = require("dotenv");
const path = process.env.NODE_ENV
  ? ".env.development"
  : ".env.production.local";
dotenv.config({ path });

module.exports = process.env;

2.在最外層創建app.js文件

const Koa = require("koa");
const http = require("http");
const cors = require("koa2-cors");
const WebSocket = require("ws");
const { koaBody } = require("koa-body");
const { APP_PORT, APP_BASE_PATH } = require("./src/config/index");
const router = require("./src/router/index");
const seq = require("./src/mysql/sequelize");
const PersonModel = require("./src/models/person");
const Mperson = PersonModel(seq);

// 創建一個Koa對象
const app = new Koa();
const server = http.createServer(app.callback());
// 同時需要在nginx配置/ws
const wss = new WebSocket.Server({ server, path: APP_BASE_PATH }); // 同一埠監聽不同的服務
// 使用了代理
app.proxy = true;
// 處理跨域
app.use(cors());
// 解析請求體(也可以使用koa-body)
app.use(
  koaBody({
    multipart: true,
    // textLimit: "1mb",  // 限制text body的大小,預設56kb
    formLimit: "10mb", // 限製表單請求體的大小,預設56kb,前端報錯413
    // encoding: "gzip",    // 前端報415
    formidable: {
      // uploadDir: path.join(__dirname, "./static/"), // 設置文件上傳目錄
      keepExtensions: true, // 保持文件的尾碼
      // 最大文件上傳大小為512MB(如果使用反向代理nginx,需要設置client_max_body_size)
      maxFieldsSize: 512 * 1024 * 1024,
    },
  })
);
app.use(router.routes());

wss.on("connection", function (ws) {
  let messageIndex = 0;
  ws.on("message", async function (data, isBinary) {
    console.log(data);
    const message = isBinary ? data : data.toString();
    if (JSON.parse(message).type !== "personData") {
      return;
    }
    const result = await Mperson.findAll();
    wss.clients.forEach((client) => {
      messageIndex++;
      client.send(JSON.stringify(result));
    });
  });
  ws.onmessage = (msg) => {
    ws.send(JSON.stringify({ isUpdate: false, message: "pong" }));
  };
  ws.onclose = () => {
    console.log("服務連接關閉");
  };
  ws.send(JSON.stringify({ isUpdate: false, message: "首次建立連接" }));
});

server.listen(APP_PORT, () => {
  const host = process.env.APP_REDIS_HOST;
  const port = process.env.APP_PORT;
  console.log(
    `環境:${
      process.env.NODE_ENV ? "開發環境" : "生產環境"
    },伺服器地址:http://${host}:${port}/findExcerpt`
  );
});
module.exports = server;

這裡可以先不引入websocketMperson,這是後續發佈內容時才會用到。

註:app.use(cors())必須在app.use(router.routes())之前,不然訪問路由時會顯示跨域。

3.在package.json中添加命令"dev": "set NODE_ENV=development && nodemon app.js",
然後就可以直接運行npm run dev啟動服務了

mysql創建資料庫建表

在伺服器上開放mysql3306,還有接下來使用到的redis6379
使用root連接資料庫,可以看到所有的資料庫。

註:sequelize6版本最低支持mysql5.7,雖然不會報錯,但是有提示

image.png

mysql預設情況下不允許root直接連接,需要手動放開

  • 進入mysql:mysql -uroot -p
  • 使用mysql:use mysql;
  • 授權給所有ip:GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456(密碼)' WITH GRANT OPTION;
  • 刷新許可權:FLUSH PRIVILEGES;

redis使用密碼遠程連接後,就不需要在本地安裝redis

redis預設也是不允許遠程連接,需要手動放開。

  1. 使用find / -name redis.conf先找到redis配置文件

    • bind 127.0.0.1修改為bind 0.0.0.0;
    • 設置protected-mode no;
    • 設置密碼requirepass 123456密碼123456替換為自己的。
  2. 進入src使用./redis-server ../redis.conf重新啟動

使用sequelize-automate自動生成表模型

創建文件sequelize-automate.config.js,然後在package.json增加一條命令"automate": "sequelize-automate -c './sequelize-automate.config.js'",使用npm run automate自動生成表模型。

註:建議自己維護createdAtupdatedAt,因為在Navicat Premium 15上創建表後,自動生成的模型雖然會自動增加createdAtupdatedAt,但是不會同步到資料庫上,需要刪除資料庫中的表,然後再重新啟動服務才會同步,但是當id為主鍵且不為null時,模板會生成defaultValuenull,在個別表中會報錯

const Automate = require("sequelize-automate");
const dbOptions = {
  database: "test",
  username: "test",
  password: "123456",
  dialect: "mysql",
  host: "1.15.42.9",
  port: 3306,
  logging: false,
  define: {
    underscored: false,
    freezeTableName: false,
    charset: "utf8mb4",
    timezone: "+00:00",
    dialectOptions: {
      collate: "utf8_general_ci",
    },
    timestamps: true, // 自動創建createAt和updateAt
  },
};
const options = {
  type: "js",
  dir: "./src/models",
  camelCase: true,
};
const automate = new Automate(dbOptions, options);
(async function main() {
  const code = await automate.run();
  console.log(code);
})();

連接mysql和redis

mysql連接,timezone預設是世界時區, "+08:00"指標準時間加8個小時,也就是北京時間

const { Sequelize } = require("sequelize");
const { APP_DATA_BASE, APP_USERNAME, APP_PASSWORD, APP_DATA_HOST } = require("../config/index");
const seq = new Sequelize(APP_DATA_BASE, APP_USERNAME, APP_PASSWORD, {
  host: APP_DATA_HOST,
  dialect: "mysql",
  define: {
    timestamps: true,
  },
  timezone: "+08:00"
});
seq
  .authenticate()
  .then(() => {
    console.log("資料庫連接成功");
  })
  .catch((err) => {
    console.log("資料庫連接失敗", err);
  });

seq.sync();
// 強制同步資料庫,會先刪除表,然後創建 seq.sync({ force: true });
module.exports = seq;

redis連接,database不同環境使用不同的分片

const Redis = require("redis");
const {
  APP_REDIS_HOST,
  APP_REDIS_PORT,
  APP_REDIS_PASSWORD,
  APP_REDIS_DB
} = require("../config");

const client = Redis.createClient({
  url: "redis://" + APP_REDIS_HOST + ":" + APP_REDIS_PORT,
  host: APP_REDIS_HOST,
  port: APP_REDIS_PORT,
  password: APP_REDIS_PASSWORD,
  database: APP_REDIS_DB,
});
client.connect();
client.on("connect", () => {
  console.log("Redis連接成功!");
});
// 連接錯誤處理
client.on("error", (err) => {
  console.error(err);
});

// client.set("age", 1);

module.exports = client;

一切都沒問題後,現在開始編寫路由代碼,也就是一個個介面

編寫登錄註冊

controllers新建user.js模塊,具體邏輯:登錄和註冊是同一個介面,前端提交時,會先判斷這個用戶是否存在,不存在會將傳來的密碼進行加密,然後將用戶信息存入到資料庫,同時使jsonwebtoken頒發token,token本身是無狀態的,也就是說,在時間到期之前不會銷毀!這裡可以在服務端維護一個令牌黑名單,用於退出登錄。

const response = require("../utils/resData");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const { APP_JWT_SECRET } = require("../config/index");
const seq = require("../mysql/sequelize");
const UserModel = require("../models/user");
const Muser = UserModel(seq);
// 類定義
class User {
  constructor() {}
  // 註冊用戶
  async register(ctx, next) {
    try {
      const { userName: user_name, password: pass_word } = ctx.request.body;
      if (!user_name || !pass_word) {
        ctx.body = response.ERROR("userNotNull");
        return;
      }
      // 判斷用戶是否存在
      const isExist = await Muser.findOne({
        where: {
          user_name: user_name,
        },
      });
      if (isExist) {
        const res = await Muser.findOne({
          where: {
            user_name: user_name,
          },
        });
        // 密碼是否正確
        if (bcrypt.compareSync(pass_word, res.dataValues.password)) {
          // 登錄成功
          const { password, ...data } = res.dataValues;
          ctx.body = response.SUCCESS("userLogin", {
            token: jwt.sign(data, APP_JWT_SECRET, { expiresIn: "30d" }),
            userInfo: res.dataValues,
          });
        } else {
          ctx.body = response.ERROR("userAlreadyExist");
        }
      } else {
        // 加密
        const salt = bcrypt.genSaltSync(10);
        // hash保存的是 密文
        const hash = bcrypt.hashSync(pass_word, salt);
        const userInfo = await Muser.create({ user_name, password: hash });
        const { password, ...data } = userInfo.dataValues;
        ctx.body = response.SUCCESS("userRegister", {
          token: jwt.sign(data, APP_JWT_SECRET, { expiresIn: "30d" }),
          userInfo,
        });
      }
    } catch (error) {
      console.error(error);
      ctx.body = response.SERVER_ERROR();
    }
  }
}

module.exports = new User();

增加校驗用戶是否登錄的中間件

middleware創建index.js,將用戶信息掛載到ctx.state.user上,方便後續別的地方需要用到用戶信息

const jwt = require("jsonwebtoken");
const { APP_JWT_SECRET } = require("../config/index");
const response = require("../utils/resData");

// 上傳文件時,如果存在token,將token添加到state,方便後面使用,沒有或者失效,則返回null
const auth = async (ctx, next) => {
  // 會返回小寫secret
  const token = ctx.request.header["love-token"];
  if (token) {
    try {
      const user = jwt.verify(token, APP_JWT_SECRET);
      // 在已經頒發token介面上面添加user對象,便於使用
      ctx.state.user = user;
    } catch (error) {
      ctx.state.user = null;
      console.log(error.name);
      if (error.name === "TokenExpiredError") {
        ctx.body = response.ERROR("tokenExpired");
      } else if (error.name === "JsonWebTokenError") {
        ctx.body = response.ERROR("tokenInvaild");
      } else {
        ctx.body = response.ERROR("unknownError");
      }
      return;
    }
  }
  await next();
};

module.exports = {
  auth,
};

增加路由頁面

controllers所有文件都導入到index.js中,然後再暴露出去,這樣在router文件就只需要引入controllers/index.js即可

const hotword = require("./hotword");
const issue = require("./issue");
const person = require("./person");
const user = require("./user");
const common = require("./common");
const wallpaper = require("./wallpaper");
const fileList = require("./fileList");
const ips = require("./ips");
module.exports = {
  hotword,
  issue,
  person,
  user,
  common,
  wallpaper,
  fileList,
  ips,
};
const router = require("koa-router")();
const {
  hotword,
  person,
  issue,
  common,
  user,
  wallpaper,
  fileList,
  ips,
} = require("../controllers/index");
const { auth } = require("../middleware/index");

// router.get("/", async (ctx) => {
//   ctx.body = "歡迎訪問該介面";
// });
router.get("/wy/find", hotword.findHotword);
router.get("/wy/pageQuery", hotword.findPageHotword);

// 登錄才能刪除,修改評論(協商緩存)
router.get("/findExcerpt", person.findExcerpt);
router.get("/addExcerpt", person.addExcerpt);
router.get("/updateExcerpt", auth, person.updateExcerpt);
router.get("/delExcerpt", auth, person.delExcerpt);

// 不走緩存
router.post("/findIssue", issue.findIssue);
router.post("/addIssue", issue.addIssue);
router.post("/delIssue", issue.delIssue);
router.post("/editIssue", issue.editIssue);

router.post("/register/user", user.register);
router.post("/upload/file", common.uploadFile);
router.post("/paste/upload/file", common.pasteUploadFile);
// router.get("/download/file/:name", common.downloadFile);
// 強緩存
router.get("/wallpaper/findList", wallpaper.findPageWallpaper);

// 文件列表
router.post("/file/list", auth, fileList.findFileLsit);
router.post("/save/list", auth, fileList.saveFileInfo);
router.post("/delete/file", auth, fileList.delFile);

// ip
router.post("/find/ipList", (ctx, next) => ips.findIpsList(ctx, next));
module.exports = router;

註:需要用戶登錄的介面在路由前加auth中間件即可

獲取ip、上傳、下載

獲取ip

獲取ip可以單獨提取出來,當作中間件,這裡當用戶訪問我的ip地址頁面時,會自動在資料庫中添加記錄,同時使用redis存儲當前ip,10分鐘內再次訪問不會再增加。

註:當服務線上上使用nginx反向代理時,需要在app.js添加app.proxy = true,同時需要配置nginx來獲取用戶真實ip

const response = require("../utils/resData");
const { getClientIP } = require("../utils/common");
const seq = require("../mysql/sequelize");
const IpsModel = require("../models/ips");
const MIps = IpsModel(seq);
const client = require("../utils/redis");
const { reqIp } = require("../api/ip");

class Ips {
  constructor() {}
  async findIpsList(ctx, next) {
    try {
      if (!process.env.NODE_ENV) {
        await this.addIps(ctx, next);
      }
      const { size, page } = ctx.request.body;
      const total = await MIps.findAndCountAll();
      const data = await MIps.findAll({
        order: [["id", "DESC"]],
        limit: parseInt(size),
        offset: parseInt(size) * (page - 1),
      });
      ctx.body = response.SUCCESS("common", { total: total.count, data });
    } catch (error) {
      console.error(error);
      ctx.body = response.SERVER_ERROR();
    }
  }
  async addIps(ctx, next) {
    try {
      const ip = getClientIP(ctx);
      const res = await client.get(ip);
      // 沒有才在redis中設置
      if (!res) {
        // 需要將值轉為字元串
        await client.set(ip, new Date().toString(), {
          EX: 10 * 60, // 以秒為單位存儲10分鐘
          NX: true, // 鍵不存在時才進行set操作
        });
        if (ip.length > 6) {
          const obj = {
            id: Date.now(),
            ip,
          };
          const info = await reqIp({ ip });
          if (info.code === 200) {
            obj.operator = info.ipdata.isp;
            obj.address = info.adcode.o;
            await MIps.create(obj);
          } else {
            console.log("ip介面請求失敗");
          }
        }
      }
    } catch (error) {
      console.error(error);
    }
    await next();
  }
}

module.exports = new Ips();
location / {
  proxy_set_header Host $http_host;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header REMOTE-HOST $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header Public-Network-URL http://$http_host$request_uri;
  proxy_pass http://127.0.0.1:40001;
}

上傳

配置文件.env需要配置好絕對路徑:APP_FILE_PATH = /任意位置

const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const response = require("../utils/resData");
const { APP_NETWORK_PATH, APP_FILE_PATH } = require("../config/index");

class Common {
  constructor() {}
  async uploadFile(ctx, next) {
    try {
      const { file } = ctx.request.files;
      // 檢查文件夾是否存在,不存在則創建文件夾
      if (!fs.existsSync(APP_FILE_PATH)) {
        APP_FILE_PATH && fs.mkdirSync(APP_FILE_PATH);
      }
      // 上傳的文件具體地址
      let filePath = path.join(
        APP_FILE_PATH || __dirname,
        `${APP_FILE_PATH ? "./" : "../static/"}${file.originalFilename}`
      );
      // 創建可讀流(預設一次讀64kb)
      const reader = fs.createReadStream(file.filepath);
      // 創建可寫流
      const upStream = fs.createWriteStream(filePath);
      // 可讀流通過管道寫入可寫流
      reader.pipe(upStream);
      const fileInfo = {
        id: Date.now(),
        url: APP_NETWORK_PATH + file.originalFilename,
        name: file.originalFilename,
        size: file.size,
        type: file.originalFilename.match(/[^.]+$/)[0],
      };
      ctx.body = response.SUCCESS("common", fileInfo);
    } catch (error) {
      console.error(error);
      ctx.body = response.ERROR("upload");
    }
  }
  async pasteUploadFile(ctx, next) {
    try {
      const { file } = ctx.request.body;
      const dataBuffer = Buffer.from(file, "base64");
      // 生成隨機40個字元的hash
      const hash = crypto.randomBytes(20).toString("hex");
      // 文件名
      const filename = hash + ".png";
      let filePath = path.join(
        APP_FILE_PATH || __dirname,
        `${APP_FILE_PATH ? "./" : "../static/"}${filename}`
      );
      // 以寫入模式打開文件,文件不存在則創建
      const fd = fs.openSync(filePath, "w");
      // 寫入
      fs.writeSync(fd, dataBuffer);
      // 關閉
      fs.closeSync(fd);
      const fileInfo = {
        id: Date.now(),
        url: APP_NETWORK_PATH + filename,
        name: filename,
        size: file.size || "",
        type: 'png',
      };
      ctx.body = response.SUCCESS("common", fileInfo);
    } catch (error) {
      console.error(error);
      ctx.body = response.ERROR("upload");
    }
  }
}

module.exports = new Common();

下載

直接配置nginx即可,當客戶端請求路徑以 /static 開頭的靜態資源時,nginx 會從指定的文件系統路徑 /www/wwwroot/note.loveverse.top/static 中獲取相應的文件。用於將 /static 路徑下的靜態資源標記為下載類型,用戶訪問這些資源時告訴瀏覽器下載文件

location /static {
  add_header Content-Type application/x-download;
  alias   /www/wwwroot/note.loveverse.top/static;
}

小程式登錄和公眾號驗證

小程式

.env中新增如配置,在middleware/index.js中增加refresh中間件

# 微信公眾號appID
APP_ID = wx862588761a1a5465
# 微信公眾號appSecret
APP_SECRET = a06938aae54d2f72a41e4710854354534
# 微信公眾號token
APP_TOKEN = 543543
# 微信小程式appID
APP_MINI_ID = wx663ca454434243
# 微信小程式密鑰
APP_MINI_SECRET = ee17b15f95fcd597243243432432
const refresh = async (ctx, next) => {
  // 將openId掛載到ids,供全局使用
  const token = ctx.request.header["mini-love-token"];
  if (token) {
    try {
      // -1說明沒有設置過期時間,-2表示不存在該鍵
      const ttl = await client.ttl(token);
      if (ttl === -2) {
        ctx.body = response.WARN("token已失效,請重新登錄", 401);
        return;
      }
      // 過期時間小於一個月,增加過期時間
      if (ttl < 30 * 24 * 60 * 60) {
        await client.expire(token, 60 * 24 * 60 * 60);
      }
      // 掛載openid
      const ids = await client.get(token);
      const info = ids.split(":");
      ctx.state.ids = {
        sessionId: info[0],
        openId: info[1],
      };
    } catch (error) {
      ctx.state.ids = null;
      console.error(error);
      ctx.body = response.ERROR("redis獲取token失敗");
      return;
    }
  } else {
    ctx.body = response.WARN("請先進行登錄", 401);
    return;
  }
  await next();
};

公眾號驗證

安裝xml2js,將包含 XML 數據的字元串解析為 JavaScript 對象傳給公眾號。在utils/constant.js添加常量,然後編寫響應公眾號的介面chat.js。這裡公眾號調試難免會需要將開發環境映射到公網,這裡使用xshell隧道做內網穿透

image.png

image.png

const template = `
<xml>
 <ToUserName><![CDATA[<%-toUsername%>]]></ToUserName>
 <FromUserName><![CDATA[<%-fromUsername%>]]></FromUserName>
 <CreateTime><%=createTime%></CreateTime>
 <MsgType><![CDATA[<%=msgType%>]]></MsgType>
 <% if (msgType === 'news') { %>
 <ArticleCount><%=content.length%></ArticleCount>
 <Articles>
 <% content.forEach(function(item){ %>
 <item>
 <Title><![CDATA[<%-item.title%>]]></Title>
 <Description><![CDATA[<%-item.description%>]]></Description>
 <PicUrl><![CDATA[<%-item.picUrl || item.picurl || item.pic || item.thumb_url %>]]></PicUrl>
 <Url><![CDATA[<%-item.url%>]]></Url>
 </item>
 <% }); %>
 </Articles>
 <% } else if (msgType === 'music') { %>
 <Music>
 <Title><![CDATA[<%-content.title%>]]></Title>
 <Description><![CDATA[<%-content.description%>]]></Description>
 <MusicUrl><![CDATA[<%-content.musicUrl || content.url %>]]></MusicUrl>
 <HQMusicUrl><![CDATA[<%-content.hqMusicUrl || content.hqUrl %>]]></HQMusicUrl>
 </Music>
 <% } else if (msgType === 'voice') { %>
 <Voice>
 <MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>
 </Voice>
 <% } else if (msgType === 'image') { %>
 <Image>
 <MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>
 </Image>
 <% } else if (msgType === 'video') { %>
 <Video>
 <MediaId><![CDATA[<%-content.mediaId%>]]></MediaId>
 <Title><![CDATA[<%-content.title%>]]></Title>
 <Description><![CDATA[<%-content.description%>]]></Description>
 </Video>
 <% } else { %>
 <Content><![CDATA[<%-content%>]]></Content>
 <% } %>
</xml>
`;
module.exports = { template };
const crypto = require("crypto");
const xml2js = require("xml2js");
const ejs = require("ejs");
const {
  template
} = require("../utils/constant");
const { APP_ID, APP_SECRET, APP_TOKEN } = require("../config/index");
const response = require("../utils/resData");

const wechat = {
  appID: APP_ID,
  appSecret: APP_SECRET,
  token: APP_TOKEN,
};
const compiled = ejs.compile(template);
function reply(content = "", fromUsername, toUsername) {
  const info = {};
  let type = "text";
  info.content = content;
  if (Array.isArray(content)) {
    type = "news";
  } else if (typeof content === "object") {
    if (content.hasOwnProperty("type")) {
      type = content.type;
      info.content = content.content;
    } else {
      type = "music";
    }
  }
  info.msgType = type;
  info.createTime = new Date().getTime();
  info.fromUsername = fromUsername;
  info.toUsername = toUsername;
  return compiled(info);
}

class Wechat {
  constructor() {}
  // 公眾號驗證
  async verifyWechat(ctx, next) {
    try {
      let {
        signature = "",
        timestamp = "",
        nonce = "",
        echostr = "",
      } = ctx.query;
      const token = wechat.token;
      // 驗證token
      let str = [token, timestamp, nonce].sort().join("");
      let sha1 = crypto.createHash("sha1").update(str).digest("hex");
      if (sha1 !== signature) {
        ctx.body = "token驗證失敗";
      } else {
        // 驗證成功
        if (JSON.stringify(ctx.request.body) === "{}") {
          ctx.body = echostr;
        } else {
          // 解析公眾號xml
          let obj = await xml2js.parseStringPromise(ctx.request.body);
          let xmlObj = {};
          for (const item in obj.xml) {
            xmlObj[item] = obj.xml[item][0];
          }
          console.info("[ xmlObj.Content ] >", xmlObj);
          // 文本消息
          if (xmlObj.MsgType === "text") {
              const replyMessageXml = reply(
                str,
                xmlObj.ToUserName,
                xmlObj.FromUserName
              );
              ctx.type = "application/xml";
              ctx.body = replyMessageXml;
            // 關註消息
          } else if (xmlObj.MsgType === "event") {
            if (xmlObj.Event === "subscribe") {
              const str = msg.attendion;
              const replyMessageXml = reply(
                str,
                xmlObj.ToUserName,
                xmlObj.FromUserName
              );
              ctx.type = "application/xml";
              ctx.body = replyMessageXml;
            } else {
              ctx.body = null;
            }
            // 其他消息
          } else {
            const str = msg.other;
            const replyMessageXml = reply(
              str,
              xmlObj.ToUserName,
              xmlObj.FromUserName
            );
            ctx.type = "application/xml";
            ctx.body = replyMessageXml;
          }
        }
      }
    } catch (error) {
      console.error(error);
      // 返回500,公眾號會報錯
      ctx.body = null;
    }
  }
}
module.exports = new Wechat();

註:公眾號和小程式是另一個項目,所以不在github上

調試

以腳本命令的方式運行,可以看到完整的數據列印,排查問題更方便
image.png

存在的問題

  1. websocket服務端沒封裝,應該單獨分出一個文件來寫。
  2. ip地址記錄只有訪問ip列表這個頁面才會增加,一直停留在其他頁面無法獲取ip記錄
  3. pseron介面測試協商緩存不生效,返回304,但是瀏覽器還是顯示200
  4. sequelize-automate自動生成的createdAtupdatedAt沒有同步到資料庫中
  5. 上傳文件緩慢

等等還有其他一些未列舉的問題

代碼鏈接

https://github.com/loveverse/love-blog

參考文章

https://github.com/jj112358/node-api(nodejs從0到1更加詳細版)

https://www.freesion.com/article/34551095837/(xshell內網穿透原理)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 8月17日,在第14屆中國資料庫技術大會(DTCC2023)上,天翼雲科技有限公司資料庫首席技術官李躍森以《天翼雲TeleDB持續創新之路》為題發表演講,介紹了天翼雲TeleDB資料庫的發展歷程、技術突破以及國產化能力。 ...
  • * 0.結論先行 * 1.背景介紹 * 2.測試過程 * 3.結果對比 - 附錄 > myloader還預設禁用binlog了 ## 0. 結論先行 重要結論先說:導入大批量數據時,採用GreatSQL 8.0.32-24中新增並行load data特性是最快的,關於該特性的描述詳見:[Change ...
  • 前端下載文件一般使用的是blob 核心的步驟是獲取後端響應的文件流,用blob創建一個臨時的URL,然後創建一個隱藏的<a>標簽,實現下載需求。 那就先上代碼 function download(item) { axios.get(getServerUrl() + "/teacher/output/ ...
  • > 導語:最近開發了一個基於 uniapp 框架的項目,有一些感觸和體會,所以想記錄以下一些技術和經驗,在這裡做一個系列總結,算是對自己做一個交代吧。 ## 目錄 * 簡介 * 全局文件 * 全局組件 * 常用 API * 條件編譯 * 插件開發 ## 簡介 uniapp 是 DCloud 公司於 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 本篇主要討論以下兩種翻書動畫的實現: 第一種是整頁翻轉的效果: 這種整頁翻轉的效果主要是做rotateY的動畫,並結合一些CSS的3d屬性實現。 第二種折線翻轉的效果,如下圖所示: 主要是通過計算頁面翻折過來的位置。 這兩種原理上都不是很 ...
  • 在移動端頁面中,由於屏幕空間有限,導航條扮演著非常重要的角色,提供了快速導航到不同頁面或功能的方式。用戶也通常會在導航條中尋找他們感興趣的內容,因此導航條的曝光率較高。在這樣的背景下,提供一個動態靈活的導航條,為產品賦能,變得尤其重要。 ...
  • ##### 17 JavaScript 中的call和apply 對於咱們逆向工程師而言. 並不需要深入的理解call和apply的本質作用. 只需要知道這玩意執行起來的邏輯順序是什麼即可 在運行時. 正常的js調用: ```js function People(name, age){ this.n ...
  • 地圖在 app 中使用還是很廣泛的,常見的應用常見有: 1、獲取自己的位置,規劃路線。 2、使用標記點進行標記多個位置。 3、繪製多邊形,使用圍牆標記位置等等。 此篇文章就以高德地圖為例,以上述三個常見需求為例,教大家如何在 uniapp 中添加地圖。 作為一個不管閑事的前端姑娘,我就忽略掉那些繁瑣 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...