200字
畢業專題小札記05:用 Gin 框架打造後端 API——從路由設計到請求處理的完整實作
2026-01-27
2026-01-27

小小商家一點靈的後端使用 Gin 框架LINE Bot SDK 製作,主要負責以下功能:

  • 提供前端:充當前端 LIFF App 的伺服器。

  • 轉送資料:將 Google 試算表中的資料轉送到 LIFF App 的點餐和訂單頁面,並將新訂單送進 Google 試算表。

  • 計算顧客價值指標:使用 Insyra 計算每位顧客的 RFM 和 CAI。

  • 推播訊息:推播 LINE 訊息給顧客。

Gin 框架

Gin 是 Go 語言生態系中最受歡迎的 Web 框架之一,自 2014 年發布以來就以其卓越的性能和簡潔的 API 設計受到開發者青睞。

為什麼選擇 Gin?

高性能

Gin 使用 httprouter 作為路由引擎,這是一個基於 Radix Tree(基數樹)實現的高效路由器。相較於其他框架,Gin 的路由查找速度極快,即使有成千上萬個路由,性能損耗也微乎其微。根據官方的基準測試,Gin 的性能比許多其他 Go Web 框架快 40 倍以上。

簡潔易用

Gin 的 API 設計非常直觀,學習曲線平緩。它提供了豐富的功能,但又不會讓人感到複雜。例如處理 JSON 請求只需要一行 c.ShouldBindJSON(),回傳 JSON 回應也只需要 c.JSON()

中間件支援

Gin 內建完善的中間件機制,可以輕鬆加入認證、日誌、CORS、錯誤處理等功能。中間件可以串接使用,靈活組合,而且執行順序清晰明確。

豐富的功能

  • 參數綁定:自動將 JSON、表單、URL 參數綁定到結構體。

  • 參數驗證:內建驗證器,可以驗證必填欄位、格式等。

  • 路由分組:方便組織和管理大量路由。

  • 檔案上傳:簡化檔案處理流程。

  • 自訂渲染:支援 HTML、JSON、XML 等多種格式。

活躍的社群

Gin 在 GitHub 上有超過 8 萬顆星,是 Go 語言最受歡迎的專案之一。這意味著:

  • 文件完整且持續更新

  • 遇到問題容易找到解決方案

  • 有大量的第三方中間件和擴充套件可用

  • 長期維護有保障

Gin 的核心概念

Context(上下文)

在 Gin 中,每個 HTTP 請求都會創建一個 gin.Context 物件。這個物件貫穿整個請求處理流程,包含了請求的所有資訊,也提供了回應的所有方法。例如:

func handler(c *gin.Context) {

    // 取得請求參數
    name := c.Query("name")           // URL 參數
    id := c.Param("id")                // 路徑參數

    // 解析 JSON
    var data MyStruct
    c.ShouldBindJSON(&data)

    // 回傳回應
    c.JSON(200, gin.H{"message": "success"})

}

路由與路由群組

Gin 支援 RESTful 風格的路由設計,並可以用群組來組織相關路由:

r := gin.Default()

// 單一路由
r.GET("/ping", pingHandler)

// 路由群組
api := r.Group("/api")
{
    api.GET("/users", getUsers)
    api.POST("/users", createUser)
}

中間件鏈

中間件會按照註冊順序執行,可以在請求到達 Handler 之前或之後進行處理:

r.Use(LoggerMiddleware())    // 第一個執行

r.Use(AuthMiddleware())      // 第二個執行

r.GET("/data", dataHandler)  // 最後執行

我為什麼選擇 Gin

對於小小商家一點靈這個專案,Gin 是最適合的選擇:

1. 效能需求:計算 RFM/CAI 需要處理大量資料,Gin 的高效能確保了快速回應。

2. 開發效率:專案時程有限,Gin 的簡潔 API 讓我能快速開發。

3. 中間件需求:需要實作 Bearer Token 認證,Gin 的中間件機制非常方便。

4. JSON 處理:與 Google Apps Script 和前端的溝通都使用 JSON,Gin 的 JSON 支援完善。

5. 靜態檔案服務:需要提供 LIFF App 的靜態檔案,Gin 內建這個功能。

