第三方金流串接 – 開立訂單

閱讀時間約 24 分鐘

※ 利用transactionHandler將資料寫入database

● 在orderController.ts檔案中使用 try...catch 區塊 

try {

} catch (err) {
// 處理錯誤
res.status(500).json({ errors: err })
throw err;
}

程式碼解說:

1.try 區塊

區塊內的程式碼表示你希望執行的資料庫操作、網路請求等,但可能會引發錯誤。

2.catch 區塊

  • 當 try 區塊中的程式碼引發錯誤時,程式控制會跳轉到 catch 區塊。
  • 錯誤處理:在 catch 區塊內,你可以處理錯誤,這裡展示了如何向客戶端返回一個 HTTP 500 狀態碼,表示伺服器內部錯誤,並將錯誤訊息以 JSON 格式返回給客戶端。
  • 重新拋出錯誤:throw err; 這行將錯誤重新拋出,以便讓上層的錯誤處理邏輯能夠捕獲並處理這個錯誤。

●  Knex.js 的資料庫連接:

try {
await transactionHandler(this.knexSql)
}

程式碼解說:

  • await:這個關鍵字用來等待一個返回 Promise 的異步操作完成,並獲取其返回值。在這段程式碼中,await 會使程式碼暫停執行,直到 transactionHandler 完成。
  • transactionHandler(this.knexSql):呼叫 transactionHandler 函數並傳入 this.knexSql 作為參數。這個函數會處理所有與資料庫事務相關的邏輯。

● 在utlis檔案中的index.ts新增 執行execTransaction的結果

const execTransaction = async (): Promise<T> => {
const trx = await knex.transaction();

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();
}
};
//新增 執行execTransaction的結果
return await execTransaction();

程式碼解說:

  • 調用 execTransaction 函數,並等待其完成後返回結果。

● return await execTransaction();和transactionHandler是什麼關係

1.transactionHandler 函數:

這個函數是用來處理整個交易邏輯的框架。它設置了重試機制和異常處理,確保在交易過程中任何操作失敗時能夠進行重試。

2.execTransaction

這個內部函數實際上執行了交易操作,包括開始交易、執行回調函數、提交或回滾交易。

3.return await execTransaction();

這行代碼在 transactionHandler 的最後,確保呼叫 execTransaction 並等待其完成,然後將結果返回給調用者。

  • transactionHandler使用execTransaction  來實際執行交易操作。
  • transactionHandler 則作為外部接口,調用和管理 execTransaction 的執行。

● 使用 await 來處理非同步操作

try {

await transactionHandler(this.knexSql, async (trx: Knex.Transaction)

)}

程式碼解說:

  1. async 關鍵字:
    • 定義這個函數為非同步函數,使得函數可以使用 await 關鍵字來處理非同步操作。
    • 非同步函數返回一個 Promise,這允許在交易完成或失敗時進行處理。
  2. (trx: Knex.Transaction) 參數:
    • trx:代表交易物件,用來在交易中執行所有資料庫操作。這確保了所有操作都在一個交易上下文中,並在交易結束時進行提交或回滾。
    • Knex.Transaction:定義 trx 的類型,保證它是 Knex.js 提供的交易物件,具有交易的所有方法和屬性。

● 將content(產品資訊) 內的資料寫進preOrder(預購)

try {

await transactionHandler(this.knexSql, async (trx: Knex.Transaction) => {
//對裡面每一個項目product做寫入database動作
contents.map(product) => this.productModel.
}

)}

程式碼解說:

contents.map:

    • contents 是一個陣列,其中包含多個產品物件。
    • map 方法用來遍歷 contents 陣列中的每個 product,並對每個 product 執行一個回調函數。

● 更新 preOrder(產品的預售資料),使用 productModel.preSell 方法

新增productModel:

/production的型別和定義-新增(對伺服器來說是做預售的動作)
export interface IProductModel extends IBase<Product> {
preSell(product: Pick<Product, "id" | "amount" | "price">, trx?: Knex.Transaction)
: Promise<Boolean>;
}

程式碼解說:

1.介面 IProductModel:

主要用來定義產品模型所需的方法和屬性。

2.擴展自 IBase<Product>

  • IBase 是一個基礎介面,通常定義了常見的 CRUD 操作(Create、Read、Update、Delete)。
  • IProductModel 擴展了這個基礎介面,表示它繼承了 IBase 所定義的所有方法和屬性。

