最近接到個需求:清洗訂單數據,數據量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);
}