相較於其他框架(如 Echo、Fiber),Gin 的生態系更成熟、文件更完整,對新手更友善,AI 也比較會使用(這才是重點😂)

專案架構

小小商家一點靈的後端架構如下:

backend/
├── main.go                    # 程式入口
├── internal/
│   ├── config/               # 讀取設定檔(環境變數、密鑰等)
│   │   └── config.go
│   ├── handlers/             # 處理 HTTP 請求
│   │   ├── handler.go       # 主要 Handler 結構
│   │   ├── data.go          # 資料相關的 API
│   │   ├── calculate.go     # RFM/CAI 計算
│   │   └── push.go          # LINE 訊息推播
│   ├── services/            # 業務邏輯層
│   │   └── line.go          # LINE Bot 相關服務
│   ├── models/              # 資料模型
│   │   ├── product.go
│   │   ├── category.go
│   │   ├── customer.go
│   │   └── order.go
│   ├── google_sheet/        # Google 試算表操作
│   │   ├── get.go           # 讀取資料
│   │   └── post.go          # 寫入資料
│   ├── middleware/          # 中間件
│   │   └── bearer_auth.go  # Bearer Token 認證
│   └── routes/              # 路由設定
│       └── routes.go

這種架構遵循分層設計的原則:

  • Handlers:處理 HTTP 請求和回應,負責參數驗證和格式轉換

  • Services:封裝業務邏輯,處理複雜的操作流程

  • Models:定義資料結構

  • Google Sheet:專門處理與 Google 試算表的互動

這樣的好處是每一層各司其職,程式碼清晰且容易維護。

主程式入口:main.go

package main

import (
    "log"
    "net/http"
    
    "lineliteshop1.0/internal/config"
    "lineliteshop1.0/internal/handlers"
    "lineliteshop1.0/internal/routes"
    
    "github.com/gin-gonic/gin"
    "github.com/line/line-bot-sdk-go/v8/linebot/messaging_api"
)

func main() {
    // 初始化 LINE Bot Messaging API 客戶端
    client := &http.Client{}
    bot, err := messaging_api.NewMessagingApiAPI(
        config.LINE_CHANNEL_TOKEN,
        messaging_api.WithHTTPClient(client),
    )
    if err != nil {
        log.Fatal(err)
    }
    
    // 初始化處理器(內部會自動初始化服務層)
    handler := handlers.NewHandler(bot)
    
    // 初始化 Gin 路由器(使用預設中間件)
    r := gin.Default()
    
    // 設定路由
    routes.SetupRoutes(r, handler)
    
    // 啟動 Gin 伺服器
    log.Println("伺服器啟動於 :10100 端口")
    log.Fatal(r.Run(":10100"))
}

主程式首先:

  1. 初始化 LINE Bot API 客戶端。

  2. 建立 Handler(處理器)。

  3. 建立 Gin 路由器。

  4. 設定所有路由。

  5. 啟動伺服器。

gin.Default() 會自動加入 Logger 和 Recovery 中間件,前者記錄每個請求,後者在程式出錯時避免整個伺服器崩潰。

路由設定:routes.go

接著我在 internal/routes/routes.go 中定義所有路由。

func SetupRoutes(r *gin.Engine, handler *handlers.Handler) {
    // 健康檢查端點
    r.GET("/", handler.HealthCheck)
    
    apiGroup := r.Group("/api")
    {
        // LINE Bot webhook 回調端點
        apiGroup.POST("/callback", handler.HandleWebhook)
        
        // Rich Menu 控制端點
        apiGroup.POST("/richmenu/switch/:userId/:richMenuId", handler.SwitchUserRichMenu)
        
        apiDataGroup := apiGroup.Group("/data")
        {
            // 分類相關
            apiDataGroup.GET("/category/:name", handler.HandleGetCategory)
            apiDataGroup.GET("/categories", handler.HandleGetCategories)
            
            // 商品相關
            apiDataGroup.GET("/products", handler.HandleGetProducts)
            
            // 顧客相關
            apiDataGroup.GET("/customer/:id", handler.HandleGetCustomer)
            apiDataGroup.GET("/customers", handler.HandleGetCustomers)
            apiDataGroup.POST("/customer", handler.HandleCustomerRegister)
            apiDataGroup.PUT("/customer", handler.HandleUpdateCustomer)
            
            // 訂單相關
            apiDataGroup.GET("/order/:id", handler.HandleGetOrder)
            apiDataGroup.GET("/orders", handler.HandleGetOrders)
            apiDataGroup.POST("/order", handler.HandlePostOrder)
            apiDataGroup.PUT("/order", handler.HandleUpdateOrder)
        }
        
        // 需要認證的端點
        apiGroup.POST("/calculate/:type", 
            middleware.BearerAuthMiddleware(config.GOOGLE_SHEET_API_TOKEN), 
            handler.HandleCalculate)
        apiGroup.POST("/line/message", 
            middleware.BearerAuthMiddleware(config.GOOGLE_SHEET_API_TOKEN), 
            handler.HandlePushMessage)
    }
    
    // LIFF 前端應用服務
    r.Static("/liff/assets", "./liff/dist/assets")
    r.StaticFile("/liff/vite.svg", "./liff/dist/vite.svg")
    r.GET("/liff", handler.ServeLIFF)
    r.NoRoute(handler.ServeLIFF)
}

