※ 訂單管理器(orderController)具有兩種功能:
createOrder:建立訂單。updateAmount:更新金額。
//orderController API接口
export class OrderController implements IOrderController {
knexSql: Knex;
orderModel: IOrderModel;
productModel: IProductModel;
constructor({ knexSql, orderModel, productModel }: {
knexSql: Knex;
orderModel: IOrderModel;
productModel: IProductModel;
}) {
this.knexSql = knexSql;
this.orderModel = orderModel;
this.productModel = productModel;
}
//建立訂單
public createOrder: IOrderController["createOrder"] = (req, res, _next) => {
let { paymentProvider, paymentWay, contents } = req.body
console.log("🚀 ~ OrderController ~ paymentProvider, paymentWay, contents :", paymentProvider, paymentWay, contents )
res.json({ status: "success" });
//1.資料驗證
//2.將資料寫入database---> ORDER
//3/金流API的串接(ECPAY,PAYPAL)
//4. return database create success
};
//更新金額
public updateAmount: IOrderController["updateAmount"] = (_req, _res, _next) => {
// Todo
}
}
※ 確認 req.body 是否正確傳遞了所需的數據:
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 的主要用途:
- 接收請求:從客戶端接收一個包含訂單詳情的 HTTP 請求。
- 解析數據:從請求主體中提取訂單的詳細資訊,例如支付提供者、支付方式和訂單內容。
- 處理邏輯:根據提取的數據執行必要的業務邏輯,例如計算總金額、檢查產品庫存等。
- 保存訂單:將處理後的訂單數據保存到資料庫中。
- 回應客戶端:返回一個成功或失敗的回應給客戶端,通常包括訂單的詳細資訊或錯誤訊息。
※ 建立order路由:routes –––> order.ts

1.匯入模組:
import express from 'express';
import { ControllerContext } from "@/manager/controllerManager";
程式碼解說:
- express:從 express 模組匯入,用來創建路由器。
- ControllerContext:從 controllerManager 匯入,代表控制器上下文的型別。
2.安裝訂單路由器:
export const mountOrderRouter = ({
controllerCtx
}: { controllerCtx: ControllerContext }) => {}
程式碼解說:
- 定義並導出 mountOrderRouter 函數。
- 函數接受一個參數 controllerCtx,類型為 ControllerContext,其中包含了所有的控制器。
- 通過這個參數,可以訪問並調用 orderController 的方法來處理訂單相關的請求。
3.使用express router:
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;
程式碼解說:
- 返回已設定好的路由器,這樣可以將它掛載到應用程式的主路徑上。
※ ControllerContext新增orderController :manager –––> controllerManager.ts
export interface ControllerContext {
productController: IProductController;
orderController: IOrderController;
}
程式碼解說:
- 新增 orderController 屬性到 ControllerContext 介面,表示控制器上下文中包含了訂單控制器。
※ ControllerContext新增orderController 實例:manager –––> controllerManager.ts
const orderController = OrderController.createConstructor({
orderModel: modelCtx.orderModel,
})
程式碼解說:
- 透過
createConstructor方法創建orderController,並且確保這個控制器在初始化時擁有所需的orderModel作為其依賴項,以便在後續的業務邏輯中使用。
※ controllerManager.ts中建立 OrderController :manager –––> modelManager.ts

import { IOrderModel, OrderModel } from "@/model/order";
//在一個地方管理和使用不同的模型
//定義一個模型相關的環境資訊
export interface ModelContext {
orderModel: IOrderModel;
}
//與資料庫相關的初始化操作
export const modelManager = ({ knexSql }: { knexSql: Knex }): ModelContext => {
const orderModel = OrderModel.createModel({ knexSql })
return { orderModel }
}
程式碼解說:
- IOrderController:介面,定義了訂單控制器應該具備的方法和結構。
- OrderController:類別,實現了 IOrderController 介面,包含處理訂單相關邏輯的方法。
- 定義了一個名為
ModelContext的介面,這個介面包含了一個屬性orderModel,其類型為IOrderModel。 - 參數解構: modelManager函數接收一個參數 { knexSql },其類型為 { knexSql: Knex }。這意味著 knexSql 是一個 Knex 類型的物件,這個物件通常用於與資料庫進行互動。
- 創建 orderModel: 在函數內部,我們使用 OrderModel.createModel 方法創建了一個 orderModel 實例,並將 knexSql 作為參數傳遞給 createModel 方法,這樣 orderModel 就能與資料庫進行互動。
- 返回 ModelContext 物件: 最後,我們返回一個包含 orderModel 屬性的物件,這個物件的類型為 ModelContext。
※ 建立createController來製造一個新的 OrderController 實例:Controller –––>OrderController.ts

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 的建構函數進行初始化。
※ 新增orderController (訂單控制器):manager -->controllerManager.ts

