2024-10-28|閱讀時間 ‧ 約 0 分鐘

第三方金流串接 – 資料庫交易設定

    ※ 在orderController.ts檔案中定義createOrder: 

    public createOrder: IOrderController['createOrder'] = (req, res, _next) => {
    let { paymentProvider, paymentWay, contents } = req.body
    console.log(
    "~ file: ordreController.ts ~ line 52 ~ OrderController ~ paymentProvider, paymentWay, contents",
    paymentProvider,
    paymentWay,
    contents
    );
    res.jsonp({ status: 'success' });

    };

    程式碼解說:

    1.方法定義:

    • public createOrder:定義一個公開的 createOrder 方法,符合 IOrderController 介面中 createOrder 方法的定義。
    • (req, res, _next):這個方法接受三個參數:req(請求對象)、res(回應對象)和 _next(下一個中介軟體函數,這裡未使用)。

    2.請求主體

    • 從 req.body 中提取 paymentProvider、paymentWay 和 contents 屬性。這些屬性應該符合 CreateOrderRequestParams 介面。
    • 在後續的程式中可能會需要重新賦值這些變數,所以使用let。

    3.紀錄日誌

    • 將 paymentProvider、paymentWay 和 contents 的值輸出到控制台,用於除錯。

    4.回應客戶端

    • res.json 是 Express.js 提供的方法,用來向客戶端回傳 JSON 格式的資料。
    • 這裡回傳了一個物件 { status: 'success' },表示操作已成功完成。

    createOrder 的主要用途

    1. 接收請求:從客戶端接收一個包含訂單詳情的 HTTP 請求。
    2. 解析數據:從請求主體中提取訂單的詳細資訊,例如支付提供者、支付方式和訂單內容。
    3. 處理邏輯:根據提取的數據執行必要的業務邏輯,例如計算總金額、檢查產品庫存等。
    4. 保存訂單:將處理後的訂單數據保存到資料庫中。
    5. 回應客戶端:返回一個成功或失敗的回應給客戶端,通常包括訂單的詳細資訊或錯誤訊息。

    ※ 建立order路由

    在routes資料夾中,建立order.ts檔案:

    raw-image

    1.匯入模組:

    import express from 'express';
    import { ControllerContext } from "@/manager/controllerManager";

    程式碼解說:

    • express:從 express 模組匯入,用來創建路由器。
    • ControllerContext:從 controllerManager 匯入,代表控制器上下文的型別。

    2.定義函數 mountOrderRouter

    export const mountOrderRouter = ({
    controllerCtx
    }: { controllerCtx: ControllerContext }) => {}

    程式碼解說:

    • 定義並導出 mountOrderRouter 函數。
    • 函數接受一個參數 controllerCtx,類型為 ControllerContext,其中包含了所有的控制器。
    • 通過這個參數,可以訪問並調用 orderController 的方法來處理訂單相關的請求。

    3.設置路由:

    let router = express.Router();
    router.post('/create', controllerCtx.orderController.createOrder);

    程式碼解說:

    • 使用 express.Router() 創建一個新的路由器實例。
    • 使用 router.post('/create', controllerCtx.orderController.createOrder) 設置一個 POST 路徑 /create,當這個路徑被請求時,調用 orderController 的 createOrder 方法來處理請求。

    4.返回路由器:

    return router;

    程式碼解說:

    • 返回已設定好的路由器,這樣可以將它掛載到應用程式的主路徑上。

    ※ 建立createController來製造一個新的 OrderController 實例


    1.靜態方法 createController

    public static createController(
    { knexSql, orderModel, productModel }:
    { knexSql: Knex; orderModel: IOrderModel; productModel: IProductModel })

    }

    程式碼解說:

    • 靜態方法:定義一個靜態方法 createController,這意味著你可以直接通過類別來調用這個方法,而不需要創建類別的實例。
    • 參數:
      • knexSql:資料庫連接實例,用於資料庫操作。
      • orderModel:訂單模型,用於操作和管理訂單資料。
      • productModel:產品模型,用於操作和管理產品資料。

    2.創建並返回 OrderController 實例

    return new OrderController({ knexSql, orderModel, productModel });

    程式碼解說:

    • 使用傳入的參數創建一個新的 OrderController 實例。
    • 將 knexSql, orderModel, 和 productModel 傳遞給 OrderController 的建構函數進行初始化。

    ※ controllerManager.ts中建立 OrderController 

    1.匯入 IOrderController 和 OrderController

    import { IOrderController, OrderController } from "@/controller/orderController";

    程式碼解說:

    • IOrderController:介面,定義了訂單控制器應該具備的方法和結構。
    • OrderController:類別,實現了 IOrderController 介面,包含處理訂單相關邏輯的方法。

    2.匯入 Knex

    import { Knex } from "knex";

    程式碼解說:

    • 匯入 knex 模組中的 Knex 類型,用於資料庫操作。

    3.定義 ControllerContext 介面

    export interface ControllerContext {
    orderController: IOrderController; //新增
    }

    程式碼解說:

    • 新增 orderController 屬性到 ControllerContext 介面,表示控制器上下文中包含了訂單控制器。

    4.定義 controllerManger 函數

    export const controllerManger = ({ knexSql, modelCtx }:
    { knexSql: Knex; modelCtx: ModelContext; }): ControllerContext => {}

    程式碼解說:

    • 函數接受兩個參數:knexSql 和 modelCtx,分別是資料庫連接和模型上下文。

    5.初始化 orderController

    const orderController = OrderController.createController({
    knexSql,
    orderModel: modelCtx.orderModel,
    productModel: modelCtx.productModel,
    });

    程式碼解說:

    • 使用 createController 靜態方法來創建 orderController 的實例,傳入資料庫連接、訂單模型和產品模型。

    6.返回控制器上下文

    return {
    orderController
    }

    程式碼解說:

    • 返回一個包含 orderController 的物件,這個物件的類型符合 ControllerContext 介面。

    ※ 在app.ts新增程式碼 

    class App {
    private knexSql: Knex;//新增

    constructor() {
    this.knexSql = createDatabase();
    this.controllerCtx = controllerManger({
    knexSql: this.knexSql,

    })

    }
    private routerSetup() {

        this.app.use('/orders', mountOrderRouter({ controllerCtx: this.controllerCtx }))

      }

    1.類別定義和建構函數

    程式碼解說:

    • knexSql 屬性:定義了一個私有屬性 knexSql,類型為 Knex,用於資料庫操作。
    • 建構函數:
      • 初始化資料庫連接:調用 createDatabase() 函數初始化 knexSql。
      • 初始化控制器上下文:使用 controllerManger 函數,傳入 knexSql 來初始化 controllerCtx。

    2.路由設置方法

    程式碼解說:

    • 掛載訂單路由:使用 this.app.use 將訂單路由掛載到 /orders 路徑,並調用 mountOrderRouter 函數,傳入 controllerCtx。

    ※ 在postman驗證 

    輸入驗證資料:


    驗證結果:

    ※ 驗證方式

    在orderController.ts新增程式碼 

    public createOrder: IOrderController['createOrder'] = (req, res, _next) => {

    //1.資料驗證

    //contents [{id,amount,price}, ...]

    if (paymentProvider !== "ECPAY" && paymentProvider !== "PAYPAL")

    res.json({ status: "failed", message: "paymentProvider not valid" });

    };

    程式碼解說:

    • 資料驗證:在處理訂單創建之前,先對請求資料進行驗證。
    • paymentProvider 驗證:
      • 檢查 paymentProvider 是否為 "ECPAY" 或 "PAYPAL"。
      • 如果 paymentProvider 不是這兩者之一,回應客戶端一個 JSON 物件,表示操作失敗,並包含錯誤訊息 "paymentProvider not valid"。

    使用npm套件驗證 — express validator

    安裝軟體:

    npm install express validator

    在orderController.ts新增程式碼 

    import { isEmpty } from "lodash";
    import { body } from 'express-validator';
    //1.資料驗證
    public createOrderValidator = () => {
    const paymentProviderValidator = (value: any) => {
    return [PaymentProvider.ECPAY, PaymentProvider.PAYPAL].includes(value);
    }

    const paymentWayValidator = (value: any) => {
    return [PaymentWay.CVS, PaymentWay.PAYPAL].includes(value);
    }
    const contentValidator = (value: OrderContent[]) => {
    if (isEmpty(value)) false;

    for (const product of value) {
    if ([product.productId, product.amount, product.price].some((val) => !val))
    return false;
    }
    return true;
    }

    return [
    //設定驗證不同參數的內容是否合法
    body('paymentProvider', 'Invalid payment provider').custom(paymentProviderValidator),
    body('paymentWay', 'Invalid payment way').custom(paymentWayValidator),
    body('contents', 'Invalid product contents')
    .isArray()
    .custom(contentValidator),

    ]
    }

    程式碼解說:

    1.方法定義:

    public createOrderValidator = () => {}
    • 這是一個公開的方法,為了驗證訂單創建請求,返回一組驗證規則。

    2.驗證函數:

    const paymentProviderValidator = (value: any) => {

    return [PaymentProvider.ECPAY, PaymentProvider.PAYPAL].includes(value);

    }

    const paymentWayValidator = (value: any) => {

    return [PaymentWay.CVS, PaymentWay.PAYPAL].includes(value);

    }

    const contentValidator = (value: OrderContent[]) => {

    if (isEmpty(value)) false;

    for (const product of value) {

    if ([product.productId, product.amount, product.price].some((val) => !val))

    return false;

    }

    return true;

    }
    • 驗證支付提供者是否為合法值。
    • 驗證訂單內容是否為非空陣列,且每個產品的 productId、amount 和 price 都存在。

    3.返回驗證規則:

    return [
    body('paymentProvider', 'Invalid payment provider').custom(paymentProviderValidator),
    body('paymentWay', 'Invalid payment way').custom(paymentWayValidator),
    body('contents', 'Invalid product contents')
    .isArray()
    .custom(contentValidator),
    ]
    • 設定驗證不同參數的內容是否合法:
      • paymentProvider 必須是合法的支付提供者。
      • paymentWay 必須是合法的支付方式。
      • contents 必須是非空陣列且符合內容驗證器規則。

    在orderController.ts新增一組驗證規則 

    export interface IOrderController

    { createOrderValidator(): ValidationChain[];//新增 }

    程式碼解說:

    定義方法結構

    • createOrderValidator() 方法被定義在 IOrderController 介面中,強制所有實現這個介面的類別都必須實現這個方法。
    • 返回 ValidationChain[]:這表明方法會返回一組驗證規則,這些規則用於驗證訂單創建請求中的參數。

    在orderController.ts新增一組驗證數據是否合法 

     if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
    }

    程式碼解說:

    • if (!errors.isEmpty()):檢查是否有任何驗證錯誤。
    • return res.status(400).json({ errors: errors.array() }):
      • 如果有錯誤,則返回一個 HTTP 400 錯誤狀態碼,表示請求不合法。
      • 同時返回一個 JSON 對象,包含所有驗證錯誤。

    驗證結果:


    ※ 在router檔案中的order.ts加入中介層


     router.post(
    '/create',
    //middleware中介層
    controllerCtx.orderController.createOrderValidator(),//新增
    //controller create Order正式的內容
    controllerCtx.orderController.createOrder)

    程式碼解說:

    • 中介層(middleware)使用:
      • 在設置路由時,將這個驗證方法作為中介層,確保每次收到請求時都先進行數據驗證。
    • 處理邏輯:如果驗證通過,則由 createOrder 方法處理請求。

    ※ transaction 資料庫交易主要流程

    1. 開始交易:使用 knex.transaction() 開始交易。
    2. 設置隔離級別:如果提供了 isolation,則設置交易隔離級別。
    3. 執行回調函數:調用回調函數執行具體的交易操作。
    4. 提交事務:如果回調函數成功,提交交易。
    5. 處理錯誤:如果回調函數失敗,回滾交易並根據錯誤類型進行重試或拋出異常。
    6. 重試機制:在特定錯誤(如死鎖)發生時,進行重試,直到達到最大重試次數。

    ※ 在utils檔案中的index.ts加入transaction 資料庫交易

    ※ 定義了一個名為 transactionHandler 的函數,用於處理資料庫事務並提供重試機制

    程式碼解說:

    參數:

    1. knex: Knex:資料庫連接實例。
    2. callback: (trx: Knex.Transaction) => Promise<T>:執行事務的回調函數,接受一個事務對象 trx。
    3. options:一個包含以下可選參數的物件:
      • retryTimes?: number:最大重試次數,預設值為 100。
      • maxBackOff?: number:最大回退時間(毫秒),預設值為 1000。
      • isolation?: ISOLATION_LEVELS:事務隔離級別。

    1.資料隔離性

    可以讓你在設計和實現資料庫操作時,根據應用需求選擇適當的隔離級別來保證數據的一致性和完整性。

    enum ISOLATION_LEVELS {
    READ_UNCOMMITTED = 'READ_UNCOMMITTED',
    READ_COMMITTED = 'READ_COMMITTED',
    REPEATABLE_READ = 'REPEATABLE_READ',
    SERIALIZABLE = 'SERIALIZABLE'
    }

    程式碼解說:

    1. READ_UNCOMMITTED:
      • 讀取未提交:允許一個事務讀取另一個事務尚未提交的變更。這種隔離級別風險最高,因為會有「髒讀」的問題。
    2. READ_COMMITTED:
      • 讀取已提交:只允許讀取已提交的變更,防止「髒讀」,但可能會有「不可重複讀」的問題,即同一事務中讀取的數據可能會改變。
    3. REPEATABLE_READ:
      • 可重複讀:確保同一事務中的多次讀取結果一致,防止「髒讀」和「不可重複讀」,但可能會有「幻讀」問題,即在事務期間其他事務新增的記錄會被讀取到。
    4. SERIALIZABLE:
      • 可序列化:最高級別的隔離,所有事務按順序執行,防止「髒讀」、「不可重複讀」和「幻讀」。這種隔離級別會帶來較高的性能開銷。

    2.解構選項參數

    const { retryTimes = 100, maxBackOff = 1000, isolation } = options;

    程式碼解說:

    • retryTimes:設置重試次數,如果未提供則默認為 100。
    • maxBackOff:設置最大回退時間(毫秒),如果未提供則默認為 1000。
    • isolation:事務隔離級別,默認為未設置。

    3.初始化重試計數器

    let attempts = 0;

    程式碼解說:

    • 初始化 attempts(追蹤或交易重試的次數) 變數為 0,表示尚未進行任何重試。
    • 當交易處理失敗並需要重試時,attempts 會自增,用來追蹤已進行的重試次數。

    4.定義 execTransaction 函數: 這個內部函數用於執行事務,並在失敗時進行重試。

    const execTransaction = async (): Promise<T> => {
    const trx = await knex.transaction();
    try {
    if (isolation) await trx.raw(`SET TRANSACTION ISOLATION LEVEL ${isolation}`);
    const result = await callback(trx);
    await trx.commit();
    return result;
    } catch (err: any) {
    await trx.rollback();
    if (err.code !== '1205') throw err;
    if (attempts > retryTimes) throw Error('[Transaction] retry times is up to max');
    attempts++;
    await sleep(maxBackOff);
    return execTransaction();
    }
    };

    程式碼解說:

    • execTransaction 是一個泛型異步函數,返回類型為 Promise<T>,表示該函數會返回一個解決為類型 T 的承諾。
    • 使用 knex.transaction() 開始一個新的交易,並將交易對象存儲在 trx 變數中。
    • 如果指定了 isolation(隔離級別),則設置該事務的隔離級別。
    • 執行傳入的回調函數 callback,並將 trx 作為參數傳遞。
    • 如果回調函數成功,則提交交易並返回結果。
    • 如果回調函數失敗,則回滾交易。
    • 如果錯誤代碼不是 1205(通常表示死鎖),則重新拋出錯誤。
    • 檢查重試次數是否超過 retryTimes,如果超過則拋出錯誤。
    • 增加 attempts 計數器,等待指定的回退時間後,重新執行 execTransaction。

    sleep 函數:用於在重試前等待指定時間。

    function sleep(maxBackOff: number) {
    return new Promise((resolve) => setTimeout(() => resolve(1), maxBackOff));
    }

    程式碼解說:

    • maxBackOff:表示延遲的時間,單位為毫秒。
    • setTimeout 用於設置一個定時器,在 maxBackOff 毫秒後執行回調函數。
    • 回調函數中調用 resolve(1),解決(resolve)這個承諾,表示延遲結束。



    分享至
    成為作者繼續創作的動力吧!
    © 2024 vocus All rights reserved.