Gin 的路由設定非常簡單。用 Group 可以把相關的路由組織在一起,並統一加上前綴。例如我把所有資料相關的 API 都放在 /api/data/ 底下。

關於認證:計算 RFM/CAI 和推播訊息這兩個功能比較敏感,我不希望任何人都能透過這個 API 白嫖我的伺服器算力,或是利用我的官方帳號推播訊息,所以加上了 Bearer Token 認證中間件。只有帶著正確 Token 的請求才能通過。這個 Token 會在 Google 試算表中定義,使用 Google Apps Script 讀取並包含在請求裡。

關於 LIFF 服務:後端不只提供 API,也負責提供 LIFF App 的靜態檔案。r.NoRoute(handler.ServeLIFF) 這行很重要,它讓所有找不到的路由都導向 index.html,讓前端處理路由。這是前端使用 SPA(Single Page Application)時必須的設定。

請求處理流程

當一個 HTTP 請求進入後端時,會經過以下處理流程:

請求 → Gin 路由器 → 中間件(可選)→ Handler → 回應

我們用一個實際的例子來說明。假設 Google Apps Script 要呼叫計算 RFM 的 API:

1. 請求進入
POST /api/calculate/rfm

Headers: 

  Authorization: Bearer abc123token

  Content-Type: application/json

Body:

  {

    "data": [[...]],

    "config": {...}

  }
2. Gin 路由器匹配

Gin 收到請求後,會根據 HTTP 方法(POST)和路徑(/api/calculate/rfm)來找對應的路由。

routes.go 中,這個請求會匹配到:

apiGroup.POST("/calculate/:type", 
    middleware.BearerAuthMiddleware(config.GOOGLE_SHEET_API_TOKEN), 
    handler.HandleCalculate)

這裡的 :type 是路徑參數,會被解析成 rfm

3. 執行中間件

在執行 Handler 之前,請求會先經過 BearerAuthMiddleware 中間件。

func BearerAuthMiddleware(token string) gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")

        // 檢查是否有 Authorization header
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing authorization header"})

            c.Abort()  // 終止請求,不會執行後面的 Handler
            return
        }

        // 驗證 token 格式和內容
        if len(authHeader) < 7 || authHeader[:7] != "Bearer " {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header format"})
            c.Abort()
            return
        }

        
        providedToken := authHeader[7:]

        if providedToken != token {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
            c.Abort()
            return
        }

        c.Next()  // token 正確,繼續執行下一個處理器
    }
}

如果 token 驗證失敗c.Abort() 會終止請求,直接回應錯誤,不會執行後面的 Handler。如果驗證成功c.Next() 會讓請求繼續往下走。

4. 執行 Handler

通過中間件後,請求進入 handler.HandleCalculate

func (h Handler) HandleCalculate(c gin.Context) {

    // 1. 取得路徑參數
    t := c.Param("type")  // 取得 "rfm"

    

    // 2. 解析 JSON 請求體
    var jsonData map[string]any

    if err := c.ShouldBindJSON(&jsonData); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    

    // 3. 資料轉換和處理
    data := jsonData["data"].([]any)

    // ... 轉換成 DataTable


    // 4. 根據 type 執行對應的計算
    switch t {
    case "rfm":

        // 呼叫 Insyra 計算 RFM
        result := mkt.RFM(dataTable, config)

        
        // 5. 回傳結果
        c.JSON(http.StatusOK, gin.H{
            "RFM": result.ColNamesToFirstRow().To2DSlice(),
        })
    }

}

