最近接到个需求:清洗订单数据,数据量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);
}