← Back to list

飲料訂購 APP — Go 後端專案架構與運作邏輯整理

這篇文章紀錄目前飲料訂購 App 的 Go 後端架構,以及 App 送出訂單後,後端如何處理請求、驗證資料、計算金額,最後寫入 MySQL。

AndyLin · 2026-06-13 08:54 · 0 claps · 13.2 min read
#go #ordering-system #mysql
Open on Medium ↗

飲料訂購 APP — Go 後端專案架構與運作邏輯整理

這篇文章紀錄目前飲料訂購 App 的 Go 後端架構,以及 App 送出訂單後,後端如何處理請求、驗證資料、計算金額,最後寫入 MySQL。

這個專案主要目標是提供一組飲料訂購 API,讓前端 App 可以完成會員註冊登入、查詢飲料、建立訂單、查詢訂單與更新訂單狀態。

一、專案整體架構

目前 Go 後端專案大致分成以下幾個部分:

go-drinkproject
├── cmd
│   └── server
│       └── main.go
├── internal
│   ├── auth
│   │   ├── password.go
│   │   └── token.go
│   ├── handlers
│   │   ├── auth_handler.go
│   │   ├── drink_handler.go
│   │   ├── order_handler.go
│   │   └── response.go
│   ├── models
│   │   └── models.go
│   ├── router
│   │   └── router.go
│   └── store
│       ├── store.go
│       ├── memory_store.go
│       └── mysql_store.go
├── schema.sql
├── go.mod
└── README.md

整個後端想成幾層:

cmd/server :程 式進入點,啟動 HTTP server

router : 設定 API 路由

handlers : 接收 HTTP request、解析 JSON、回傳 response

models:定義資料格式,例如 Drink、Order、User

store:資料存取層,負責操作 MySQL 或記憶體資料

auth:密碼加密與 JWT token 相關邏輯

schema.sql:MySQL 資料表結構

這樣拆分的好處是,每一層的責任清楚。

Handler 不需要知道 SQL 怎麼寫,只要呼叫 store

store 不需要知道 HTTP 狀態碼,只負責資料操作。

二、Server 啟動流程

後端入口在 cmd/server/main.go

程式啟動後,會先檢查環境變數 MYSQL_DSN

如果有設定 MYSQL_DSN,代表要連接 MySQL:

mysqlStore, err := store.NewMySQLStore(store.NormalizeDSN(dsn))

如果沒有設定,後端會使用 MemoryStore

return store.NewMemoryStore()

也就是說,目前專案支援兩種資料來源:

MemoryStore : 資料存在記憶體,server 重開後資料會消失,適合測試

MySQLStore : 資料存在 MySQL,server 重開後資料仍會保留,適合正式使用

這個設計透過 Store interface 完成。

Handler 不需要知道背後是 MySQL 還是記憶體,只要呼叫共同定義的方法

例如:

CreateOrder(req models.CreateOrderRequest)
ListOrders()
GetOrder(id int)
UpdateOrderStatus(id int, status models.OrderStatus)

這是目前架構中很重要的一個設計:用 interface 隔離資料來源。

三、Router 如何分配 API

API 路由定義在 internal/router/router.go

目前主要路由有:

GET    /health

POST   /api/auth/register
POST   /api/auth/login
GET    /api/me

GET    /api/drinks
GET    /api/drinks/{id}

POST   /api/orders
GET    /api/orders
GET    /api/orders/{id}
PATCH  /api/orders/{id}/status

Router 的工作是把不同 URL 分配給不同 handler。

例如:

mux.HandleFunc("/api/orders", orderHandler.Orders)
mux.HandleFunc("/api/orders/", orderHandler.OrderByID)

當 App 呼叫 POST /api/orders 時,請求會進入 order_handler.go 裡的 Orders 方法,接著再依照 HTTP method 判斷要建立訂單或查詢訂單。

四、Models 定義資料格式

internal/models/models.go 定義了後端主要使用的資料格式。

例如飲料:

type Drink struct {
    ID          int    `json:"id"`
    Name        string `json:"name"`
    Description string `json:"description"`
    Price       int    `json:"price"`
    ImageURL    string `json:"image_url"`
    Category    string `json:"category"`
    IsAvailable bool   `json:"is_available"`
}

訂單:

type Order struct {
    ID           int         `json:"id"`
    CustomerName string      `json:"customer_name"`
    Phone        string      `json:"phone"`
    Items        []OrderItem `json:"items"`
    TotalPrice   int         `json:"total_price"`
    Status       OrderStatus `json:"status"`
    CreatedAt    time.Time   `json:"created_at"`
}

建立訂單時,前端送來的格式是:

