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);
  }

好,现在开始跑一下看看十几万订单能不能清洗并且准确扣除对应的奖励吧。

在这里插入图片描述

在这里插入图片描述

可以。好了,让它自己安安静静本地调用生产跑几个小时就行了。

这是自己的实战分享,在开发中遇到的好想法或者对一些特殊场景的设计处理,记录下来分享。鞠躬!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章