# 軟體設計文件(SDD) - 專案名稱:海大外送平台 - 撰寫日期:2025/11/10 - 發展者:呂翰昇、陳宥霖、陳婕熙、鄭靜怡、謝誌評 --- ## 版次變更記錄 | 版次 | 變更項目 | 變更日期 | | --- | -------- | -------- | | 0.1 | 初版| 2025/11/10| | 0.2 | C4 diagram 的 level 1 & level 2 | 2025/11/12| | 0.3 | 更新3、4、7、8|2025/11/22 | | 0.4 | 更新5、6、API介面清單 |2025/11/25 | | 0.5 | 修改C4、新增前端UML、實作框架補充 | 2025/12/03| | 0.6 |補充議題4、5 | 2025/12/23| | 0.7 |更改部署方式 |2025/12/24| --- ## 目錄 1. 系統模型與架構 (System Model / System Architecture) 2. 介面需求與設計 (Interface Requirement and Design) 3. 流程設計 (Process Design) 4. 使用者畫面設計 (User Interface Design) 5. 資料設計 (Data Design) 6. 類別圖設計 (Class Diagram) 7. 實作方案 (Implementation Languages and Platforms) 8. 設計議題 (Design Issue) --- ## 1. 系統模型與架構 (System Model / System Architecture) ![C4-level 3-container.drawio (2)](https://hackmd.io/_uploads/SyextnhTZ-x.png) ![component diagram](https://hackmd.io/_uploads/H1Tc33p-We.png) --- ## 2. 介面需求與設計 (Interface Requirement and Design) ### 2.1 API 介面清單 | 介面名稱 | 介面提供者 | 介面使用者 | 連結方式 | 輸入資料 (Request) | 輸出資料 (Response) | | -------- | ---------- | ---------- | -------- | ------------------- | -------------------- | | **UserLoginAPI** | AuthService 模組 | 前端登入頁面模組 | `HTTP POST /auth/login` | Body: `{ "email": string, "password": string }` | 200: `{ "data": { "token": string, "user": { "id": string, "email": string, "role": "customer \| deliverer \| restaurant" } } }` | | **GetRestaurantsAPI** | RestaurantService 模組 | 前端餐廳列表頁面模組 | `HTTP GET /restaurants` | Query 可選: `keyword`, `page`, `pageSize` | 200: `{ "data": [ { "id": string, "name": string, "imageUrl?": string, "address?": string, "phone?": string } ] }` | | **GetRestaurantMenuAPI** | RestaurantService 模組 | 前端餐廳菜單頁面模組 | `HTTP GET /restaurants/:id/menu` | Path: `id` = 餐廳 ID | 200: `{ "data": [ { "id": string, "name": string, "description": string, "price": number, "sizes": [], "spicinessOptions": [] } ] }` | | **CreateOrderAPI** | OrderService 模組 | 前端下單頁面模組 | `HTTP POST /orders` | Body: `{ "restaurantId": string, "items": [ { "menuItemId": string, "quantity": number, "size?": string, "spiciness?": string } ], "deliveryLocation": { "name": string, "lat?": number, "lng?": number }, "notes?": string, "requestedTime?": ISO8601 string }` | 201: `{ "data": { "id": string, "status": "available", "etaMinutes": number } }` | | **GetOrdersAPI** | OrderService 模組 | 前端訂單列表頁面模組 | `HTTP GET /orders?status=active\|history` | Query: `status = "active" \| "history"` | 200: `{ "data": [ { "id": string, "restaurantName": string, "status": string, "etaMinutes?": number, "placedAt": ISO8601 string } ] }` | | **GetOrderDetailAPI** | OrderService 模組 | 前端訂單詳情頁面模組 | `HTTP GET /orders/:id` | Path: `id` = 訂單 ID | 200: `{ "data": { "id": string, "restaurantId": string, "userId": string, "items": [], "status": string, "etaMinutes?": number, "placedAt": ISO8601 string, "requestedTime?": ISO8601 string, "deliveryLocation": { ... }, "notes?": string } }` | | **CancelOrderAPI** | OrderService 模組 | 前端訂單詳情頁面模組 | `HTTP PATCH /orders/:id/cancel` | Path: `id` = 訂單 ID | 200: `{ "data": { "status": "cancelled" } }` | | **GetAvailableDeliveriesAPI** | DeliveryService 模組 | 外送員 App「可接單列表」模組 | `HTTP GET /delivery/available` | Header: `Authorization: Bearer <token>` (角色為 deliverer) | 200: `{ "data": [ { "id": string, "code": string, "fee": number, "distanceKm": number, "etaMinutes": number, "merchant": { "name": string, "address": string, "lat": number, "lng": number }, "customer": { "name": string, "phone": string, "address": string } } ] }` | | **AcceptDeliveryAPI** | DeliveryService 模組 | 外送員 App「任務詳情」模組 | `HTTP POST /delivery/:id/accept` | Path: `id` = 任務 / 訂單 ID | 200: `{ "data": { "status": "assigned" } }` | | **GetActiveDeliveriesAPI** | DeliveryService 模組 | 外送員 App「進行中 / 歷史任務」模組 | `HTTP GET /delivery/active` | Header: `Authorization: Bearer <token>` | 200: 回傳該外送員進行中與歷史任務列表 `{ "data": [ ... ] }` | | **UpdateDeliveryStatusAPI** | DeliveryService 模組 | 外送員 App「任務詳情/狀態更新」模組 | `HTTP PATCH /delivery/:id/status` | Path: `id` = 任務 ID;Body: `{ "status": "en_route_to_pickup \| picked_up \| delivering \| delivered \| cancelled" }` | 200: `{ "data": { "status": "<status>" } }` | | **UpdateDeliveryLocationAPI** (可選) | DeliveryService 模組 | 外送員 App 背景定位上報模組 | `HTTP --- ## 3. 流程設計 (Process Design) ### 3.1 核心功能流程 ##### 3.1.1 使用者註冊與登入驗證 ![使用者註冊與登入驗證](https://hackmd.io/_uploads/r1psk7mZbe.png =45%x) ##### 3.1.2 顧客下單流程 ![顧客下單流程](https://hackmd.io/_uploads/rJO7gXQWZe.png =45%x) ##### 3.1.3 外送員接單流程 ![外送員接單流程](https://hackmd.io/_uploads/rkb8fX7WWx.png =45%x) ##### 3.1.4 餐廳接單流程 ![餐廳接單流程](https://hackmd.io/_uploads/BJ0sfQ7bZe.png =45%x) ##### 3.1.5 系統派單流程 ![系統派單流程](https://hackmd.io/_uploads/SkWk7XXWbg.png =45%x) ### 3.2 輔助功能流程 ##### 3.2.1 餐廳瀏覽與搜尋流程 ![餐廳瀏覽與搜尋流程](https://hackmd.io/_uploads/Byf1V7QbZx.png =45%x) ##### 3.2.2 菜單管理流程 ![菜單管理流程](https://hackmd.io/_uploads/Sy2rrXQbZl.png =45%x) ##### 3.2.3 購物車管理流程 ![購物車管理流程](https://hackmd.io/_uploads/SJZbkH7--e.png =45%x) ##### 3.2.4 訂單狀態追蹤流程 ![訂單狀態追蹤流程](https://hackmd.io/_uploads/B1SY4mXWWl.png =45%x) ### 3.3 補充處理功能 ##### 3.3.1 使用者評價流程 ![使用者評價流程](https://hackmd.io/_uploads/S1DdB77Z-g.png =45%x) ##### 3.3.2 歷史訂單查詢流程 ![歷史訂單查詢流程](https://hackmd.io/_uploads/Sks34XXW-x.png =45%x) --- ## 4. 使用者畫面設計 (User Interface Design) ![未命名設計](https://hackmd.io/_uploads/rysri1NZZe.png) --- ## 5. 資料設計 (Data Design) ### 5.1 資料模型 資料儲存在Mongodb atlas #### 5.1.1 使用者集合 (users) 儲存所有系統參與者的帳號資訊,包含消費者、外送員與餐廳管理員。 |欄位名稱 (Field)|資料型別 (Type)|說明 (Description)| |---|---|---| |id|String|使用者唯一識別碼用`oid.to_hex()`轉換(Primary Key) |email|String|登入信箱(Index)| |password|String|`hash()` |role|String|角色權限:customer, deliverer, restaurant| #### 5.1.2 餐廳集合 (restaurants) 儲存餐廳基本資訊。 |欄位名稱 (Field)|資料型別 (Type)|說明 (Description)| |---|---|---| |id| String |餐廳唯一識別碼 (PK)| |name| String| 餐廳名稱| |imageUrl| String |餐廳封面圖片 URL| |address| String| 餐廳地址| |phone| String |聯絡電話| #### 5.1.3 菜單項目集合 (menu_items) 儲存各餐廳中菜單的資訊。 |欄位名稱 (Field)|資料型別 (Type)|說明 (Description)| |---|---|---| |shop_id| String| 餐廳ID (Primary Key, Foreign Key)| |name| String| 餐點名稱 (Primary Key)| |description| String| 餐點描述| |price |numberInt |單價| |sizes| Array<String>| 尺寸選項 (如 ["Regular", "Large"])| |spicinessOptions |Array<String>| 辣度選項 (如 ["Mild", "Hot"])| |imageUrl |String |餐點圖片 URL| #### 5.1.4 訂單集合 (orders) 儲存訂單資訊。 |欄位名稱 (Field)|資料型別 (Type)|說明 (Description)| |---|---|---| |id| String |訂單唯一識別碼 (PK)| |restaurantId |String |餐廳 ID | |userId |String |買家 ID (Foreign Key)| |items |Array<Object> |訂購內容快照 (含餐點ID、數量、客製化選項)| |status| String| 訂單狀態 (available, delivering, delivered, cancelled)| |etaMinutes |Number |預計抵達時間 (分鐘)| |placedAt |Date| 下單時間| |requestedTime|Date|指定取餐時間 |deliveryLocation |Object |配送地點資訊 {name, lat, lng}| |notes |String |備註 (如:請放警衛室)| |statusTimeStamps|Obeject|`availible:Date` <br> `cancelled:Date` ... #### 5.1.5 外送任務集合 (delivery_tasks) 儲存訂單任務資訊。 |欄位名稱 (Field)|資料型別 (Type)|說明 (Description)| |---|---|---| |id |String |任務識別碼 (order_id)| |riderId| String| 接單外送員 ID | |status |String |配送狀態 (assigned, picked_up, delivering...)| |merchant |Object |商家資訊Snapshot {name, address, lat, lng}| |customer |Object| 客戶資訊Snapshot {name, phone, address}| |history| Array<Object> |狀態變更歷程 (含時間戳記與狀態)| ### 5.2 檔案結構 - 後端 ``` Expressing_server ├── Cargo.toml ├── .env └── src/ ├── main.rs ├── lib.rs └── routes/ ├── mod.rs ├── auth.rs ├── availible_delivery.rs ├── accept_delivery.rs ├── menu.rs ├── order.rs └── restaurants.rs ``` ### 5.3 XML / JSON 結構 前後端溝通採用 JSON 作為主要資料交換格式。以下展示結構範例。 #### 1. 通用回應格式 (Standard Response Envelope) 所有 API 請求皆遵循統一的回傳結構:成功時回傳 `data` 物件,失敗時回傳 `message` 與 `code`。 * **成功回應範例 (Success Response)** ```json { "data": { "token": "<jwt>", "user": { "id": "u123", "email": "user@example.com", "role": "customer" } } } ``` * **失敗回應範例 (Success Response)** ```json { "message": "invalid credentials", "code": "auth.invalid" } ``` #### 2. 餐廳與菜單 (Restaurants & Menu) * **餐廳列表物件** ```json { "data": [ { "id": "rest-001", "name": "Marina Burger", "imageUrl": "[https://example.com/logo.jpg](https://example.com/logo.jpg)" } ] } ``` * **菜單項目結構** ```json { "items": [ { "id": "menu-001", "name": "Burger", "description": "Delicious beef burger", "price": 180, "sizes": ["Regular", "Large"], "spicinessOptions": ["Mild", "Medium", "Hot"], "imageUrl": null } ] } ``` #### 3. 買家訂單 (Customer Orders) * **建立訂單請求 (Create Order Request)** ```json { "restaurantId": "rest-001", "items": [ { "menuItemId": "menu-001", "size": "Regular", "spiciness": "Mild", "addDrink": true, "quantity": 2 } ], "deliveryLocation": { "name": "資工系館" }, "notes": "請在警衛室交付", "requestedTime": "2025-11-23T10:30:00Z" } ``` #### 4. 外送員任務 (Delivery Tasks) * **可接單任務結構 (Task Object)** ```json { "data": [ { "id": "ord-001", "code": "A1-892", "fee": 85, "distanceKm": 1.2, "etaMinutes": 12, "merchant": { "name": "Marina Burger", "address": "基隆市中正區...", "lat": 25.0, "lng": 121.5 }, "customer": { "name": "王小明", "phone": "0912345678", "address": "基隆市..." } } ] } ``` * **更新狀態回應** ```json { "data": { "status": "assigned" //available -> assigned -> en_route_to_pickup -> picked_up -> delivering -> delivered } } ``` --- ## 6. 類別圖設計 (Class Diagram) ```mermaid classDiagram direction LR class User { +id: string +name: string +email: string +passwordHash: string +role: Role +phone?: string +createdAt: datetime +lastLoginAt?: datetime } class Customer { +defaultAddressId?: string +preferences?: string +rating?: float } class Deliverer { +availability: bool +vehicleType?: string +currentTaskId?: string +rating?: float } class RestaurantAdmin { +restaurantId: string } class Restaurant { +id: string +name: string +imageUrl?: string +address: Address +phone?: string +hours?: string +isOpen: bool +prepTimeMinutes?: int } class Address { +id: string +label: string +line1?: string +lat?: number +lng?: number } class MenuItem { +id: string +restaurantId: string +name: string +description: string +price: number +isAvailable: bool +imageUrl?: string } class MenuOptionGroup { +id: string +name: string +type: OptionType +required: bool } class MenuOption { +id: string +name: string +priceDelta: number +isDefault: bool } class Order { +id: string +restaurantId: string +customerId: string +status: OrderStatus +etaMinutes?: int +placedAt: datetime +requestedTime?: datetime +deliveryLocationName: string +notes?: string +total: number +deliveryFee?: number +serviceFee?: number } class OrderItem { +menuItemId: string +menuNameSnapshot: string +quantity: int +size?: string +spiciness?: string +options: string[] +unitPrice: number +subTotal: number } class StatusHistoryEntry { +status: string +at: datetime +note?: string } class Payment { +id: string +orderId: string +amount: number +method: string +status: PaymentStatus +txRef?: string +paidAt?: datetime +refundedAt?: datetime } class DeliveryTask { +id: string +orderId: string +restaurantId: string +delivererId?: string +status: DeliveryStatus +fee?: number +distanceKm?: number +etaMinutes?: int +pickupTime?: datetime +deliveredTime?: datetime +dropoffName: string } class Review { +id: string +orderId: string +restaurantId: string +delivererId?: string +reviewerId: string +score: int +comment?: string +createdAt: datetime } class Role { <<enumeration>> customer deliverer restaurant } class OrderStatus { <<enumeration>> available assigned en_route_to_pickup picked_up delivering delivered cancelled } class DeliveryStatus { <<enumeration>> available assigned en_route_to_pickup picked_up delivering delivered cancelled } class PaymentStatus { <<enumeration>> pending paid failed refunded } class OptionType { <<enumeration>> single multiple } User <|-- Customer User <|-- Deliverer User <|-- RestaurantAdmin RestaurantAdmin "1" --> "1" Restaurant : manages Restaurant "1" *-- "1" Address Customer "1" --> "0..*" Address Restaurant "1" *-- "*" MenuItem MenuItem "1" *-- "0..*" MenuOptionGroup MenuOptionGroup "1" *-- "*" MenuOption Customer "1" --> "*" Order : places Restaurant "1" --> "*" Order Order "1" *-- "*" OrderItem Order "1" o-- "1" Payment Order "1" --> "0..1" DeliveryTask Order "1" *-- "*" StatusHistoryEntry DeliveryTask "0..1" --> "1" Deliverer DeliveryTask "1" *-- "*" StatusHistoryEntry Order "1" --> "0..*" Review Review --> Restaurant Review --> Deliverer ``` ![image](https://hackmd.io/_uploads/SJYtPEXbbx.png) --- ## 7. 實作方案 (Implementation Languages and Platforms) - 平台:iOS App - 前端技術與框架:SwiftUI,SwiftData,MapKit - 後端技術與框架: - 語言:Rust - 框架:Axum - 主要函式庫與服務: - 資料庫:Mongodb atlas - 函式庫: ``` Rust [dependencies] axum = "0.8.6" tokio = { version = "1.48.0", features = ["full"] } serde = { version = "1.0.228", features = ["derive"] } hyper = "0.14" mongodb = "3.3.0" futures = "0.3" dotenv = "0.15" bcrypt = "0.12" jsonwebtoken = "8" tower-http = { version = "0.5", features = ["full"] } serde_json = "1.0.145" rand = "0.8" ``` - 部署方式:zeabur --- ## 8. 設計議題 (Design Issue) ### 議題 1:後端框架選擇 - **議題內容:** 本系統後端可採用 Python、Spring Boot 或 Rust,各框架在效能與開發成本上有些許差異,需要選擇最符合系統需求的方案。 - **可能解決方案:** 1. **Python(FastAPI / Django)** - 優點:熟悉語言、開發速度快 - 缺點:效能較弱、並發能力有限 2. **Rust(axum)** - 優點:高效能、高安全性、可精準掌控記憶體 - 缺點:學習難度高、開發時間較長 - **最後解決方案與理由:** 選擇 **Rust**,原因是系統需要高效能與穩定性;雖然 Rust 學習門檻較高,但能提升整體後端效能並具備長期延展性。 ### 議題 2:系統帳號 or 商家介面(Merchant Interface) - **議題內容:** 需決定系統是否提供「商家介面」讓餐廳自行管理菜單與訂單,或由系統管理者統一維護所有餐廳資訊。 - **可能解決方案:** 1. **系統管理者帳號** - 所有餐廳資料由管理者統一維護 - 權限單純、操作流程簡單 - 開發成本最低、維護容易 2. **商家介面(Merchant Interface)** - 商家可自行上架餐點、更新菜單與管理訂單 - 權限與資料控管較複雜 - 開發成本大幅增加(前後端皆需額外開發) - **最後解決方案與理由:** 餐點由商家自行上架、編輯,系統管理者頁面待實作。 ### 議題 3:「順路幫帶模式」或「固定取餐點模式」 - **議題內容:** 作為校園外送平台,需具備校園專屬特色,否則就失去「校園」的意義。 需決定要採用「順路幫帶模式」或「固定取餐點模式」作為主要運作方式。 - **可能解決方案:** 1. **順路幫帶(使用者自行輸入校內地點 + 其他同學願意協助)** - 優點: - 彈性高,可依據學生所在位置即時送達 - 可認識更多本、外系同學 - 缺點: - 外送動線不固定,難以估算抵達時間 - 安全性與責任歸屬較複雜,無法確認誰負責或著延遲怎麼辦 - 系統需額外處理:距離判斷、順路判斷、路線比對、接單限制等邏輯 - 使用這個平台多為陌生人,需要認人,若地點不明確,例如只給系館沒有備註樓層,易造成找不到人的困境。 - 若地點不在一樓,該由外送員還是消費者來爬樓梯?爬樓梯是否須給小費 - 若外送員不住宿舍,會有門禁的問題 2. **固定取餐點(例如:各系館門口、學餐外、宿舍門口)** - 優點: - 取餐流程清楚、管理簡單 - 外送員路線固定,可快速完成多張訂單 - 適合校園環境(大家習慣在固定點取餐) - 外送員與使用者都更容易確認位置 - 缺點: - 彈性較低,無法送到個人宿舍房門 - 若取餐點人潮多可能造成壅塞 - **最後解決方案與理由:** 採用 **固定取餐點模式**。 原因如下: - 校園環境相對封閉,固定地點可避免取餐混亂 - 外送員多為學生,固定點可縮短取餐時間,提高效率 - 系統運作簡單,不需額外開發路線比對與順路判斷邏輯 - 更符合校園生活習慣(系館門口、學餐門口、宿舍門口前) ### 議題 4:移除 Demo 模式,強制走正式後端 - **議題內容:** 早期為方便展示內建 demo 菜單/訂單,容易與真實資料混淆。需改為全程呼叫後端 API,避免假資料干擾。 - **可能解決方案:** 1. **保留 demo 開關**:以環境變數切換。 - 優點:展示方便。 - 缺點:易誤用、測試結果不可信。 2. **完全移除 demo**:刪除 mock 資料與開關。 - 優點:唯一資料來源即為正式後端,風險低。 - 缺點:無離線展示能力。 - **最後解決方案與理由:** 採 **完全移除 demo**。實作:刪除 `DemoConfig`、mock services/計時器,菜單/訂單/評論初始為空並全部呼叫後端。避免假資料造成誤判。 ### 議題 5:外送地點兩階段選擇(分類 → 地點) - **議題內容:** 原本下單/設定頁只有單一地點下拉,遇到地點多或分類缺失時體驗不佳。需支援先選分類、再選地點,並在後端無資料時保底可選。 - **可能解決方案:** 1. **單一列表**:直接列出「分類 • 地點名」。 - 優點:實作簡單。 - 缺點:長列表可用性差,分類感弱。 2. **兩階段選擇**:先選分類,再選該分類地點;後端回空時提供 fallback。 - 優點:易尋址,分群清晰;有預設地點防止空白。 - 缺點:需多一個 Picker。 - **最後解決方案與理由:** 採 **兩階段選擇**。實作:呼叫 `/delivery/locations` 取得分類/地點,提供分類與地點兩個 Picker,後端回空時以「預設地點」作為 fallback,避免選單消失。