利用後端框架 express 實作抽獎網站的 API


Posted by Christy on 2022-01-28

本文為利用後端框架 express 實作抽獎網站的過程,包含 express + sequelize 的基本設置、規劃前台與後台畫面及路由、獎項的 API 與抽獎機率實作,最後把網站部署到 Heroku 上面

零、內容摘要

0. 實作前沒有什麼想法,很苦惱

只要做抽獎頁面就好,我原以為是整個餐廳網站都要全部改成 express,仔細看作業說明,就是做一個抽獎的網站,改寫是下一週的作業。

剛開始還是因為沒有畫面,所以不知道該做什麼,而不知道該做什麼的時候,就連問題在哪都不清楚,自然卡東卡西的,「只要定義清楚問題,自然就會有答案」又再次驗證了這句話。

1. 把需要做的功能寫下來拆解,好像逐漸有了雛形

a. 使用 express + sequelize 先把抽獎網站雛形做出來,包含前台與後台頁面

b. 實作新增、讀取、刪除、編輯功能

c. 實作 API

d. 實作機率

e. 部署

2. 當按下抽獎按鈕時,就會出現獎項。獎項會有獎品名稱、內容說明、以及獎品圖片

a. 按下按鈕,清空畫面

b. 去呼叫 API 回傳獎項

3. API 其實就是在 controller 裡的一個 method

仔細想想,API 就是一份資料嘛,在 method 裡面可以回傳資料,格式大概會長這樣:

{"id":1,"item":"Fourth Prize","url":"https://images.pexels.com/photos/3945317/pexels-photo-3945317.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=750&w=1260","content":"Movie tickets"}

controller 裡面的 api method 最終的目的就是「顯示某資料庫裡面的一個獎項」,而這個「顯示的計算過程」就是機率。

在增加每個獎項時,權重可以寫成 1-100 的正整數,這個正整數就等於是百分比

什麼是權重?就是希望一件事情發生的機率,例如說,權重的範圍是 1-100,權重數字越大則代表發生的機率越大,因為是機率,所以直接設定百分比是最快的。

a. 提供資料庫裡面其中一個獎項

b. 根據得獎機率,計算顯示獎項的機率

JS簡單實現:根據獎品權重計算中獎概率實現抽獎的方法

JS根據獎品權重計算中獎概率

一、思考抽獎網站全貌

1. 需要的功能

a. 身為一個管理員,我希望有一個抽獎頁面讓我管理獎項

b. 身為一個管理員,我希望可以在後台新增抽獎的品項(名字、圖片網址以及說明)以及機率

c. 身為一個管理員,我希望可以在後台編輯抽獎的品項(名字、圖片網址以及說明)以及機率

d. 身為一個管理員,我希望可以在後台刪除抽獎的品項

e. 身為一個管理員,我希望在前台能夠抽出我在後台所設定的獎項

2. 路由設計

a. 登入、登出功能:只要有一組帳密就可以了

a.1 登入:

get: /login

post: /login → /

a.2 登出:get: /logout

b. CRUD 夥伴們

b.1 新增:post: /create_article/id → /

b.2 刪除:get: /delete_article/id → /

b.3 編輯:

get /edit_article/id → form

post /edit_article/id → /

c. API

get: /api

二、規劃資料庫結構

1. 獎品資料庫:item, content, url, ratio

2. 使用者資料庫:username, password

3. model 指令:Data Types

獎品:

$ npx sequelize-cli model:generate --name Prize --attributes item:text,content:text,url:text,ratio:integer

使用者:

$ npx sequelize-cli model:generate --name User --attributes username:string,password:string

三、環境設置

註:安裝 eslint 那些的部分,參考 EsLint 我們重頭來過吧,如果已經 init 過了,就不需要再一次了,因此我從步驟三開始。

1. $ npm init

2. $ npm install eslint prettier prettier-eslint,接著再 eslint --init

3. $ npm install express,接著再 $ npm install ejs body-parser express-session connect-flash bcrypt

4. $ npm install --save sequelize mysql2

5. $ npm install --save-dev sequelize-cli

6. $ npx sequelize-cli init

7. 改 config 檔案 development

8. 建立 models: user & prize

獎品:

$ npx sequelize-cli model:generate --name Prize --attributes item:text,content:text,url:text,ratio:integer

