iT邦幫忙

2024 iThome 鐵人賽

DAY 18
0
Kubernetes

K8s 資料庫管理系統系列 第 18

day 18 K8s 考古題資料庫管理系統

  • 分享至 

  • xImage
  •  

今天是第十八天我們可以寫一個k8s 考古題資料庫管理系統,以下是我的程式碼

1. 系統需求分析

  • 使用者介面 (UI): 讓使用者可以方便地查詢考古題、瀏覽題目、以及進行增刪改等操作。
  • 資料庫 (DB): 儲存 Kubernetes 的考古題及相關答案與分類。
  • 後端服務 (API): 提供前端使用者介面和資料庫的溝通,處理題目查詢、更新等邏輯。
  • 容器化部署: 使用 Kubernetes 將該系統進行容器化並且部署至 K8s 集群上,實現可擴展和高可用的系統。

2. 技術選擇

  • 前端: 可使用 HTML、CSS、JavaScript、React 或 Vue.js 等來構建。
  • 後端: 使用 Node.js(Express) 或 Python(Flask/Django)來建立 API。
  • 資料庫: 使用 MySQL 或 PostgreSQL 來儲存考古題數據。
  • Kubernetes (K8s): 將應用封裝成容器並部署到 K8s 集群上。

3. 系統結構

  • 資料庫模型
    • questions 表:儲存考古題的題目內容、類型、難度、答案等。
    • categories 表:分類不同的考題主題。
    • users 表:儲存使用者資訊(例如老師、學生)。

4. 核心功能

  • 題目管理
    • 添加、編輯、刪除、查詢考古題。
    • 按類型、難度、分類篩選考古題。
  • 使用者管理
    • 管理教師和學生帳戶。
  • 測驗功能
    • 根據資料庫中的題目生成測驗,並自動批改答案。

5. 系統步驟實現

(a) 資料庫模型設計 (以 MySQL 為例)

CREATE TABLE questions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    description TEXT,
    difficulty VARCHAR(50),
    answer TEXT,
    category_id INT,
    FOREIGN KEY (category_id) REFERENCES categories(id)
);

CREATE TABLE categories (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) UNIQUE,
    password VARCHAR(255),
    role ENUM('teacher', 'student')
);

(b) 後端 API (以 Node.js 和 Express 為例)

const express = require('express');
const mysql = require('mysql');

const app = express();
app.use(express.json());

const db = mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: 'password',
  database: 'k8s_exam_db'
});

db.connect(err => {
  if (err) throw err;
  console.log('MySQL Connected...');
});

// 查詢所有考古題
app.get('/questions', (req, res) => {
  let sql = 'SELECT * FROM questions';
  db.query(sql, (err, results) => {
    if (err) throw err;
    res.json(results);
  });
});

// 新增考古題
app.post('/questions', (req, res) => {
  const { title, description, difficulty, answer, category_id } = req.body;
  let sql = 'INSERT INTO questions (title, description, difficulty, answer, category_id) VALUES (?, ?, ?, ?, ?)';
  db.query(sql, [title, description, difficulty, answer, category_id], (err, result) => {
    if (err) throw err;
    res.json({ message: 'Question added', id: result.insertId });
  });
});

// 刪除考古題
app.delete('/questions/:id', (req, res) => {
  let sql = 'DELETE FROM questions WHERE id = ?';
  db.query(sql, [req.params.id], (err, result) => {
    if (err) throw err;
    res.json({ message: 'Question deleted' });
  });
});

app.listen(3000, () => {
  console.log('Server started on port 3000');
});

(c) 前端界面 (以 React 為例)

import React, { useState, useEffect } from 'react';

function App() {
  const [questions, setQuestions] = useState([]);

  useEffect(() => {
    fetch('http://localhost:3000/questions')
      .then(res => res.json())
      .then(data => setQuestions(data));
  }, []);

  return (
    <div>
      <h1>Kubernetes 考古題資料庫</h1>
      <ul>
        {questions.map(question => (
          <li key={question.id}>
            {question.title} - {question.difficulty}
          </li>
        ))}
      </ul>
    </div>
  );
}