3.preSell 方法,用於處理預售檢查:

  • 方法宣告:preSell「指的是方法的唯一標識符」(product: Pick<Product, "id" | "amount" | "price">)「指的是接受的參數」: Promise<Boolean>「指的是返回值類型」。
  • 參數 product:
    • 這個參數使用了 Pick 工具類型,表示從 Product 類型中選擇了 id、amount 和 price 三個屬性。
    • product 是一個包含這些屬性的物件。

參數 trx(可選)

  • trx?: Knex.Transaction 表示 trx 是一個可選的參數,用於在交易中進行操作。
  • Knex.Transaction 是 Knex.js 中的交易物件,確保操作在交易上下文中執行
  • 返回值:
    • 這個方法返回一個 Promise,其解析值為 Boolean 類型。
    • Promise<Boolean> 表示這個方法是非同步的。

● 實作對外presell

public preSell = async 
(product: Pick<Product, "id" | "amount" | "price">, trx?: Knex.Transaction)
=> {
//從資料庫裡面找資料
let queryBuilder = this.knexSql(this.tableName)
.where({ id: product.id })
.where(this.schema.amount, '>=', product.amount)
.update(
this.schema.preOrder,
this.knexSql.raw(`?? + ?`, [this.schema.preOrder, product.amount])
);

if (trx) queryBuilder = queryBuilder.transacting(trx);

const result = await queryBuilder;
//成功update就會是大於一的數字
return !!result;
}

程式碼解說:

1.方法宣告

  • product:這個參數是一個物件,包含 id、amount 和 price 屬性,代表產品的預售資訊。
  • trx:這個可選參數是 Knex.js 交易物件,用於在事務中進行操作。

2.從資料庫裡面找資料並進行更新

let queryBuilder = this.knexSql(this.tableName)
.where({ id: product.id })
.where(this.schema.amount, '>=', product.amount)
.update(
this.schema.preOrder,
this.knexSql.raw(`?? + ?`, [this.schema.preOrder, product.amount])
);
  • 建立一個 SQL 查詢,進行條件篩選並更新資料庫中的資料。
  • ?? 和 ? 是 Knex.js 的占位符,用來動態插入資料表名稱和數值。
  • ??:用於插入資料庫欄位名,這裡插入的是 this.schema.preOrder。
  • ?:用於插入數值,這裡插入的是 product.amount。
  • raw 是 Knex.js 提供的一個方法,用於執行原始 SQL 查詢或建構複雜的 SQL 表達式。

3.是否在交易中進行操作

if (trx) queryBuilder = queryBuilder.transacting(trx);
  • 如果提供了 trx 參數,將查詢設定為在這個交易中進行。

4.執行查詢並取得結果

const result = await queryBuilder;

5.返回結果

return !!result;
  • 這行確保返回一個布林值,表示更新操作是否成功。成功更新時,結果應該是一個大於零的數字,表示受影響的行數。

● 對裡面每一個項目product做寫入database動作

const results = await Promise.all(
//對裡面每一個項目product做寫入database動作
contents.map(
async (product) =>
await this.productModel.preSell(
{
id: product.productId,
...pick(product, ['price', 'amount'])
},
trx
)));

1.Promise.all

const results = await Promise.all()

程式碼解說:

  • Promise.all 用來同時執行多個非同步操作,並等待所有這些操作完成。
  • 返回一個包含每個操作結果的陣列。

2.非同步函數

async (product) => await this.productModel.preSell()

程式碼解說:

  • 定義了一個非同步函數 async (product) =>,並立即調用 preSell 方法。
  • 這個非同步函數接收一個 product 作為參數,表示 contents 陣列中的每一個產品物件。

3.呼叫 preSell 方法

await this.productModel.preSell(
{
id: product.productId,
...pick(product, ['price', 'amount'])
},
trx
)

程式碼解說:

  • 參數:
    • 傳遞一個物件 { id: product.productId, ...pick(product, ['price', 'amount']) },其中包含產品的 id、price 和 amount 屬性。
    • trx 是可選的交易物件,用來確保操作在交易上下文中執行。
  • pick 函數:
    • pick(product, ['price', 'amount']) 用來選擇 product 物件中的 price 和 amount 屬性。
    • 這樣生成的物件包括 id, price, 和 amount。
    • pick 被用來選擇 product 物件中的 price 和 amount 屬性,然後將這些屬性與 id 一起傳遞給 preSell 