使用者:

$ npx sequelize-cli model:generate --name User --attributes username:string,password:string

9. Running Migrations

$ npx sequelize-cli db:migrate

註:到這裡後,資料庫裡面應該會有剛剛建的兩個表格了

四、實作抽獎網站功能

參考 用後端好朋友們 express & sequelize 實作部落格

實作可以分成三個部分:

1. express + sequelize 基本建置

2. 規劃頁面及路由

3. 實作前台與後台頁面還有 CRUD 功能

4. 實作 API & 抽獎機率

5. 難題紀錄

a. bootstrap 圖片置中對齊 class="w-50 rounded mx-auto d-block"

b. 載入靜態檔案,要在 index.js 加上這行 app.use(express.static(`${__dirname}/public`));

6. 檔案結構及內容

  • controllers folder

    • prize.js api 的結果以及要做的事,也寫在這裡

    • user.js

  • public folder

    • api.js 按下抽獎按鈕時,清空網頁並且呼叫 API 顯示抽獎結果
  • views folder

    • template

      • head.ejs 引入 bootstrap

      • navbar.ejs 導覽列

    • user

      • login.ejs 顯示登入頁面
    • add.ejs 顯示新增獎項頁面

    • cms.ejs 顯示後台所有獎項頁面

    • edit.ejs 顯示編輯獎項頁面

    • index.ejs 負責顯示抽獎前台頁面

  • index.js: 負責基本設定及路由

// index.js

const express = require("express");
const bodyParser = require("body-parser");
const session = require("express-session");
const flash = require("connect-flash");
const app = express();
const port = 5001;

const userController = require("./controllers/user");
const prizeController = require("./controllers/prize");

app.set("view engine", "ejs");
app.use(
  session({
    secret: "keyboard cat",
    resave: false,
    saveUninitialized: true,
  })
);
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.use(flash());
app.use(express.static(`${__dirname}/public`));

app.use((req, res, next) => {
  res.locals.username = req.session.username;
  res.locals.errorMessage = req.flash("errorMessage");
  next();
});

app.get("/", prizeController.index);

function redirectBack(req, res) {
  res.redirect("back");
}

app.get("/api", prizeController.api);

app.get("/login", userController.login);
app.post("/login", userController.handleLogin, redirectBack);
app.get("/logout", userController.logout);

app.get("/cms", prizeController.cms);

app.get("/add", prizeController.add);
app.post("/add", prizeController.handleNewPrize, redirectBack);

app.get("/delete_prize/:id", prizeController.delete);
app.get("/edit_prize/:id", prizeController.edit);
app.post("/edit_prize/:id", prizeController.handleEdit, redirectBack);

app.listen(port, () => {
  console.log("success");
});
// controllers > prize.js

const db = require("../models");
const { Prize } = db;

const prizeController = {
  cms: async (req, res, next) => {
    try {
      const prizes = await Prize.findAll({
        order: [["id", "ASC"]],
      });
      res.render("cms", {
        prizes,
      });
    } catch (err) {
      req.flash("errorMessage", err.toString());
      return next();
    }
  },

  add: (req, res) => {
    res.render("add");
  },

  handleNewPrize: async (req, res, next) => {
    const { item, content, url, ratio } = req.body;
    if (!item || !content || !url || !ratio) {
      req.flash("errorMessage", "Please fill in all the required fields.");
      return next();
    }
    if (isNaN(Number(ratio)) || ratio < 1 || ratio > 100) {
      req.flash("errorMessage", "Please fill in ratio with 1 to 100.");
      return next();
    }
    try {
      await Prize.create({
        item,
        content,
        url,
        ratio,
      });
      res.redirect("/cms");
    } catch (err) {
      req.flash("errorMessage", err.toString());
      return next();
    }
  },

  index: async (req, res, next) => {
    try {
      const prizes = await Prize.findAll({
        order: [["id", "ASC"]],
      });
      res.render("index", {
        prizes,
      });
    } catch (err) {
      req.flash("errorMessage", err.toString());
      return next();
    }
  },

  delete: async (req, res, next) => {
    try {
      const prize = await Prize.findOne({
        where: {
          id: req.params.id,
        },
      });
      await prize.destroy();
      res.redirect("/cms");
    } catch (err) {
      req.flash("errorMessage", err.toString());
      return next();
    }
  },

  edit: async (req, res, next) => {
    try {
      const prize = await Prize.findOne({
        where: {
          id: req.params.id,
        },
      });
      res.render("edit", {
        prize,
      });
    } catch (err) {
      req.flash("errorMessage", err.toString());
      return next();
    }
  },

  handleEdit: async (req, res, next) => {
    const { item, content, url, ratio } = req.body;
    if (!item || !content || !url || !ratio) {
      req.flash("errorMessage", "Please fill in all the required fields.");
      return next();
    }
    if (isNaN(Number(ratio)) || ratio < 1 || ratio > 100) {
      req.flash("errorMessage", "Please fill in ratio with 1 to 100.");
      return next();
    }
    try {
      const prize = await Prize.findOne({
        where: {
          id: req.params.id,
        },
      });
      await prize.update({
        item,
        content,
        url,
        ratio,
      });
      res.redirect("/cms");
    } catch (err) {
      req.flash("errorMessage", err.toString());
      return next();
    }
  },

  api: async (req, res) => {
    try {
      const drawResult = await getDraw();
      const randomNumber = Math.floor(Math.random() * 100);
      const id = drawResult[randomNumber];

      const data = await Prize.findOne({
        where: { id },
        attributes: { include: ["item", "content", "url"] },
      });
      res.send(data);
    } catch (err) {
      req.flash("errorMessage", err.toString());
      return next();
    }
  },
};