type CreateOrderRequest struct {
    CustomerName string                   `json:"customer_name"`
    Phone        string                   `json:"phone"`
    Items        []CreateOrderItemRequest `json:"items"`
}

其中每個品項只需要送:

type CreateOrderItemRequest struct {
    DrinkID   int    `json:"drink_id"`
    Quantity  int    `json:"quantity"`
    Sweetness string `json:"sweetness"`
    IceLevel  string `json:"ice_level"`
}

這裡有一個重要設計:前端不用送飲料名稱、單價或總價。 這些資料都由後端根據 MySQL 裡的飲料資料計算。

五、建立訂單的完整流程

當 App 送出一筆飲料訂單時,流程如下:

App
 ↓
POST /api/orders
 ↓
router.go
 ↓
order_handler.go
 ↓
validateCreateOrderRequest
 ↓
store.CreateOrder
 ↓
MySQL transaction
 ↓
查 users
 ↓
查 drinks
 ↓
計算 subtotal / total_price
 ↓
寫入 orders
 ↓
寫入 order_items
 ↓
commit
 ↓
查回完整訂單
 ↓
回傳 JSON 給 App

1. App 送出訂單

前端會送出類似這樣的 JSON:

{
  "customer_name": "Andy",
  "phone": "0912345678",
  "items": [
    {
      "drink_id": 1,
      "quantity": 2,
      "sweetness": "半糖",
      "ice_level": "少冰"
    }
  ]
}

2. Handler 解析與驗證資料

order_handler.go 會先把 request body 解析成 CreateOrderRequest

接著驗證:

  • customer_name 不能是空字串
  • phone 不能是空字串
  • items 不能是空陣列
  • drink_id 必須大於 0
  • quantity 必須大於 0

如果驗證失敗,後端會直接回傳 400 Bad Request

3. Store 開始建立訂單

驗證通過後,handler 會呼叫:

h.store.CreateOrder(req)

如果目前使用 MySQL,實際會進入 mysql_store.goCreateOrder

六、MySQL 寫入邏輯

建立訂單時,後端會使用 transaction。

原因是建立訂單不是只寫一張表,而是至少會寫入:

  1. orders
  2. order_items

如果其中一個步驟失敗,整筆訂單就不應該成立。 所以後端使用 transaction 確保資料一致性。

tx, err := s.db.Begin()
defer tx.Rollback()

如果所有步驟成功,最後才會:

tx.Commit()

七、會使用到哪些資料表

目前 MySQL schema 中和訂單有關的資料表主要有四張:

users : 會員資料

drinks : 飲料商品資料

orders : 訂單主檔

order_items : 訂單明細

建立訂單時,真正新增資料的是:

orders : 一張訂單的主資料

order_items : 訂單中的每個飲料品項

另外會讀取:

users : 用 phone 找會員 ID

drinks : 用 drink_id 找飲料名稱、價格、是否可販售

八、orders 表的角色

orders 是訂單主檔。

它記錄一張訂單的總覽資料,例如:

CREATE TABLE IF NOT EXISTS orders (
  id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  order_no VARCHAR(32) NOT NULL UNIQUE,
  customer_name VARCHAR(80) NOT NULL,
  phone VARCHAR(30) NOT NULL,
  total_price INT UNSIGNED NOT NULL DEFAULT 0,
  status ENUM('pending', 'making', 'finished', 'canceled') NOT NULL DEFAULT 'pending',
  note VARCHAR(500) NOT NULL DEFAULT '',
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  user_id BIGINT NOT NULL,
  INDEX idx_orders_user_id (user_id)
);

當一張訂單建立時,後端會寫入:

  • order_no
  • customer_name
  • phone
  • total_price
  • status
  • user_id

其中 status 預設是:

pending

代表訂單已建立,但還沒有開始製作。

九、order_items 表的角色

order_items 是訂單明細。

一張訂單可以有多個品項,所以需要獨立一張表保存。

