Node讀取JSON文件並在for循環中的延時Sleep處理(延時任務清洗數據接口實戰演示)

最近接到個需求:清洗訂單數據,數據量10W+。
實現邏輯很繁瑣複雜:
循環拿到全部用戶的邀請號ID —> 根據邀請號ID循環拿到所有對應的被邀請者ID —> 根據被邀請者ID拿到用戶token —> 根據用戶token查到全部訂單 —> 循環檢查全部訂單狀態(判斷是否退費) —> 如果退費就取消對應邀請人的獎勵紅包 —> 扣除普通現金獎勵紅包後,重新計算邀請人數個數,生成階梯紅包。

業務代碼確實很繁瑣很複雜,其中最重要的還是邀請人與被邀請人是多對多關係,被邀請人拿訂單詳情是一對多關係,其中參雜各種判斷和三重for循環,可想而知這個清洗數據接口放在服務器上面跑的話,影響是很大的。
因此,我這裏拆分爲線下接口調用線上接口緩解生產環境壓力。具體步驟如下:

1:線下本地每次用get方式往生產的接口傳入一個邀請人ID,也就是生產環境每次只清洗處理一個邀請人。
2:由於開發環境和生產環境數據庫不同,因此把生產環境全部邀請人導出一個JSON文件。
3:線下本地開發環境讀取JSON文件,每次傳一個邀請人ID到生產接口清洗。
4:因爲邏輯很長,要用延時器一個個發送,間隔爲4秒。(測試接口3~5秒)

好了現在上代碼,請無視業務代碼,這裏是自己一個記錄學習而已:

先從生產庫把真實生產環境的邀請人ID導出JSON。

在這裏插入圖片描述

放到隨便一個地方,然後node讀取該JSON文件。
坑點:

(1)讀取文件一定要寫絕對路徑,相對路徑找不到文件!
(2)fs方式讀取出來的都是String類型,不信的話自己加個Type of看一下data。不是看起來是JSON就是JSON類型了。
(3)在function 回調函數裏面附值是沒用的,除非強行變成同步。不信可以自己各個地方console.log()看一下。

// 先引入
const fs = require('fs');

fs.readFile(
      // 注意這裏一定要寫絕對路徑,相對路徑無法識別。
    '/Users/XXXX/Desktop/XXXXX_project/XXXXapi/app/controller/data.json',
      'utf8',
      async function(err, data) {
        if (err) {
          console.log(err);
        }
        // 一定要JSON轉了纔是JSON格式,不然讀取都是String。
        const dataList = JSON.parse(data);
      },
    );
因爲node裏面沒有JAVA的sleep()線程睡眠,所以要自己實現讓進程sleep()。times就是自己定義的傳進來的多少秒。
function sleep(times) {
      let now = new Date();
      const exit = now.getTime() + times;
      while (true) {
        now = new Date();
        if (now.getTime() > exit) {
          return;
        }
      }
    }
最後看完整代碼:

坑點:

(1)setTimeout那種不適合用在for循環中的延時任務,一開始我也是死磕setTimeOut;但是仔細想:
A:如果for放在setTimeOut裏面,那麼就是延時N秒後一瞬間跑完for,達不到每個數據延時數秒再拿下一個數據。
B:如果for放在setTimeOut外面,那麼會不停重複幾次輸出相同的一組元素(很難描述,可以自己體會)
被迫無奈,自己實現Sleep().

async test() {
    fs.readFile(
      '/Users/自己文件存放的位置/data.json',
      'utf8',
      async function(err, data) {
        if (err) {
          console.log(err);
        }
        const dataList = JSON.parse(data);
        for (const item of dataList.record) {
          sleep(4000); // 停滯四秒
          const res = await axios.get(
            `https://線上接口的域名+路徑?invite_user_id=${item.invite_user_id}`,
          );
          console.log(item.invite_user_id);
          console.log(res.data);
        }
      },
    );

    function sleep(times) {
      let now = new Date();
      const exit = now.getTime() + times;
      while (true) {
        now = new Date();
        if (now.getTime() > exit) {
          return;
        }
      }
    }
    this.ctx.success();
  }