● 檢查 results 陣列中的任意一個元素是否為 false:

if (results.some(result => !result))
throw new Error('Cannot buy, because out of stuff.');

程式碼解說:

  1. results.some(result => !result):
    • results:這是一個布林值陣列,表示每個產品的預售結果。
    • some 方法:some 方法用來測試陣列中的某些元素是否通過由提供的函數實現的測試。只要有一個元素通過測試,就會返回 true。
    • 箭頭函數 result => !result:這是一個測試函數,用來檢查陣列中的元素是否為 false。如果有任何 result 的值是 false,那麼 result => !result 就會返回 true。
  2. throw new Error('Cannot buy, because out of stuff.');:
    • throw:關鍵字用來拋出一個異常。
    • new Error:創建一個新的錯誤物件,錯誤訊息為 'Cannot buy, because out of stuff.'。
    • 這表示如果有任何一個產品的預售結果為 false,程式碼會拋出一個錯誤,中止操作並向使用者提供相關訊息。

● 計算 contents 陣列中所有產品的總價格

1.reduce 方法:

const totalPrice = contents.reduce()

程式碼解說:

    • reduce 方法用來遍歷陣列中的每個元素,並將它們累加成單一的值。
    • 語法:array.reduce(callback, initialValue)。

2.回調函數

(acc, product) => acc + product.price * product.amount

程式碼解說:

  • acc(累加器):用來累加每個產品的總價格。
  • product:當前正在遍歷的產品物件。
  • 返回值:回調函數的返回值是 acc + product.price * product.amount,表示將當前產品的價格乘以數量後加到累加器中。

3.初始值 0

, 0

程式碼解說:

  • 這是累加器的初始值,表示從 0 開始累加。

※ 建立genUID(生成唯一識別碼),創造13位時間戳 + 7位額外字符 = 20位總長度

● 在utlis資料夾中的index.ts新增 genUID

export const genUID = () => {
// timestamp 13+7 = 20
const alpha = 'abcdefghij'//每個數字去轉換成英文的大小寫
const timestampStr = new Date().getTime().toString();
const code = timestampStr.split('') //['1', '2', '3',...]
.map((v, idx) => idx % 2 ? v : alpha[Number(v)]) //['1', 'b', '3',...]
.join('')

const id = uuid().split('-')[0];
return `${code}${id.substring(0, id.length - 1)}`;
}

1.定義字母映射

const alpha = 'abcdefghij';// 每個數字對應的字母

程式碼解說:

  • 用來將數字 0-9 對應為字母 'a'-'j'。

2. 獲取時間戳

const timestampStr = new Date().getTime().toString();

程式碼解說:

  • 獲取當前的時間戳並轉換為字符串形式。

3.時間戳轉換和字母映射

const code = timestampStr.split('')
.map((v, idx) => idx % 2 ? v : alpha[Number(v)])
.join('');

程式碼解說:

  • 將時間戳轉換為一個字符陣列。
  • 使用 map 方法遍歷陣列中的每個元素,對於索引為偶數的數字,將其轉換為對應的字母。
  • 最後將陣列重新轉換為字符串。

※ 安裝UUID是一個具體的標準化識別碼。

npm install uid


4.生成 UUID 部分

const id = uuid().split('-')[0];

程式碼解說:

  • uuid() 方法生成一個新的 UUID。
  • split('-') 方法將 UUID 按照 - 分隔符拆分成一個陣列。
  • [0] 用來提取陣列中的第一個元素,也就是 UUID 的前半部分。

※ 開立訂單:

● 在controller資料夾中的orderController.ts新增開立訂單

 const uid = genUID();
const orderData = {
id: uid,
total: totalPrice,
createdAt: new Date(),
updatedAt: new Date(),
paymentProvider,
paymentWay,
status: OrderStatus.WAITING,
contents,
};
// 傳遞 orderData 並包括 trx
await this.orderModel.create(orderData, trx);
res.json({ status: 'success' })

1.生成唯一識別碼 uid

const uid = genUID();

程式碼解說:

  • 呼叫 genUID 函數來生成一個唯一識別碼 uid,用於標識這個訂單。

2.建立訂單資料 orderData