1. 定義 ControllerContext 介面
export interface ControllerContext {
orderController: IOrderController;
}
程式碼解說:
ControllerContext 介面,新增orderController (訂單控制器)。
2. 定義 controllerManager 函數
export const controllerManager = ({ knexSql, modelCtx }: {
knexSql: Knex;
modelCtx: ModelContext
}): ControllerContext => {
程式碼解說:
新增加knexSql: 一個 Knex 的實例,用於與資料庫進行交互。
3. 創建 orderController
const orderController = OrderController.createController({
knexSql,
orderModel: modelCtx.orderModel,
productModel: modelCtx.productModel,
});
程式碼解說:
這部分創建了 orderController,使用 OrderController 的 createController 方法,並傳入 knexSql、orderModel 和 productModel。
※ 在app.ts新增程式碼 :src --> 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。
※ 將訂單相關的路由配置到 Express 應用中 :src --> app.ts
import { mountOrderRouter } from './routes/order';
private routerSetup() {
this.app.use('/', indexRouter);
this.app.use('/users', usersRouter);
this.app.use('/products',
mountProductRouter({ controllerCtx: this.controllerCtx })
);//將mountProductRouter的router傳出來
this.app.use('orders',
mountOrderRouter({ controllerCtx: this.controllerCtx })
);//將mountOrderRouter的router傳出來
}
程式碼解說:
import { mountOrderRouter } from './routes/order'時,從 ./routes/order 模組中匯入了一個名為 mountOrderRouter 的函數。接著,這個函數被用在 this.app.use 中,它是 Express.js 用來設置中介軟體和路由器的方法。
this.app.use('orders', mountOrderRouter({ controllerCtx: this.controllerCtx }))意思是,在 Express 應用程序中,將所有指向 /orders 路徑的請求交給 mountOrderRouter 函數處理,而 mountOrderRouter 函數會返回一個路由器實例。
controllerCtx: this.controllerCtx 是傳遞給 mountOrderRouter 的參數,通常是一些需要在路由處理器中用到的上下文信息。
※ 在postman驗證 :
輸入驗證資料:


驗證結果:


※ 資料驗證方式:Controller --> orderController.ts
1. 第一種驗證方式:
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"。
2.第二種驗證方式使用npm套件做資料驗證 — express validator:
安裝軟體:
npm install express validator使用express validator驗證:Controller --> orderController.ts
import { isEmpty } from "lodash";
import { body } from 'express-validator';
//1.資料驗證
public createOrderValidator = () => {
//驗證Provider
const paymentProviderValidator = (value: any) => {
return [PaymentProvider.ECPAY, PaymentProvider.PAYPAL].includes(value);
}
//驗證paymentWay
const paymentWayValidator = (value: any) => {
return [PaymentWay.CVS, PaymentWay.PAYPAL].includes(value);
}
//驗證contents資料細項
const contentValidator = (value: OrderContent[]) => {
if (isEmpty(value)) return false;
for (const product of value) {
if ([product.productId, product.amount, product.price].some((vail) => !vail))
return false;
}
return true;
}
return [
//設定驗證不同參數的內容是否合法
body("paymentProvider", "Invalid payment provider")
.custom(paymentProviderValidator)
]
}
}
程式碼解說:
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)) return 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 必須是非空陣列且符合內容驗證器規則。
使用createOrderValidator() 函數來驗證數據 :
export interface IOrderController
{ createOrderValidator(): ValidationChain[];//新增 }
程式碼解說:
定義方法結構:
- createOrderValidator() 方法被定義在 IOrderController 介面中,強制所有實現這個介面的類別都必須實現這個方法。
- 返回 ValidationChain[]:這表明方法會返回一組驗證規則,這些規則用於驗證訂單創建請求中的參數。
※ 加入中介層:router --> order.ts

router.post(
'/create',
//middleware中介層
controllerCtx.orderController.createOrderValidator(),//新增
//controller create Order正式的內容
controllerCtx.orderController.createOrder)
程式碼解說:
- 中介層(middleware)使用:
- 在設置路由時,將這個驗證方法作為中介層,確保每次收到請求時都先進行數據驗證。
- 處理邏輯:如果驗證通過,則由 createOrder 方法處理請求。
驗證數據是否錯誤 :
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 對象,包含所有驗證錯誤。
驗證結果:

※ transaction 資料庫交易主要流程:
- 開始交易:使用 knex.transaction() 開始交易。
- 設置隔離級別:如果提供了 isolation,則設置交易隔離級別。
- 執行回調函數:調用回調函數執行具體的交易操作。
- 提交事務:如果回調函數成功,提交交易。
- 處理錯誤:如果回調函數失敗,回滾交易並根據錯誤類型進行重試或拋出異常。
- 重試機制:在特定錯誤(如死鎖)發生時,進行重試,直到達到最大重試次數。
※ 將資料寫入資料庫:transactionHandler交易函數
※ transaction 資料庫交易功能:utils ––>index.ts

// transaction 資料庫交易功能
export const transactionHandler = async <T = any>(
//傳入參數
knex: Knex,
callback: (trx: Knex.Transaction) => Promise<T>,
options: {
retryTimes?: number,
maxBackOff?: number,
isolation?: ISOLATION_LEVEL;
} = {}
) => {
const { retryTimes = 100, maxBackOff = 1000, isolation } = options;
let attempts = 0;
//開啟transaction
const execTransaction = async (): Promise<T> => {
const trx = await knex.transaction();
//執行外面傳進來的callback function
try {
if (isolation) await trx.raw(`SET TRANSACTION ISOLATION_LEVEL SERIALIZABLE`)
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();
};
};
//將transaction執行結果傳出去
return await execTransaction();
};
程式碼解說:
1.knex: Knex- knex 是一個資料庫連接實例,用於執行 SQL 查詢。
2.callback: (trx: Knex.Transaction) => Promise<T>- callback 是一個帶有交易 (transaction) 物件 trx 的回呼函式 (callback function)。這個回呼函式需要返回一個 Promise,並且可以執行多個資料庫操作。
3.options = { isolation?: ISOLATION_LEVEL }
- retryTimes?: number:最大重試次數,預設值為 100。
- maxBackOff?: number:最大回退時間(毫秒),預設值為 1000。
- options 是一個選用的參數物件,可以指定交易的隔離級別。isolation 可以是不同的隔離級別,定義了交易之間的隔離程度。
4.解構選項參數:
const { retryTimes = 100, maxBackOff = 1000, isolation } = options;
程式碼解說:
- 利用解構賦值 (destructuring assignment) 從
options物件中提取retryTimes,maxBackOff, 和isolation三個屬性。 - retryTimes:設置重試次數,如果未提供則默認為 100。
- maxBackOff:設置最大回退時間(毫秒),如果未提供則默認為 1000。
- isolation:事務隔離級別,默認為未設置。
5.初始化重試計數器:
let attempts = 0;
程式碼解說:
- 初始化 attempts(追蹤或交易重試的次數) 變數為 0,表示尚未進行任何重試。
- 當交易處理失敗並需要重試時,attempts 會自增,用來追蹤已進行的重試次數。
6.開始一個新的資料庫交易:
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。
7.將transaction執行結果傳出去:
//將transaction執行結果傳出去
return await execTransaction();
程式碼解說:
等待execTransaction函數執行完成,然後將其結果返回給調用者。
※ 定義資料隔離性介面:
enum ISOLATION_LEVELS {
READ_UNCOMMITTED = 'READ_UNCOMMITTED',
READ_COMMITTED = 'READ_COMMITTED',
REPEATABLE_READ = 'REPEATABLE_READ',
SERIALIZABLE = 'SERIALIZABLE'
}
程式碼解說:
- READ_UNCOMMITTED:
- 讀取未提交:允許一個事務讀取另一個事務尚未提交的變更。這種隔離級別風險最高,因為會有「髒讀」的問題。
- READ_COMMITTED:
- 讀取已提交:只允許讀取已提交的變更,防止「髒讀」,但可能會有「不可重複讀」的問題,即同一事務中讀取的數據可能會改變。
- REPEATABLE_READ:
- 可重複讀:確保同一事務中的多次讀取結果一致,防止「髒讀」和「不可重複讀」,但可能會有「幻讀」問題,即在事務期間其他事務新增的記錄會被讀取到。
- SERIALIZABLE:
- 可序列化:最高級別的隔離,所有事務按順序執行,防止「髒讀」、「不可重複讀」和「幻讀」。這種隔離級別會帶來較高的性能開銷。
※ 新增sleep 函數:用於在重試前等待指定時間。
function sleep(maxBackOff: number) {
return new Promise((resolve) => setTimeout(() => resolve(1), maxBackOff));
}
程式碼解說:
- maxBackOff:表示延遲的時間,單位為毫秒。
- setTimeout 用於設置一個定時器,在 maxBackOff 毫秒後執行回調函數。
- 回調函數中調用 resolve(1),解決(resolve)這個承諾,表示延遲結束。





