export default App;

(d) 部署到 Kubernetes

  1. Dockerfile:將應用容器化
dockerfile
# 使用 Node.js 作為基礎映像
FROM node:14

# 設定工作目錄
WORKDIR /app

# 複製 package.json 和安裝依賴
COPY package*.json ./
RUN npm install

# 複製應用程式檔案
COPY . .

# 暴露應用程式運行的埠
EXPOSE 3000

# 啟動應用程式
CMD ["npm", "start"]
  1. Kubernetes 部署配置檔案 (deployment.yaml)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: k8s-exam-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: k8s-exam
  template:
    metadata:
      labels:
        app: k8s-exam
    spec:
      containers:
      - name: k8s-exam
        image: your-docker-image
        ports:
        - containerPort: 3000
---
apiVersion: v1
kind: Service
metadata:
  name: k8s-exam-service
spec:
  selector:
    app: k8s-exam
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: LoadBalancer
  1. 使用 kubectl 部署:
kubectl apply -f deployment.yaml

6. 額外功能擴充

(a) 使用者身份驗證與授權

為了保護系統內的資料,使用者需要先登入,並根據其角色(教師或學生)獲得不同的操作權限。

  1. JWT(JSON Web Token)驗證
    使用 JWT 來實現無狀態的身份驗證,讓後端可以驗證 API 請求的合法性。

    • 註冊 API: 用戶可以註冊帳號(分為教師或學生角色)。
    • 登入 API: 用戶輸入帳號密碼,後端驗證後返回一個 JWT token,後續的 API 請求將攜帶這個 token 以進行身份驗證。
    • JWT 驗證中間件: 後端在處理 API 請求時驗證 token 的有效性。
  2. 後端 API(基於 JWT 的登入與註冊)

    • 新增 jsonwebtoken 套件來管理 JWT token。
    • 使用者角色 (role) 判斷不同操作權限。
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

// 註冊使用者
app.post('/register', async (req, res) => {
  const { username, password, role } = req.body;
  const hashedPassword = await bcrypt.hash(password, 10);
  let sql = 'INSERT INTO users (username, password, role) VALUES (?, ?, ?)';
  db.query(sql, [username, hashedPassword, role], (err, result) => {
    if (err) throw err;
    res.json({ message: 'User registered' });
  });
});

// 登入使用者並生成 JWT
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  let sql = 'SELECT * FROM users WHERE username = ?';
  db.query(sql, [username], async (err, result) => {
    if (err) throw err;
    if (result.length === 0) return res.status(400).json({ message: 'User not found' });

    const user = result[0];
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) return res.status(400).json({ message: 'Invalid credentials' });

    // 生成 JWT
    const token = jwt.sign({ id: user.id, role: user.role }, 'secretkey', { expiresIn: '1h' });
    res.json({ token });
  });
});