Handler 的工作包括:

  • 取得路徑參數(:type)。

  • 解析請求體(JSON)。

  • 呼叫業務邏輯(計算 RFM)。

  • 格式化並回傳結果。

5. 回應

最後,Gin 會把 Handler 產生的回應送回給客戶端:

{

  "RFM": [

    ["CustomerID", "R_Score", "F_Score", "M_Score", "RFM_Score"],

    ["A001", 5, 4, 5, 14],

    ["A002", 3, 2, 3, 8],

    ...

  ]

}
中間件的作用

中間件就像是請求處理流程中的「檢查站」,可以在請求到達 Handler 之前或之後做一些事情。常見的用途包括:

  • 認證與授權:檢查使用者是否有權限存取這個 API。

  • 日誌記錄:記錄每個請求的資訊(gin.Default() 自動包含)。

  • 錯誤處理:捕捉錯誤避免伺服器崩潰(gin.Default() 自動包含)

  • CORS 設定:處理跨域請求。

  • 限流:防止 API 被濫用。

中間件可以串接多個,它們會按照設定的順序依次執行。例如:

apiGroup.POST("/calculate/:type",

    middleware.LoggerMiddleware(),      // 第一個中間件

    middleware.BearerAuthMiddleware(), // 第二個中間件

    handler.HandleCalculate)           // 最後才是 Handler

請求會先經過 Logger,再經過 Auth,最後才到 Handler。

資料 API:與 Google 試算表互動

讀取商品資料

這個 handler 負責把 Google 試算表中的商品資料傳給前端 LIFF App。

func (h *Handler) HandleGetProducts(c *gin.Context) {
    // 呼叫 Google Sheet API 獲取商品資料
    products, err := google_sheet.GetProducts()
    if err != nil {
        log.Printf("獲取商品資料失敗: %v", err)
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get products"})
        return
    }
    c.JSON(http.StatusOK, gin.H{
        "status":  "success",
        "message": "Products retrieved successfully",
        "data":    products,
    })
}

Handler 的職責很單純:接收請求、呼叫對應的函數、回傳結果。真正與 Google 試算表互動的邏輯都封裝在 google_sheet 套件裡。

新增訂單

這個 handler 則是把前端傳來的訂單送進 Google 試算表。

func (h *Handler) HandlePostOrder(c *gin.Context) {
    var order models.Order
    if err := c.ShouldBindJSON(&order); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    // 將訂單寫入 Google Sheet
    err := google_sheet.PostOrder(order)
    if err != nil {
        log.Printf("新增訂單失敗: %v", err)
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create order"})
        return
    }
    
    c.JSON(http.StatusCreated, gin.H{
        "status":  "success",
        "message": "Order created successfully",
    })
}

c.ShouldBindJSON(&order) 會自動把 JSON 請求體解析成 Order 結構,非常方便。

RFM/CAI 計算 API

這是後端最核心的功能之一,負責呼叫 Insyra 來計算顧客價值指標。

