iT邦幫忙

3

【我可以你也可以的Node.js】第十五篇 - 打造一個 API 讓自己減肥永遠都是夢想- 肥宅的第一步 #Express #麥當勞報報優惠券

嗨,大家好我是 Robin
今天要分享我前陣子一直想寫但是遲遲沒實作的小玩具,
那就是...
我想要我有一支程式可以每天幫我領麥當勞報報的優惠券!

我的一小步,是肥宅的一大步 - Robin

會有這想法的故事是這樣的,幾乎每週我都會與同事前往麥當勞,當然身為專業吃麥當勞的我一定會使用優惠券 Combo 連擊,而這個 Combo 券連擊一定要搭配麥當勞報報的優惠券。但是優惠券不是說拿就拿,必須每天領取,但是有時候過於忙碌會忘記領取,有一天去吃麥當勞的時候我發覺... 我發覺我無法使出我的優惠券 Combo 連擊。
身為一個肥宅...
慚愧阿!慚愧~
於是心生歹念想寫這個~

此篇學習目標 ◑ω◐ :

這篇的目標只有一個,就是...

  • 使用 Express 打造一支 API 領取麥當勞優惠券
  • 部署至 Heroku

下篇會再講肥宅的第二步,每天自動領取。
因為我還沒做... Orz

注意須知:

這篇原本在很早很早很早就開始寫(約莫兩週前),
但是一直都覺得哪裡寫不好哪裡不ok的(到現在也是xD)。

此篇主要不是分享技術,
但是還是會有原始碼和該檔案的主要目的,
而是希望版上能有更多的這種為了更快學習一個語言或框架而發想寫一個專案~
感覺會讓寫程式在茫然的讀者能夠藉由寫專案獲得成就感。
進而讓自己的程式能力因為遇到問題而進步 !
就是一個寫不好沒關係!

滿足個人需求並且獲得成就感才是快樂學習的動力來源啊啊啊!
再進階一點就是滿足大眾需求~ 前提是要滿足的了自己...


使用 Express 打造一支 API 領取麥當勞優惠券

先看成果

  • 看一下目前優惠券的狀態

    • 主要需求:
      1. 獲取沒過期的優惠券
      2. 取得沒過期的歡樂貼數量
  • 拿每日優惠券

    • 主要需求:
      1. 獲取今日的優惠券
      2. 告訴我今日是領到優惠券 or 歡樂貼
  • 登入拿 Token

    • 主要需求:
      • 拿取上述兩個 API 所需的 Token

先使用上篇使用的 Express-generator,建造一個骨架。
還沒看的可以先回去看~

專案結構

結構大部分都是使用 Express-generator 所建制的,
除了 Controller 的部分是參照這位大大寫的自訂 MVC 的概念,想說可以順便熟悉 MVC ,如果內容觀念有誤再請多多見諒和留言告知我。
Node.js-Backend見聞錄(10):關於後端觀念(六)-關於MVC

.
├── LICENSE
├── README.md
├── app.js
├── bin
│   └── www
├── controllers
│   ├── lotteryController.js
│   └── userController.js
├── models
│   ├── lottery.js
│   ├── request.js
│   └── user.js
├── package-lock.json
├── package.json
├── public
│   └── stylesheets
│       └── style.css
├── routes
│   ├── lotteryRouter.js
│   └── usersRouter.js
├── test
│   ├── lottery.test.js
│   ├── schema
│   │   ├── lottery.json
│   │   └── user.json
│   └── user.test.js
└── views
    ├── error.pug
    ├── index.pug
    └── layout.pug

app.js

const usersRouter = require('./routes/usersRouter');
const lotteryRouter = require('./routes/lotteryRouter');

app.use('/api/users', usersRouter);
app.use('/api/lottery', lotteryRouter);

Routes

這邊會有兩個

  1. user -> 用來拿取 userToken
  2. lottery -> 拿取每日優惠券或查詢
lotteryRouter.js
const express = require('express');
const lotteryController = require('../controllers/lotteryController');

const router = express.Router();

router.get('/', lotteryController.getLotteryStatus);
router.post('/', lotteryController.getLottery);

module.exports = router;
usersRouter.js
const express = require('express');
const lotteryController = require('../controllers/lotteryController');

const router = express.Router();

router.get('/', lotteryController.getLotteryStatus);
router.post('/', lotteryController.getLottery);

module.exports = router;

Controllers

lotteryController.js

這邊分成兩部分

  1. getLottery
    • 目的:獲得優惠券並且取得當日的優惠券資訊
    • 注意:過期日為當日得隔兩天所以日期 +2 即可獲得當天的優惠券,否則則是歡樂貼。
  2. getLotteryStatus
    • 目的:獲取未過期的優惠券以及歡樂貼清單