CREATE TABLE IF NOT EXISTS order_items (
  id INT AUTO_INCREMENT PRIMARY KEY,
  order_id BIGINT UNSIGNED NOT NULL,
  drink_id INT NOT NULL,
  drink_name VARCHAR(100) NOT NULL,
  quantity INT NOT NULL,
  sweetness VARCHAR(50) NOT NULL DEFAULT '',
  ice_level VARCHAR(50) NOT NULL DEFAULT '',
  unit_price INT NOT NULL,
  subtotal INT NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

假設一張訂單裡有:

  • 珍珠奶茶 2 杯
  • 檸檬紅茶 1 杯

那麼資料會長這樣:

orders : 1 筆

order_items : 2 筆

這樣設計可以讓一張訂單支援多種飲料,也方便未來做訂單明細查詢、統計熱銷品項、計算營收。

十、為什麼後端要自己計算價格

這是目前專案中很重要的安全設計。

前端 App 只送:

{
  "drink_id": 1,
  "quantity": 2
}

原因是前端資料不能完全信任。 使用者有可能攔截 request,自己把價格改成 1 元。

所以後端會用 drink_id 查 MySQL 的 drinks 表:

drink, err := getDrinkTx(tx, item.DrinkID)

然後使用資料庫中的價格計算:

subtotal := drink.Price * item.Quantity
totalPrice += subtotal

這樣可以確保最後寫入 orders.total_priceorder_items.subtotal 的金額是後端計算出來的,而不是前端隨便傳來的。

十一、為什麼 order_items 要保存 drink_name 和 unit_price

雖然 order_items 已經有 drink_id,但它仍然保存:

  • drink_name
  • unit_price

這樣做是為了保留「下單當下」的商品資訊。

假設今天珍珠奶茶是 60 元,使用者已經下單 明天店家把珍珠奶茶改成 65 元,如果舊訂單只靠 drink_id 去查 drinks.price,舊訂單金額就可能被新價格影響。

所以訂單明細會保存當下的名稱與單價 這樣即使未來飲料改名、漲價或下架,舊訂單仍然能正確顯示當時的資料。

十二、訂單狀態流程

目前訂單狀態定義在 models.go

const (
    OrderStatusPending  OrderStatus = "pending"
    OrderStatusMaking   OrderStatus = "making"
    OrderStatusFinished OrderStatus = "finished"
    OrderStatusCanceled OrderStatus = "canceled"
)

狀態含義大致是:

pending : 訂單剛建立,尚未開始製作

making : 店家正在製作

finished : 訂單完成

canceled : 訂單取消

更新訂單狀態的 API 是:

PATCH /api/orders/{id}/status

例如:

{
  "status": "making"
}

後端會檢查狀態是否合法,只有上述四種狀態可以使用。

十三、查詢訂單時的流程

查詢訂單列表時,後端會先查 orders

SELECT id, customer_name, phone, total_price, status, created_at
FROM orders
ORDER BY id DESC

接著每一張訂單再查自己的 order_items

SELECT drink_id, drink_name, quantity, sweetness, ice_level, unit_price, subtotal
FROM order_items
WHERE order_id = ?
ORDER BY id

最後組成完整的 JSON 回傳給 App。

這樣 App 拿到的資料就不是只有訂單主檔,而是包含完整品項:

{
  "id": 1,
  "customer_name": "Andy",
  "phone": "0912345678",
  "items": [
    {
      "drink_id": 1,
      "drink_name": "珍珠奶茶",
      "quantity": 2,
      "sweetness": "半糖",
      "ice_level": "少冰",
      "unit_price": 60,
      "subtotal": 120
    }
  ],
  "total_price": 120,
  "status": "pending",
  "created_at": "..."
}

十四、目前架構的優點

目前這個 Go 後端架構有幾個優點:

  1. 分層清楚 Router、Handler、Model、Store 各自負責不同事情,程式比較好維護。
  2. Store interface 彈性高 可以用 MySQL,也可以用 MemoryStore 測試。Handler 不需要改。
  3. 訂單資料設計合理 orders 存主檔,order_items 存明細,符合一般訂單系統設計。
  4. 價格由後端計算 避免前端竄改價格,安全性較好。
  5. 使用 transaction 建立訂單時避免只寫入一半資料。
  6. 保留下單當下資料 order_items 保存 drink_nameunit_price,避免未來飲料資料變動影響舊訂單。

메타데이터
post_id
17eefcd8ea9a
slug
飲料訂購-app-go-後端專案架構與運作邏輯整理-17eefcd8ea9a
url
https://medium.com/@croc0909/%E9%A3%B2%E6%96%99%E8%A8%82%E8%B3%BC-app-go-%E5%BE%8C%E7%AB%AF%E5%B0%88%E6%A1%88%E6%9E%B6%E6%A7%8B%E8%88%87%E9%81%8B%E4%BD%9C%E9%82%8F%E8%BC%AF%E6%95%B4%E7%90%86-17eefcd8ea9a
canonical_url
https://medium.com/@croc0909/%E9%A3%B2%E6%96%99%E8%A8%82%E8%B3%BC-app-go-%E5%BE%8C%E7%AB%AF%E5%B0%88%E6%A1%88%E6%9E%B6%E6%A7%8B%E8%88%87%E9%81%8B%E4%BD%9C%E9%82%8F%E8%BC%AF%E6%95%B4%E7%90%86-17eefcd8ea9a
author_url
https://medium.com/@croc0909
status
ok
fetched_at
2026-06-20 20:29:01