// JWT 驗證中間件
const authenticateJWT = (req, res, next) => {
  const token = req.headers.authorization && req.headers.authorization.split(' ')[1];
  if (token == null) return res.sendStatus(401);

  jwt.verify(token, 'secretkey', (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
};

// 只有教師可以添加考古題
app.post('/questions', authenticateJWT, (req, res) => {
  if (req.user.role !== 'teacher') return res.sendStatus(403);

  const { title, description, difficulty, answer, category_id } = req.body;
  let sql = 'INSERT INTO questions (title, description, difficulty, answer, category_id) VALUES (?, ?, ?, ?, ?)';
  db.query(sql, [title, description, difficulty, answer, category_id], (err, result) => {
    if (err) throw err;
    res.json({ message: 'Question added', id: result.insertId });
  });
});

(b) 題目篩選功能

增加篩選條件來讓使用者可以根據不同標準檢索考古題,如分類、難度、關鍵字等。

  • 後端 API(增加篩選參數):
app.get('/questions', (req, res) => {
  const { category_id, difficulty, keyword } = req.query;
  let sql = 'SELECT * FROM questions WHERE 1=1';

  if (category_id) sql += ` AND category_id = ${db.escape(category_id)}`;
  if (difficulty) sql += ` AND difficulty = ${db.escape(difficulty)}`;
  if (keyword) sql += ` AND title LIKE ${db.escape('%' + keyword + '%')}`;

  db.query(sql, (err, results) => {
    if (err) throw err;
    res.json(results);
  });
});
  • 前端界面(篩選功能):
function App() {
  const [questions, setQuestions] = useState([]);
  const [category, setCategory] = useState('');
  const [difficulty, setDifficulty] = useState('');
  const [keyword, setKeyword] = useState('');

  useEffect(() => {
    const query = new URLSearchParams({ category, difficulty, keyword }).toString();
    fetch(`http://localhost:3000/questions?${query}`)
      .then(res => res.json())
      .then(data => setQuestions(data));
  }, [category, difficulty, keyword]);

  return (
    <div>
      <h1>Kubernetes 考古題資料庫</h1>
      
      {/* 篩選選項 */}
      <label>Category:</label>
      <input value={category} onChange={(e) => setCategory(e.target.value)} />
      <label>Difficulty:</label>
      <input value={difficulty} onChange={(e) => setDifficulty(e.target.value)} />
      <label>Keyword:</label>
      <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />

      <ul>
        {questions.map(question => (
          <li key={question.id}>
            {question.title} - {question.difficulty}
          </li>
        ))}
      </ul>
    </div>
  );
}

(c) 測驗功能

為學生增加測驗功能,系統從資料庫中隨機抽取一定數量的題目,並提供批改測驗的功能。

  • 後端 API(隨機選取考古題並批改)
app.get('/quiz', authenticateJWT, (req, res) => {
  let sql = 'SELECT * FROM questions ORDER BY RAND() LIMIT 10';
  db.query(sql, (err, results) => {
    if (err) throw err;
    res.json(results);
  });
});

// 提交測驗並批改
app.post('/quiz/submit', authenticateJWT, (req, res) => {
  const { answers } = req.body;
  let correctAnswers = 0;

  answers.forEach(answer => {
    let sql = 'SELECT * FROM questions WHERE id = ? AND answer = ?';
    db.query(sql, [answer.question_id, answer.answer], (err, result) => {
      if (result.length > 0) correctAnswers++;
    });
  });

  res.json({ score: correctAnswers });
});

(d) 儲存與管理測驗紀錄

在系統中記錄每位學生的測驗結果,方便教師查閱。

  • 資料庫設計
CREATE TABLE quiz_records (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT,
    score INT,
    date DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);
  • 後端 API(記錄測驗結果):
app.post('/quiz/submit', authenticateJWT, (req, res) => {
  const { answers } = req.body;
  let correctAnswers = 0;

  answers.forEach(answer => {
    let sql = 'SELECT * FROM questions WHERE id = ? AND answer = ?';
    db.query(sql, [answer.question_id, answer.answer], (err, result) => {
      if (result.length > 0) correctAnswers++;
    });
  });

  let sql = 'INSERT INTO quiz_records (user_id, score) VALUES (?, ?)';
  db.query(sql, [req.user.id, correctAnswers], (err, result) => {
    if (err) throw err;
    res.json({ score: correctAnswers, message: 'Quiz result saved' });
  });
});

7. 部署與擴展

(a) 資料持久化

為了確保資料庫的持久化,我們可以在 Kubernetes 集群中使用 Persistent Volume (PV) 和 Persistent Volume Claim (PVC) 來管理資料庫儲存。

  • Persistent Volume 配置
apiVersion: v1
kind: PersistentVolume
metadata:
  name: mysql-pv
spec:
  capacity:
    storage: 5Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: /mnt/data
  • Persistent Volume Claim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

(b) 整合 CI/CD

使用 CI/CD 工具(如 Jenkins、GitLab CI、GitHub Actions)來自動

化部署流程,每次程式碼變更都自動部署最新版本至 Kubernetes 集群。

一. 後端 API

後端是用 Node.js 和 Express 框架來構建的,並使用了 MySQL 作為資料庫。這裡的後端 API 負責處理使用者的註冊、登入、考古題的管理、測驗以及身份驗證。

1. 依賴庫與環境設置

const express = require('express');
const app = express();
const mysql = require('mysql');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
  • express:一個用於建立伺服器的 Node.js 框架。
  • mysql:用來連接和操作 MySQL 資料庫。
  • jsonwebtoken:用來生成和驗證 JWT(JSON Web Token),用於身份驗證。
  • bcrypt:用來加密使用者的密碼,以確保安全。

2. 註冊 API

app.post('/register', async (req, res) => {
  const { username, password, role } = req.body;
  const hashedPassword = await bcrypt.hash(password, 10);
  let sql = 'INSERT INTO users (username, password, role) VALUES (?, ?, ?)';
  db.query(sql, [username, hashedPassword, role], (err, result) => {
    if (err) throw err;
    res.json({ message: 'User registered' });
  });
});
  • 功能:這個 API 允許新用戶註冊。用戶提交 usernamepasswordrole(例如學生或教師)。
  • 步驟
    1. 先從 req.body 中取得使用者的註冊資料。
    2. 使用 bcrypt.hash 來加密使用者密碼,保護密碼不以明文儲存。
    3. 將加密後的密碼和使用者名稱、角色一起儲存到 MySQL 資料庫的 users 表中。

3. 登入 API

app.post('/login', (req, res) => {
  const { username, password } = req.body;
  let sql = 'SELECT * FROM users WHERE username = ?';
  db.query(sql, [username], async (err, result) => {
    if (err) throw err;
    if (result.length === 0) return res.status(400).json({ message: 'User not found' });

    const user = result[0];
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) return res.status(400).json({ message: 'Invalid credentials' });

    const token = jwt.sign({ id: user.id, role: user.role }, 'secretkey', { expiresIn: '1h' });
    res.json({ token });
  });
});
  • 功能:這個 API 允許用戶登入,並返回一個 JWT token 作為身份驗證憑證。
  • 步驟
    1. req.body 中取得使用者的登入資料。
    2. 查詢 MySQL 資料庫中的 users 表,檢查該使用者是否存在。
    3. 使用 bcrypt.compare 比對密碼是否正確。如果正確,生成一個 JWT token,token 會包含使用者的 ID 和角色,並設置過期時間為 1 小時。
    4. 返回這個 JWT token 給前端,後續 API 請求會附帶這個 token。

4. JWT 驗證中間件

const authenticateJWT = (req, res, next) => {
  const token = req.headers.authorization && req.headers.authorization.split(' ')[1];
  if (token == null) return res.sendStatus(401);

  jwt.verify(token, 'secretkey', (err, user) => {
    if (err) return res.sendStatus(403);
    req.user = user;
    next();
  });
};
  • 功能:這個中間件用於保護需要身份驗證的路由。它會檢查請求的 Authorization 標頭,確保其中包含合法的 JWT token。
  • 步驟
    1. 從請求的 Authorization 標頭中提取 JWT token。
    2. 使用 jwt.verify 驗證 token 的合法性。如果驗證失敗,回應 403 Forbidden 錯誤;如果成功,將解碼的使用者資料附加到 req.user,然後繼續執行下一個中間件。

5. 考古題管理 API

app.post('/questions', authenticateJWT, (req, res) => {
  if (req.user.role !== 'teacher') return res.sendStatus(403);

  const { title, description, difficulty, answer, category_id } = req.body;
  let sql = 'INSERT INTO questions (title, description, difficulty, answer, category_id) VALUES (?, ?, ?, ?, ?)';
  db.query(sql, [title, description, difficulty, answer, category_id], (err, result) => {
    if (err) throw err;
    res.json({ message: 'Question added', id: result.insertId });
  });
});
  • 功能:只有教師身份的使用者能夠新增考古題。
  • 步驟
    1. 使用 authenticateJWT 中間件來驗證請求是否包含有效的 JWT token。
    2. 如果驗證通過,檢查 req.user.role 是否為 teacher,只有教師才可以新增題目。
    3. req.body 中提取題目的資訊,並將其插入資料庫中的 questions 表。
    4. 回傳新增成功的訊息和新考古題的 ID。

6. 隨機測驗 API

app.get('/quiz', authenticateJWT, (req, res) => {
  let sql = 'SELECT * FROM questions ORDER BY RAND() LIMIT 10';
  db.query(sql, (err, results) => {
    if (err) throw err;
    res.json(results);
  });
});
  • 功能:這個 API 會隨機選取 10 道考古題,供學生進行測驗。
  • 步驟
    1. 使用 authenticateJWT 中間件來驗證請求。
    2. questions 表中隨機選取 10 道題目,並返回給前端。

二. 前端 React 應用

這個前端應用是用 React.js 來構建的,使用者可以在這裡搜尋考古題、進行測驗等。

1. 使用狀態和效果鉤子來載入資料

function App() {
  const [questions, setQuestions] = useState([]);
  const [category, setCategory] = useState('');
  const [difficulty, setDifficulty] = useState('');
  const [keyword, setKeyword] = useState('');

  useEffect(() => {
    const query = new URLSearchParams({ category, difficulty, keyword }).toString();
    fetch(`http://localhost:3000/questions?${query}`)
      .then(res => res.json())
      .then(data => setQuestions(data));
  }, [category, difficulty, keyword]);
  • 功能:這段程式碼利用 React 的狀態 (useState) 和效果鉤子 (useEffect) 來根據使用者選擇的篩選條件查詢考古題。
  • 步驟
    1. useState 設置 questionscategorydifficultykeyword 來存放使用者輸入的篩選條件和查詢結果。
    2. useEffect 在每當篩選條件變化時觸發,會構建 API 查詢字串並向後端發送請求,將返回的考古題資料儲存到 questions

2. 顯示考古題列表

  return (
    <div>
      <h1>Kubernetes 考古題資料庫</h1>
      
      {/* 篩選選項 */}
      <label>Category:</label>
      <input value={category} onChange={(e) => setCategory(e.target.value)} />
      <label>Difficulty:</label>
      <input value={difficulty} onChange={(e) => setDifficulty(e.target.value)} />
      <label>Keyword:</label>
      <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />

      <ul>
        {questions.map(question => (
          <li key={question.id}>
            {question.title} - {question.difficulty}
          </li>
        ))}
      </ul>
    </div>
  );
}
  • 功能:前端提供了

搜尋考古題的 UI,允許使用者依照分類、難度和關鍵字來進行篩選,並且在頁面上顯示搜尋結果。

  • 步驟
    1. 提供篩選輸入框,當使用者修改輸入框時,對應的狀態變數(例如 category)會被更新。
    2. 利用 questions.map() 來動態生成每個考古題的列表項目,顯示題目名稱和難度。

這些程式碼示範了如何構建一個後端 API 和前端 React 應用,實現 Kubernetes 考古題資料庫管理系統。每個功能都設計為簡單明瞭,易於擴展和調整。


上一篇
day 17 k8s 民調資料庫管理系統
下一篇
day 19 k8s交通分析資料庫管理系統
系列文
K8s 資料庫管理系統30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言