const lottery = require('../models/lottery');

async function getLottery(req, res) {
  const nowDate = new Date();
  nowDate.setDate(nowDate.getDate() + 2); // The lottery expire at the day after tomorrow.
  const getLotteryResp = await lottery.getLottery(req.body.accessToken);
  const getLotteryListResp = await lottery.getLotteryList(req.body.accessToken);
  const stickerListResp = await lottery.getStickerList(req.body.accessToken);
  const lotteryToday = await getLotteryListResp.body.results.coupons.filter(
    (lotteryTarget) => lotteryTarget.object_info.redeem_end_datetime === nowDate.format('yyyy/mm/dd 23:59:59'),
  );
  const stickerToday = await stickerListResp.body.results.stickers.filter(
    (stickerTarget) => stickerTarget.obtain_datetime > new Date().format('yyyy/mm/dd 00:00:00'),
  );
  const todayGet = (lotteryToday.length !== 0 && stickerToday.length === 0) ? lotteryToday[0].object_info.title : '歡樂貼QQ';
  if (getLotteryResp.body.rc !== 1) {
    res.status(getLotteryResp.statusCode);
    res.json({
      errorMessage: getLotteryResp.body.rm,
    });
    return;
  }
  res.status(getLotteryResp.statusCode);
  res.json({
    lottery: getLotteryResp.body.results.coupon.object_info.title,
    todayLottery: todayGet,
  });
}

async function getLotteryStatus(req, res) {
  const lotteryList = [];
  const getLotteryListResp = await lottery.getLotteryList(req.body.accessToken);
  const stickerListResp = await lottery.getStickerList(req.body.accessToken);
  if (getLotteryListResp.body.rc !== 1 && stickerListResp.body.rc !== 1) {
    await res.status(400);
    await res.json({
      errorMessage: 'sticker or lottery have some problem ...',
    });
    return;
  }
  const lotteryNotExpire = await getLotteryListResp.body.results.coupons.filter((lotteryTarget) => lotteryTarget.object_info.redeem_end_datetime > new Date().format('yyyy/mm/dd HH:MM:ss'));
  for (let i = 0; i < lotteryNotExpire.length; i += 1) {
    lotteryList.push(lotteryNotExpire[i].object_info.title);
  }
  await res.status(200);
  await res.json({
    lottery: lotteryList,
    totalStickersAmount: stickerListResp.body.results.stickers.length,
  });
}

module.exports = {
  getLottery,
  getLotteryStatus,
};

userController.js
  • 目的:獲得該 User 的 Token
const user = require('../models/user.js');

module.exports = async (req, res) => {
  const resp = await user.getToken(req.body.account, req.body.password);
  if (resp.body.rc !== '1') {
    await res.status(resp.statusCode);
    await res.json({
      errorMessage: resp.body.rm,
    });
    return;
  }
  await res.status(resp.statusCode);
  await res.json({ token: resp.body.results.member_info.access_token });
};

Models

lottery.js

這邊分成三部分

  1. getLottery 打每日優惠券的 API
  2. getLotteryList 打每日優惠券清單的 API
  3. getStickersList 打歡樂貼清單的 API
const request = require('./request');
require('dotenv').config();
require('date.format');

const deviceTime = new Date().format('yyyy/mm/dd HH:MM:ss');
const source = {
  app_version: process.env.APP_VERSION,
  device_time: deviceTime,
  device_uuid: process.env.DEVICE_UUID,
  model_id: process.env.MODEL_ID,
  os_version: process.env.OS_VERSION,
  platform: process.env.PLATFORM,
};


async function getLottery(accessToken, sourceInfo = source) {
  const option = {
    url: `${process.env.MC_HOST}/lottery/get_item`,
    json: {
      access_token: accessToken,
      source_info: sourceInfo,
    },
  };
  const response = await request.postRequest(option);
  return response;
}


async function getLotteryList(accessToken, sourceInfo = source) {
  const option = {
    url: `${process.env.MC_HOST}/coupon/get_list`,
    json: {
      access_token: accessToken,
      source_info: sourceInfo,
    },
  };
  const response = await request.postRequest(option);
  return response;
}

async function getStickerList(accessToken, sourceInfo = source) {
  const option = {
    url: `${process.env.MC_HOST}/sticker/get_list`,
    json: {
      access_token: accessToken,
      source_info: sourceInfo,
    },
  };
  const response = await request.postRequest(option);
  return response;
}

module.exports = {
  getLottery,
  getLotteryList,
  getStickerList,
};
user.js
  • 打 麥當勞報報 Login 的 API