const orderData = {
id: uid,
total: totalPrice,
createdAt: new Date(),
updatedAt: new Date(),
paymentProvider,
paymentWay,
status: OrderStatus.WAITING,
contents,
};

程式碼解說:

  • id: uid:使用生成的唯一識別碼。
  • total: totalPrice:訂單的總價格,先前計算得到。
  • createdAt: new Date():設定訂單的創建時間為當前時間。
  • updatedAt: new Date():設定訂單的更新時間為當前時間。
  • paymentProvider:支付提供者,例如 PayPal。
  • paymentWay:支付方式,例如信用卡。
  • status: OrderStatus.WAITING:訂單狀態設置為 "等待中"。
  • contents:訂單的內容,即包含的產品信息。

3.將訂單資料寫入資料庫

await this.orderModel.create(orderData, trx);

程式碼解說:

  • this.orderModel.create(orderData, trx):呼叫 create 方法,將 orderData 傳遞進去,並使用 trx 交易物件來確保這些操作在交易中進行。

4.返回成功訊息

res.json({ status: 'success' });
  • 向客戶端返回一個 JSON 格式的成功訊息,表示訂單創建成功。

※ 確認結果:

postman:

raw-image

MySQL:

raw-image













    全端網頁開發專業知識分享
    留言0
    查看全部
    發表第一個留言支持創作者!
    ※ 在orderController.ts檔案中定義createOrder:  public createOrder: IOrderController['createOrder'] = (req, res, _next) => { let { paymentProvider, paym
    ※ 訂單開立流程 → 完結的流程 前端資料驗證。 將商品的數量寫入(預扣)→ id。 利用ID去打第三方金流的API來產生第三方金流的訂單。 當使用者繳完錢之後,第三方金流他會打我們提供的update資訊的API。 ※ 從前端需要傳入的資料: 商品ID、商品數量、使用哪個payment
    ※ 路由設定: 在routes資料夾中,建立product.ts: ※ 設置產品相關的路由: 1.匯入模組: import express from 'express'; import { ControllerContext } from '@/manager/controllerM
    ※ 建立一個管理者的資料夾,負責建立不同的管理 — manager: ※ 在manager資料夾中,建立modelManager.ts: ※ 設定資料庫模型的管理: 匯入模組: import { IProductModel, ProductModel } from "@/model/prod
    ※ 認識MVC架構: ※ 認識lodash: Lodash 是一個很實用的 JavaScript 函式庫。它提供了很多預設的實用函數,可以更容易地處理數據、操作物件和字串,並控制函數的執行。目的是為了簡化重複性的代碼,減少開發時間和錯誤。 Lodash 中常用的功能: 陣列處理:_.map
    ※ 認識Knex.js: Knex.js是一個專門串接資料庫的抽象化層,它支援多種關聯式資料庫,包括 PostgreSQL、MySQL、MariaDB、SQLite3、Oracle 和 Amazon Redshift 等。好處是連接以上的資料庫時,可以直接使用Knex.js的語法,就會自動創建出
    ※ 在orderController.ts檔案中定義createOrder:  public createOrder: IOrderController['createOrder'] = (req, res, _next) => { let { paymentProvider, paym
    ※ 訂單開立流程 → 完結的流程 前端資料驗證。 將商品的數量寫入(預扣)→ id。 利用ID去打第三方金流的API來產生第三方金流的訂單。 當使用者繳完錢之後,第三方金流他會打我們提供的update資訊的API。 ※ 從前端需要傳入的資料: 商品ID、商品數量、使用哪個payment
    ※ 路由設定: 在routes資料夾中,建立product.ts: ※ 設置產品相關的路由: 1.匯入模組: import express from 'express'; import { ControllerContext } from '@/manager/controllerM
    ※ 建立一個管理者的資料夾,負責建立不同的管理 — manager: ※ 在manager資料夾中,建立modelManager.ts: ※ 設定資料庫模型的管理: 匯入模組: import { IProductModel, ProductModel } from "@/model/prod
    ※ 認識MVC架構: ※ 認識lodash: Lodash 是一個很實用的 JavaScript 函式庫。它提供了很多預設的實用函數,可以更容易地處理數據、操作物件和字串,並控制函數的執行。目的是為了簡化重複性的代碼,減少開發時間和錯誤。 Lodash 中常用的功能: 陣列處理:_.map
    ※ 認識Knex.js: Knex.js是一個專門串接資料庫的抽象化層,它支援多種關聯式資料庫,包括 PostgreSQL、MySQL、MariaDB、SQLite3、Oracle 和 Amazon Redshift 等。好處是連接以上的資料庫時,可以直接使用Knex.js的語法,就會自動創建出
    你可能也想看
    Google News 追蹤
    Thumbnail
    這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
    Thumbnail
    ※ 原本狀態:伺服器渲染 這是 MVC 架構下的 request / response 示意圖,在這張圖呈現的架構裡,畫面和資料都由同一個架構處理。 伺服器渲染流程: 瀏覽器針對特定網址送出請求。 路由器解析請求後,轉接給對應的 controller。 controller 按照要求,透過
    Thumbnail
    在前一篇我們已經成功地建立簽核表單及簽核節點並關聯回請假表單,而本篇會接著介紹如何管理簽核節點狀態並同步更新簽核表單狀態。
    Thumbnail
    本文介紹瞭如何在後端系統開發時設計不同表單的簽核流程,包括請假表單和採購表單。以及如何動態生成簽核表單,並建立簽核節點。另外還介紹瞭如何利用繼承來簡化簽核流程的設定。
    Thumbnail
    KSQL引擎, 串流形式的SQL? 聽了應該霧煞煞吧! 想像一下傳統的SQL, 是不是一個指令一個動作, 每發送一個指令之後就必須等到查詢/寫入…動作皆完成之後才回應, 然而在Streaming的應用上這顯然不太可行, 每分每秒都有資料流入的情境下, 資料的狀態都在變化, 假設我們一個指令一個動作,
    Thumbnail
    Request內容 package main import ( "fmt" "log" "net/http" "strings" ) func request(w http.ResponseWriter, r *http.Request) { //這些資訊是輸出到伺服器端的列印訊息
    Thumbnail
    Accept:用戶端能夠接收的內容類型。 Accept: text/plain, text/html Accept-Charset:瀏覽器可以接受的字元編碼集。 Accept-Charset: utf8 Accept-Encoding:指定瀏覽器可以支援的web伺服器返回內容壓縮編碼
    Thumbnail
    HTTP伺服器端 package main import ( "net/http" ) type Refer struct { handler http.Handler refer string } //實現中介軟體邏輯​ func (this *Refer) ServeHTTP(
    Thumbnail
    這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
    Thumbnail
    ※ 原本狀態:伺服器渲染 這是 MVC 架構下的 request / response 示意圖,在這張圖呈現的架構裡,畫面和資料都由同一個架構處理。 伺服器渲染流程: 瀏覽器針對特定網址送出請求。 路由器解析請求後,轉接給對應的 controller。 controller 按照要求,透過
    Thumbnail
    在前一篇我們已經成功地建立簽核表單及簽核節點並關聯回請假表單,而本篇會接著介紹如何管理簽核節點狀態並同步更新簽核表單狀態。
    Thumbnail
    本文介紹瞭如何在後端系統開發時設計不同表單的簽核流程,包括請假表單和採購表單。以及如何動態生成簽核表單,並建立簽核節點。另外還介紹瞭如何利用繼承來簡化簽核流程的設定。
    Thumbnail
    KSQL引擎, 串流形式的SQL? 聽了應該霧煞煞吧! 想像一下傳統的SQL, 是不是一個指令一個動作, 每發送一個指令之後就必須等到查詢/寫入…動作皆完成之後才回應, 然而在Streaming的應用上這顯然不太可行, 每分每秒都有資料流入的情境下, 資料的狀態都在變化, 假設我們一個指令一個動作,
    Thumbnail
    Request內容 package main import ( "fmt" "log" "net/http" "strings" ) func request(w http.ResponseWriter, r *http.Request) { //這些資訊是輸出到伺服器端的列印訊息
    Thumbnail
    Accept:用戶端能夠接收的內容類型。 Accept: text/plain, text/html Accept-Charset:瀏覽器可以接受的字元編碼集。 Accept-Charset: utf8 Accept-Encoding:指定瀏覽器可以支援的web伺服器返回內容壓縮編碼
    Thumbnail
    HTTP伺服器端 package main import ( "net/http" ) type Refer struct { handler http.Handler refer string } //實現中介軟體邏輯​ func (this *Refer) ServeHTTP(