async function getDraw() {
  const totalRatio = await Prize.sum("ratio");
  const allPrizes = await Prize.findAll();
  let countRatio = [];
  for (const prize of allPrizes) {
    const ratio = (prize.ratio / totalRatio).toFixed(2) * 100;
    countRatio.push({
      id: prize.id,
      ratio,
    });
  }
  let result = [];
  for (let i = 0; i < countRatio.length; i++) {
    for (let j = 1; j <= countRatio[i].ratio; j++) {
      result.push(countRatio[i].id);
    }
  }
  return result;
}

module.exports = prizeController;
// controllers > user.js

const bcrypt = require("bcrypt");
const db = require("../models");
const { User } = db;

const userController = {
  login: (req, res) => {
    res.render("user/login");
  },

  handleLogin: async (req, res, next) => {
    const { username, password } = req.body;
    if (!username || !password) {
      req.flash("errorMessage", "Please fill in all the required fields.");
      return next();
    }

    try {
      let user = await User.findOne({
        where: {
          username,
        },
      });
      if (!user) {
        req.flash("errorMessage", "invalid user or password");
        return next();
      }
      bcrypt.compare(password, user.password, (err, isSuccess) => {
        if (err || !isSuccess) {
          req.flash("errorMessage", "invalid user or password");
          return next();
        }
        req.session.username = user.username;
        res.redirect("/");
      });
    } catch (err) {
      req.flash("errorMessage", err.toString());
      return next();
    }
  },

  logout: (req, res) => {
    req.session.destroy();
    res.redirect("/");
  },
};

module.exports = userController;
// public > api.js

const mainHTML = document.querySelector(".wrapper");

document.querySelector(".draw-btn").addEventListener("click", () => {
  mainHTML.innerHTML = "";
  drawResult();
});

async function getDrawAPI() {
  try {
    const res = await fetch("/api");

    // 我原本沒有宣告 draw 就不小心產生了一個全域變數
    return (draw = await res.json());

    // 寫這樣比較好
    const draw = await res.json();
    return draw;

  } catch (err) {
    return console.log(`Error: ${draw.error} & ${draw.message}`);
  }
}

async function drawResult() {
  try {
    const result = await getDrawAPI();
    mainHTML.innerHTML = `
      <div class="wrapper">
        <div class="text-center">
          <h3 class="mt-3">${result.item}</h3>
          <p>${result.content}</p>
        </div>
        <img class="w-50 rounded mx-auto d-block img-fluid" src="${result.url}">
      </div>
    `;
  } catch (err) {
    return console.log(`Error: ${result.error} & ${result.message}`);
  }
}









Related Posts

A Swift Tour 閱讀筆記

A Swift Tour 閱讀筆記

React Hooks - Day1

React Hooks - Day1

MTR04_0806

MTR04_0806


Comments