require('date.format');
const md5 = require('md5');
const request = require('./request');


async function getToken(userAccount, userPassword) {
  const deviceTime = new Date().format('yyyy/mm/dd HH:MM:ss');
  const appVersion = process.env.APP_VERSION;
  const callTime = new Date().format('yyyymmddHHMMss');
  const paramString = `${userAccount}${userPassword}`;
  const modelId = process.env.MODEL_ID;
  const osVersion = process.env.OS_VERSION;
  const platform = process.env.PLATFORM;
  const deviceUuid = process.env.DEVICE_UUID;
  const orderNo = `${process.env.DEVICE_UUID}${callTime}`;
  const maskMd5 = md5(`Mc${orderNo}${platform}${osVersion}${modelId}${deviceUuid}${deviceTime}${appVersion}${paramString}Donalds`);
  const parm = {
    account: userAccount,
    password: userPassword,
    OrderNo: orderNo,
    mask: maskMd5,
    source_info: {
      app_version: appVersion,
      device_time: deviceTime,
      device_uuid: deviceUuid,
      model_id: modelId,
      os_version: osVersion,
      Platform: process.env.PLATFORM,
    },
  };
  const option = {
    url: 'https://api.mcddaily.com.tw/login_by_mobile',
    json: parm,
  };
  const response = await request.postRequest(option);
  return response;
}

module.exports = {
  getToken,
};

Setting

建立一個 .env 檔(注意這個設定檔如果有敏感資訊通常是不會上傳到 repo 的)
主要用於設定一些 device information
ACCESS_TOKEN,USER_ACCOUNT 和 USER_PASSWORD 主要是我用來寫測試的預設帳號,可以不必理會。
主要會被存取方式就是以 process.env.XXX

MC_HOST = https://api1.mcddailyapp.com
APP_VERSION = '2.2.0'
MODEL_ID = 'MIX 3'
OS_VERSION = '9'
PLATFORM = 'Android'
DEVICE_UUID = 'device_uuid'
USER_ACCOUNT = ''
USER_PASSWORD = ''
ACCESS_TOKEN = ''

部署至 Heroku

這部分我選用最方便且最懶得方式
連結GitHub repositories 自動部署
aka 我推你就 deployee

前面的註冊登入就不教了連結給你~點我

  1. 建立你的 app

  2. 輸入 app 名稱(注意只能小寫) ,以及地區(只有美國和歐洲)。

  3. 選擇部署方式

  4. 連結並搜尋你的 repositories 名稱

  5. 設定自動部署(還可以選擇 branch)

  6. 打開你的 app 看一下 url 是什麼

  7. 打了發現...

  8. 在 Heroku 設定剛剛的設定檔

    就是 Key value 的輸入這樣~

接著就...

可以正常運作了~


我如何知道 APP 是呼叫哪支 API?

可以參閱此大大此篇詳細的 Charles 工具教學
mobile http request 攔截器攔起來-Charles

還可以偷看參考其他人用其他語言寫的麥當勞報報 xDD
McDaily
這邊這位大大還有貼心提醒

Please note that this app might stop working in the near future, as they have changed the hashing algorithm from simple MD5 to AES encryption + Base64 hashing using a differently formatted string.

專案目前情況

專案持續更新,直到我做到完全滿足我的需求為止。
GitHub Link: mcdonaldLottery

目前窘境:

  1. 後續自動領取的設計要自肥還是開放大眾使用
    (牽涉到 Token 代管的問題就會牽扯到資安的問題感覺很尷尬...
  2. Heroku 打麥當勞報報的 API response time 超久...
    插Log 看時間差約莫一支在3-5秒左右,上述的一支 API 可能需要打 2-3 支左右的麥當勞報報 API ,造成 API 回覆時間過長。
    (初步判定應該不是程式問題,可能是機器或是網路等原因)
  3. 免錢仔使用 Heroku 機器會休眠,如果使用 UptimeRobot 會造成我在 Heroku 的當月免費時數不夠。

關於 API response time 目前的想法:

  1. Linode 每個月用最低費用 $150 自行架設
  2. AWS ec2 用最低的免錢方案
  3. Google GCE 新會員一年免費

以上三種雲端伺服器都要實際跑過才會知道能否解決。


以上
歡迎留言或私訊討論~
感謝你


1 則留言

1
wl02843619000
iT邦新手 5 級 ‧ 2020-07-10 10:17:04

大推大推,
光看前文就一直很想看下去!!!
期待後續有更多好文!

Robin iT邦新手 4 級 ‧ 2020-07-11 14:15:28 檢舉

哇~感謝你的鼓勵 xD 我會努力的

我要留言

立即登入留言