這部分不必細究!!! 線上繁瑣的業務清洗代碼,冗長的清洗流程就不用看了。根據業務來就行,這些業務代碼沒有參考價值。也正是因爲它冗長,才採取這種設計模式。
/**
   * 清洗退費用戶接口
   */
  async cleanRefundUser() {
    // 首先根據邀請者ID拿到全部被邀請者ID,再用被邀請者ID,也就是newcomer ID去users表拿accesstoken,再用token去查用戶訂單。
    const inviteUserId = this.ctx.query.invite_user_id;
    let accessToken;
    let noRefund = 0;
    let refund = 0;
    const userIdList = await this.inviteVacationClass.getAllUserIdByInviteUserId(
      inviteUserId,
    );
    logger.info(LOG_CAT, {
      msg: '清洗數據開始',
      data: {
        inviteUserIds: inviteUserId,
        userIdLists: userIdList,
      },
    });
    let ordersActivity;
    for (const id of userIdList) {
      // 拿被邀請者的token
      const userAccessToken = await this.inviteVacationClass.getAccessTokenByUserId(
        id.newcomer_id,
      );
      if (userAccessToken) {
        accessToken = userAccessToken.access_token;
      }
      // 用token去查詢訂單,注意這裏會返回多個訂單!
      const res = await axios.get(
        `${this.app.config.get_teacher_info_api}/api/1/pay/get_order_list?access_token=${accessToken}`,
      );
      if (res && res.data.data.finish_sub_orders[0]) {
        for (const item of res.data.data.finish_sub_orders) {
          ordersActivity = item.activity;
          // 判斷是否老帶新活動
          if (
            // 過濾掉不是老帶新活動的訂單
            ordersActivity === '2019grow50yuanldx' ||
            ordersActivity === '2019grow50yuanldx_bzr_xiaoxue' ||
            ordersActivity === '2019grow50yuanldx_bzr_chuzhong' ||
            ordersActivity === '2019grow50yuanldx_xiaodi' ||
            ordersActivity === '2019grow50yuanldx_chuzhong' ||
            ordersActivity === '2019grow50yuanhanjia_jiesuo_50'
          ) {
            // 訂單狀態判斷:1 已支付     2 退費   0 未付款    3 取消
            let orderStatus;
            orderStatus = item.status;
            if (orderStatus === 1) {
              noRefund = noRefund + 1;
            } else if (orderStatus === 2) {
              // 再根據newcommer id 和 invite user id拿到對應哪個紅包。
              const redPocket = await this.ctx.app.dao.inviteNewcomerV2.inviteRedpocket.getRedPocketById(
                inviteUserId,
                id.newcomer_id,
              );
              if (redPocket[0][0]) {
                // 把該普通固定獎勵紅包設置爲退費狀態,不可領取
                await this.ctx.app.dao.inviteNewcomerV2.inviteRedpocket.cancelRedPocketById(
                  redPocket[0][0].id,
                  new Date(),
                );
              }
              logger.info(LOG_CAT, {
                msg: '用戶退費信息',
                data: {
                  newcomer_id: id.newcomer_id,
                  inviteUserIds: inviteUserId,
                  redPocketId: redPocket[0][0].id,
                  redPockets: redPocket[0][0],
                },
              });
              refund = refund + 1;
            }
          }
        }
      }
    }
    logger.info(LOG_CAT, {
      msg: '清洗普通紅包結束,開始統計剩餘人數',
      data: {
        inviteUserIds: inviteUserId,
        noRefunds: noRefund,
        refunds: refund,
      },
    });
    // 假如未退費好友小於10人,要扣除100元階梯紅包,其它實物獎勵不用管。
    if (noRefund < 10) {
      const redpocketType = 25;
      await this.ctx.app.dao.inviteNewcomerV2.inviteRedpocket.cancelExtraRedPocket(
        inviteUserId,
        new Date(),
        redpocketType,
      );
    }
    this.ctx.success('未退費人數:' + noRefund, '退費人數:' + refund);
  }

好,現在開始跑一下看看十幾萬訂單能不能清洗並且準確扣除對應的獎勵吧。

在這裏插入圖片描述

在這裏插入圖片描述

可以。好了,讓它自己安安靜靜本地調用生產跑幾個小時就行了。

這是自己的實戰分享,在開發中遇到的好想法或者對一些特殊場景的設計處理,記錄下來分享。鞠躬!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章