func (h *Handler) HandleCalculate(c *gin.Context) {
    t := c.Param("type")  // rfm、cai 或 all
    var jsonData map[string]any
    if err := c.ShouldBindJSON(&jsonData); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    // 將 JSON 資料轉換成 Insyra 的 DataTable
    data := jsonData["data"].([]any)
    var data2d [][]any
    for _, row := range data {
        data2d = append(data2d, row.([]any))
    }
    dataTable, err := insyra.Slice2DToDataTable(data2d)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    dataTable.SetRowToColNames(0)  // 將第一行設為欄位名稱
    
    config := jsonData["config"].(map[string]any)
    
    switch t {
    case "rfm":
        c.JSON(http.StatusOK, gin.H{
            "RFM": mkt.RFM(dataTable, mkt.RFMConfig{
                CustomerIDColName: config["customerIDColName"].(string),
                TradingDayColName: config["tradingDayColName"].(string),
                AmountColName:     config["amountColName"].(string),
                TimeScale:         mkt.TimeScaleDaily,
                DateFormat:        "yyyy/MM/dd HH:mm:ss",
                NumGroups:         2,
            }).ColNamesToFirstRow().To2DSlice(),
        })
    case "cai":
        c.JSON(http.StatusOK, gin.H{
            "CAI": mkt.CAI(dataTable, mkt.CAIConfig{
                CustomerIDColName: config["customerIDColName"].(string),
                TradingDayColName: config["tradingDayColName"].(string),
                TimeScale:         mkt.TimeScaleDaily,
                DateFormat:        "yyyy/MM/dd HH:mm:ss",
            }).ColNamesToFirstRow().To2DSlice(),
        })
    case "all":
        // 平行計算 RFM 和 CAI
        var rfmResult, caiResult [][]any
        calcRFM := func() {
            rfmResult = mkt.RFM(dataTable, mkt.RFMConfig{
                CustomerIDColName: config["customerIDColName"].(string),
                TradingDayColName: config["tradingDayColName"].(string),
                AmountColName:     config["amountColName"].(string),
                TimeScale:         mkt.TimeScaleDaily,
                DateFormat:        "yyyy/MM/dd HH:mm:ss",
                NumGroups:         2,
            }).ColNamesToFirstRow().To2DSlice()
        }
        calcCAI := func() {
            caiResult = mkt.CAI(dataTable, mkt.CAIConfig{
                CustomerIDColName: config["customerIDColName"].(string),
                TradingDayColName: config["tradingDayColName"].(string),
                TimeScale:         mkt.TimeScaleDaily,
                DateFormat:        "yyyy/MM/dd HH:mm:ss",
            }).ColNamesToFirstRow().To2DSlice()
        }
        parallel.GroupUp(calcRFM, calcCAI).Run().AwaitNoResult()
        c.JSON(http.StatusOK, gin.H{
            "RFM": rfmResult,
            "CAI": caiResult,
        })
    }
}

這個 API 是設計給 Google Apps Script 呼叫的,Google 試算表透過 Apps Script 把資料送來,後端計算完再把結果送回去。

這段程式把 Google Apps Script 傳過來的交易資料轉成 Insyra 的 DataTable,計算完 RFM 和 CAI 之後,再把計算結果的 DataTable 轉成切片,使用 JSON 格式回傳。

它可以單獨計算 RFM 或 CAI,也可以同時平行計算兩者。為了節省時間,減少 Google Apps Script 觸發器的運行時數(有每日上限限制),Google Apps Script 目前實際上是呼叫平行化的計算模式。

LINE Bot Webhook

這是保留給未來如果要讓程式自動回覆 LINE 訊息所需的程式,目前並沒有任何作用。

當使用者在 LINE 跟官方帳號互動時,LINE 伺服器會發送 Webhook 請求到我們的後端,由這邊的程式處理並回覆使用者。

func (h *Handler) HandleWebhook(c *gin.Context) {
    // 解析 LINE webhook 請求
    cb, err := webhook.ParseRequest(config.LINE_CHANNEL_SECRET, c.Request)
    if err != nil {
        log.Printf("解析 webhook 失敗: %v", err)
        c.JSON(http.StatusBadRequest, gin.H{"error": "Bad Request"})
        return
    }
    
    // 處理每一個 webhook 事件
    for _, event := range cb.Events {
        switch e := event.(type) {
        case webhook.MessageEvent:
            // 檢查是否為文字訊息
            if textMsg, ok := e.Message.(webhook.TextMessageContent); ok {
                // 處理文字訊息
                err := h.lineService.HandleTextMessage(e.ReplyToken, textMsg.Text)
                if err != nil {
                    log.Printf("處理文字訊息失敗: %v", err)
                }
            }
        case webhook.StickerMessageContent:
            // 處理貼圖訊息
            log.Println("收到貼圖訊息")
        default:
            log.Printf("未處理的事件類型: %T", e)
        }
    }
    
    c.JSON(http.StatusOK, gin.H{"status": "ok"})
}

Webhook 處理的流程:

  1. 驗證請求:使用 Channel Secret 驗證請求是否真的來自 LINE。

  2. 解析事件:一個請求可能包含多個事件。

  3. 分類處理:根據事件類型(文字、貼圖等)做不同處理。

  4. 回應 LINE:必須在一定時間內回應,否則 LINE 會認為請求失敗。

LINE 訊息推播

這個 API 也是設計給 Google Apps Script 呼叫的,我們到時候只要在 Google 試算表做依照顧客分群發送通知的功能,商家就可以在試算表點一點直接推播訊息給顧客。

func (h *Handler) HandlePushMessage(c *gin.Context) {
    var request struct {
        UserID  string `json:"userId"`
        Message string `json:"message"`
    }
    
    if err := c.ShouldBindJSON(&request); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    err := h.lineService.PushTextMessage(request.UserID, request.Message)
    if err != nil {
        log.Printf("推播訊息失敗: %v", err)
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to push message"})
        return
    }
    
    c.JSON(http.StatusOK, gin.H{
        "status":  "success",
        "message": "Message pushed successfully",
    })
}

Bearer Token 認證中間件

為了避免 API 被亂 call,消耗伺服器算力,或是亂發訊息,某些 API 需要認證才能使用。

我實作了一個簡單的 Bearer Token 中間件:

func BearerAuthMiddleware(token string) gin.HandlerFunc {
    return func(c *gin.Context) {
        authHeader := c.GetHeader("Authorization")
        
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Missing authorization header"})
            c.Abort()
            return
        }
        
        // 檢查是否為 Bearer token 格式
        if len(authHeader) < 7 || authHeader[:7] != "Bearer " {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header format"})
            c.Abort()
            return
        }
        
        providedToken := authHeader[7:]
        
        if providedToken != token {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
            c.Abort()
            return
        }
        
        c.Next()
    }
}

使用方式:

apiGroup.POST("/calculate/:type", 
    middleware.BearerAuthMiddleware(config.GOOGLE_SHEET_API_TOKEN), 
    handler.HandleCalculate)

如果 Token 不正確,請求就會被 c.Abort() 中斷,不會執行後面的 Handler。

提供 LIFF 靜態檔案

後端不只是 API 伺服器,也要提供 LIFF App 的靜態檔案。

func (h *Handler) ServeLIFF(c *gin.Context) {
    // 獲取請求的文件路徑
    filePath := c.Param("filepath")
    
    // 如果沒有指定文件路徑,默認提供 index.html
    if filePath == "" || filePath == "/" {
        c.File("./liff/dist/index.html")
        return
    }
    
    // 構建完整的文件路徑
    fullPath := filepath.Join("./liff", "dist", filePath)
    
    // 檢查文件是否存在
    if _, err := http.Dir(".").Open(fullPath); err != nil {
        // 如果文件不存在,返回 index.html(SPA 路由支持)
        c.File("./liff/dist/index.html")
        return
    }
    
    // 根據文件副檔名設定正確的 MIME 類型
    switch filepath.Ext(filePath) {
    case ".js":
        c.Header("Content-Type", "application/javascript")
    case ".css":
        c.Header("Content-Type", "text/css")
    case ".html":
        c.Header("Content-Type", "text/html")
    case ".svg":
        c.Header("Content-Type", "image/svg+xml")
    }
    
    // 提供靜態文件
    c.File(fullPath)
}

這個函數的重點在於支援 SPA 的路由。當使用者訪問 /liff/order 這種路徑時,實際上這個檔案並不存在,所以要回傳 index.html,讓前端的 Vue Router 來處理路由。

總結

小小商家一點靈的後端架構清晰,職責分明:

  • Gin 框架:處理 HTTP 請求和路由。

  • Handlers:驗證請求、格式轉換。

  • Services:封裝業務邏輯。

  • Google Sheet:資料儲存和讀取。

  • Insyra:計算 RFM 和 CAI。

  • LINE Bot SDK:與 LINE 平台互動。

這樣的架構讓每個模組都可以獨立測試和維護,也方便之後擴充新功能。例如如果要換掉 Google 試算表,改用真正的資料庫,只需要修改 google_sheet 套件,其他部分幾乎不用動。


完整程式碼:https://github.com/TimLai666/lineliteshop1.0

本篇文章使用的 Insyra 版本:v0.2.13

評論