# LINE LIFF 申請與串接架構

本文件說明 DDDream 多商戶會員站如何透過 **LINE LIFF** 完成「在 LINE 內開站 → 登入／綁定會員」流程。  
**以 LIFF 為主**；Messaging API Bot、LINE Notify 僅作對照，避免與 LIFF 混淆。

**Console 實際操作（Step by Step）** 請見：[line-liff-app-setup.md](./line-liff-app-setup.md)

---

## 1. 概述

| 項目 | 說明 |
|------|------|
| LIFF 在本專案的用途 | 會員站 **LINE 內登入／綁定**（非訂單／預約通知） |
| 一句話流程 | LINE Console 設 Endpoint → 前台 `/liff/:liffId` → 後端 login／bind → 寫入 `lines.lineId` → 發 Passport token |
| 本專案**不做** | 後台管理 LIFF ID、驗證 idToken、自動產 LIFF QR、以 LIFF 發通知 |

### 與 Bot、Notify 的關係

三者為**平行功能**，身分與憑證**不共用**：

| 機制 | 主要用途 | 本專案現況 |
|------|----------|------------|
| **LIFF** | LINE 身分登入／綁定會員站 | **現行可用** |
| **Messaging API Bot**（`merchants.line_channel_*`） | 官方帳號對話、Account Link、push | 程式遺留；webhook／排程多半未啟用 |
| **LINE Notify**（env `LINE_NOTIFY_*`） | 個人通知（原可取代 SMS） | 綁定 UI 仍在；**發送已停用** |

現行會員通知主通道：**mail + SMS**。

---

## 2. 整體架構

### 2.1 元件關係

```mermaid
flowchart TB
  subgraph console [LINE_Developers_Console]
    LiffApp[LIFF_App]
    Endpoint[Endpoint_URL]
  end
  subgraph line [LINE_App]
    User[User]
  end
  subgraph fe [site_frontend]
    Route["/liff/:liffId"]
    SDK["@line/liff"]
  end
  subgraph api [DDDream_Front_API]
    Login["POST .../line/liff/login"]
    Bind["POST .../line/liff/bind"]
  end
  subgraph db [Database]
    Lines["lines.lineId"]
    Members[members]
  end

  LiffApp --> Endpoint
  User --> Route
  Route --> SDK
  SDK --> Login
  SDK --> Bind
  Login --> Lines
  Bind --> Lines
  Login --> Members
```

### 2.2 商家前台 URL

```
https://{merchant.alias}.{DINGSOMETHING_DOMAIN}/liff/{liffId}
```

| 變數 | 來源 | 範例 |
|------|------|------|
| `merchant.alias` | DB `merchants.alias` | `daydreamlab` |
| `DINGSOMETHING_DOMAIN` | 主專案 `.env` → `config('app.dingsomething.domain')` | `ipetbooking.com` |
| `liffId` | LINE Console 建立 LIFF 後取得 | `1234567890-AbCdEfGh` |

範例：

```
https://daydreamlab.ipetbooking.com/liff/1234567890-AbCdEfGh
```

商家前台網址公式（藍新等用途相同）：[`MerchantHelper::getMerchantWebUrl`](../src/Helpers/MerchantHelper.php)

### 2.3 商家如何從 Host 辨識

API 以請求 **Host 的 subdomain** 對應商家，**不是** body 帶 `merchantAlias`：

- [`RequestHelper::getSubDomain`](../src/Helpers/RequestHelper.php)：staging／production 剝掉 `.{domain}` 得 `alias`
- [`GetMerchantBySubDomain`](../src/Traits/GetMerchantBySubDomain.php)：`Merchant::where('alias', $alias)`
- 本機／非 staging|production：fallback 為 `config('daydreamlab.dddream.defaultMerchant')`

### 2.4 設定存放位置

| 項目 | 是否在 env | 是否在 DB 商家欄位 | 說明 |
|------|------------|-------------------|------|
| LIFF App ID | 否 | 否 | 只在 URL path、瀏覽器 localStorage（`site.auth.liffId`） |
| 會員 LINE userId（LIFF） | 否 | `lines.lineId` | 登入／綁定後寫入 |
| Bot 憑證 | 否 | `merchants.line_channel_*` | 與 LIFF 無關 |
| LINE Notify | 全域 env | `lines.line_notify_*` | 與 LIFF 無關 |

---

## 3. LINE Console 申請步驟

以下為營運／工程在 **LINE Developers Console** 的手動設定流程。本專案**未**在後台提供 LIFF 管理 UI。

### 3.1 前置

1. 登入 [LINE Developers Console](https://developers.line.biz/)
2. 選擇 Provider：**iPetBooking**（正式環境固定使用此 Provider）
3. 確認目標商家已在 DDDream 建立，且已知 `merchants.alias`（subdomain）

### 3.2 建立 LINE Login Channel

LIFF App 必須掛在 **LINE Login channel** 上，**不是** Messaging API channel。

1. Provider 下 → **Create a new channel** → **LINE Login**
2. 填寫 Channel 基本資訊並完成建立

> 若商家另有官方帳號 Bot（`merchants.line_channel_*`），那是**另一個 channel**，與 LIFF Login channel 分開。

### 3.3 新增 LIFF App

1. 進入該 **LINE Login channel** → **LIFF** 分頁 → **Add**
2. 建議欄位：

| 欄位 | 建議 |
|------|------|
| LIFF app name | 商家名稱 + 用途（勿含不當字串） |
| Size | 通常 **Full** |
| Endpoint URL | 見下方 |
| Scope | `profile` 勾；其餘不勾 |

3. **Endpoint URL** 填法：

   ```
   https://{alias}.{DINGSOMETHING_DOMAIN}/liff/{liffId}?redirectTo=https://{alias}.{DINGSOMETHING_DOMAIN}/member
   ```

   **實務注意：**

   - 建立 LIFF 當下可能尚未知道 `liffId`。可先填商家站台根路徑，**建立取得 LIFF ID 後再改 Endpoint** 為完整 `/liff/{liffId}?redirectTo=...`。
   - URL 必須 **https**，不可含 `#` fragment。
   - **`redirectTo` 必填**（完整 https 絕對網址）：前台 login 成功後依此轉頁；缺參數時畫面會停在空白。
   - 本專案前台路由為 `/liff/:liffId`，`liffId` 會由 Vue Router 帶入並傳給 `liff.init()`。
   - 逐步操作細節見 [line-liff-app-setup.md](./line-liff-app-setup.md)。

4. 儲存後取得：

| 項目 | 範例 |
|------|------|
| LIFF ID | `1234567890-AbCdEfGh` |
| LIFF URL | `https://liff.line.me/1234567890-AbCdEfGh` |

### 3.4 設定使用者入口

本系統**不會**自動產 LIFF QR。入口需自行配置：

- 官方帳號 **Rich Menu** 連結
- **推播／訊息** 中的 LIFF URL 或 Endpoint URL
- 自行將 LIFF URL 轉成 QR 供掃描

站內 `DialogLine` 顯示的 QR 是 **`lineFriendUrl`（加好友）**，與 LIFF 登入無關。

### 3.5 多商家

| 情境 | 做法 |
|------|------|
| 多個商家、同一 Provider | 可；每商家通常 **一個 LIFF App**（或各自 Endpoint） |
| Endpoint subdomain | 各商家不同：`https://shop-a.domain/liff/...`、`https://shop-b.domain/liff/...` |
| 一次設定全站通用 | **不行**；程式無全域 liffId 對照表 |

一個 LINE Login channel 最多約 **30** 支 LIFF App（LINE 平台限制）。

### 3.6 自動化（官方有、本專案未接）

LINE 提供 [LIFF Server API](https://developers.line.biz/en/reference/liff-server/) 與 [LIFF CLI](https://developers.line.biz/en/docs/liff/liff-cli/) 可程式化建立／更新 Endpoint：

- `POST https://api.line.me/liff/v1/apps`
- `PUT https://api.line.me/liff/v1/apps/{liffId}`

需 **LINE Login channel access token**。DDDream **未**實作此自動化；建 Login Channel 本身仍多半在 Console 手動完成。

---

## 4. 本專案串接架構

### 4.1 前台（site-frontend）

程式位於主專案外層 repo：`ipetbooking-site/site-frontend/`（非 dddream package 內）。

| 項目 | 內容 |
|------|------|
| 路由 | `/liff/:liffId`（site／jtails）；wordpress 變體 `/shop/liff/:liffId` |
| SDK | `@line/liff` |
| 入口 | `liff.init({ liffId })` → `liff.getProfile()` → 取得 `userId` |
| 已綁定 | `auth/lineLogIn` → 後端 login → 取得 token |
| 未綁定 | 導向 `/login?liffUserId=...` → 手機驗證碼登入 → `lineBind` → 再 `lineLogIn` |
| LIFF 內 UX | `$globalIsLiffClient`：隱藏加好友浮層、調整 header；登入驗證碼 `forceSms` |
| liffId 持久化 | localStorage `site.auth.liffId` |

**關鍵檔案（site-frontend）：**

| 檔案 | 角色 |
|------|------|
| `src/router/site/route-login.js` | `/liff/:liffId` 路由、`liff.init` |
| `src/router/jtails/route-login.js` | jtails 同上 |
| `src/utils/line-get-profile-and-login.js` | getProfile → login；失敗導向綁定 |
| `src/store/modules/auth.js` | `lineLogIn`、`lineBind`、`setCurrentLiffId` |
| `src/components/Form/FormLogin.vue` | 綁定後再 login；`forceSms` |

### 4.2 後端（dddream）

#### API

| Method | Path | Middleware | 說明 |
|--------|------|------------|------|
| `POST` | `/api/merchant/member/line/liff/login` | 無 | 以 `lineId` 查 `lines`，發 Passport token |
| `POST` | `/api/merchant/member/line/liff/bind` | `isMember` | 已登入會員綁定 `lineId`（`unique:lines`） |

路由定義：[`src/routes/front/api.php`](../src/routes/front/api.php)

#### 請求參數（實作）

| API | Body | 備註 |
|-----|------|------|
| login | `lineId`（required string） | 商家由 Host subdomain 解析 |
| bind | `lineId`（required string, unique on `lines`） | 需 Bearer token（`isMember`） |

> [`API_SITE_DOCUMENTATION.md`](../API_SITE_DOCUMENTATION.md) 仍寫 `lineUserId` + `idToken`，**與實作不符**（見 §8）。

#### 業務邏輯摘要

**Login**（[`MerchantMemberFrontService::lineLiffLogin`](../src/Services/Merchant/Front/MerchantMemberFrontService.php)）：

1. `lines` 以 `lineId` 找紀錄 → 取 `member`
2. 若會員尚未掛此商戶 → `createMappingLevel`
3. `createToken` 回傳 access token 與會員欄位
4. 找不到 → `MemberNotFound`（前台導向綁定）

**Bind**（[`MerchantMemberFrontService::lineLiffBind`](../src/Services/Merchant/Front/MerchantMemberFrontService.php)）：

1. 找該會員 `lineId IS NULL` 的列更新；否則新建
2. 成功狀態：`MERCHANT_MEMBER_FRONT_LINE_BIND_SUCCESS`

#### 後端關鍵檔案

| 檔案 | 角色 |
|------|------|
| [`MerchantMemberFrontController.php`](../src/Controllers/Merchant/Front/MerchantMemberFrontController.php) | `lineLiffLogin`、`lineLiffBind` |
| [`MerchantMemberFrontLineLiffLoginPost.php`](../src/Requests/Merchant/Front/MerchantMemberFrontLineLiffLoginPost.php) | 驗證 `lineId`；`GetMerchantBySubDomain` |
| [`MerchantMemberFrontLineLiffBindPost.php`](../src/Requests/Merchant/Front/MerchantMemberFrontLineLiffBindPost.php) | 驗證 `lineId` + unique |
| [`Models/Line/Line.php`](../src/Models/Line/Line.php) | `lines` 模型 |
| [`2021_10_08_000000_create_lines_table.php`](../src/database/migrations/2021_10_08_000000_create_lines_table.php) | 建表 |

### 4.3 登入／綁定流程

```mermaid
sequenceDiagram
  participant User as LINE_User
  participant FE as site_frontend
  participant LIFF as LIFF_SDK
  participant API as DDDream_API
  participant DB as lines_table

  User->>FE: 開啟 /liff/{liffId}
  FE->>LIFF: liff.init
  alt 未登入且不在 LINE client
    LIFF->>User: liff.login
  end
  FE->>LIFF: liff.getProfile
  LIFF-->>FE: userId
  FE->>API: POST line/liff/login { lineId }
  alt 已綁定
    API->>DB: lines.lineId → member
    API-->>FE: Passport token
  else MemberNotFound
    FE->>FE: /login?liffUserId=...
    User->>FE: 手機驗證碼登入
    FE->>API: POST line/liff/bind { lineId }
    FE->>API: POST line/liff/login { lineId }
    API-->>FE: Passport token
  end
```

---

## 5. 資料模型與三套 LINE ID

勿混淆以下欄位：

| 位置 | 欄位 | 用途 |
|------|------|------|
| `lines` | `lineId` | **LIFF** LINE userId（登入／綁定核心） |
| `lines` | `line_notify_*` | LINE Notify token（與 LIFF 同表、不同用途） |
| `merchants_members_maps` | `line_user_id` + `nonce` | Messaging API **Account Link** |
| `merchants_members_maps` | `lineId` | 會員**手動填寫**的 LINE ID 字串 |
| `merchants` | `line_channel_id` / `secret` / `line_access_token` | **Bot** 憑證 |
| `merchants` | `line_id` / `lineFriendUrl` | 加好友顯示用 |

Front 會員 Resource 部分欄位名為 `lineId` 時，可能來自 **pivot 手動字串**，**不是** LIFF 的 `lines.lineId`。

---

## 6. LIFF / Bot / Notify 功能對照

### LIFF（現行可用）

- 會員登入／綁定
- LINE 內開站 UX（header、加好友浮層、驗證碼 `forceSms`）
- **不**發訂單／預約通知

### Messaging API Bot（`line_channel_*`）

| 功能 | 說明 | 現況 |
|------|------|------|
| Webhook 對話 | 預約總覽、儲值、關於我們、帳號區等 | 程式在；**路由未掛** |
| Account Link | Bot 綁定 `line_user_id` | 程式在；**路由未掛** |
| Rich Menu | `lineBotRichMenu` | 程式在；**路由未掛** |
| Push | 票券連結、開場提醒（`sendTicketLinkToLineUser` 等） | 程式在；**Remind schedule 已註解** |
| 後台客服回覆 | push 回覆至 LINE | 依 Bot 憑證 + `lineUserID` |

### LINE Notify（env `LINE_NOTIFY_*`）

| 功能 | 說明 | 現況 |
|------|------|------|
| 綁定／解綁 | Front OAuth；登入後 `DialogLineNotify` | API／UI 仍在 |
| 取代 SMS | `NotificationHelper::sendGateway('sms')` 原可改走 Notify | **已停用**（`getLineNotifyInfo` 直接 `return [false, null]`） |

---

## 7. Env 與設定邊界

主專案 `.env`（非 dddream package 內）：

| Env key | 用途 | 與 LIFF 關係 |
|---------|------|--------------|
| `DINGSOMETHING_DOMAIN` | 商家 subdomain 根網域 | 組 Endpoint URL |
| `LINE_NOTIFY_NAME` / `CLIENT_ID` / `CLIENT_SECRET` | Notify OAuth（**全站共用**） | 無關 |
| `DEFAULT_MERCHANT` | 非 staging|production 時 subdomain fallback | 本機開發用 |

**沒有** `LIFF_*` 或 per-merchant LIFF 相關 env。

前台 `site-frontend/.env*` 亦**無** LIFF 專用 key；`liffId` 來自 URL 與 localStorage。

---

## 8. 已知限制與注意事項

| 項目 | 說明 |
|------|------|
| **無 idToken 驗證** | 後端只信任前端傳來的 `lineId`；與 [`API_SITE_DOCUMENTATION.md`](../API_SITE_DOCUMENTATION.md) 要求 `idToken` 不一致 |
| **無商戶後台 LIFF 管理** | liffId 不在 DB；無 Admin CRUD |
| **舊 Service 層** | 無 Action 層、無 LIFF 單元測試 |
| **三套 LINE ID** | Resource／前台顯示易混淆（見 §5） |
| **Notify 路由 typo** | `line/notify/bind ` 尾端多空白（旁支，非 LIFF） |
| **Bot 與 LIFF 分離** | Bot 用 `line_user_id`；LIFF 用 `lines.lineId`；需 Account Link 才會在 Bot 端認得會員 |

若需加固，建議優先：login／bind 驗證 LINE idToken、修正 API 文件、補單元測試。

---

## 9. 相關檔案索引

### dddream（本 package）

| 路徑 | 說明 |
|------|------|
| [`src/routes/front/api.php`](../src/routes/front/api.php) | LIFF login／bind 路由 |
| [`src/Controllers/Merchant/Front/MerchantMemberFrontController.php`](../src/Controllers/Merchant/Front/MerchantMemberFrontController.php) | Controller |
| [`src/Services/Merchant/Front/MerchantMemberFrontService.php`](../src/Services/Merchant/Front/MerchantMemberFrontService.php) | 業務邏輯 |
| [`src/Requests/Merchant/Front/MerchantMemberFrontLineLiffLoginPost.php`](../src/Requests/Merchant/Front/MerchantMemberFrontLineLiffLoginPost.php) | Login Request |
| [`src/Requests/Merchant/Front/MerchantMemberFrontLineLiffBindPost.php`](../src/Requests/Merchant/Front/MerchantMemberFrontLineLiffBindPost.php) | Bind Request |
| [`src/Models/Line/Line.php`](../src/Models/Line/Line.php) | Model |
| [`src/database/migrations/2021_10_08_000000_create_lines_table.php`](../src/database/migrations/2021_10_08_000000_create_lines_table.php) | Migration |
| [`src/Traits/GetMerchantBySubDomain.php`](../src/Traits/GetMerchantBySubDomain.php) | 商家解析 |
| [`src/Helpers/RequestHelper.php`](../src/Helpers/RequestHelper.php) | subdomain 解析 |
| [`src/Helpers/MerchantHelper.php`](../src/Helpers/MerchantHelper.php) | 商家前台 URL |
| [`API_SITE_DOCUMENTATION.md`](../API_SITE_DOCUMENTATION.md) | Front API 文件（LIFF 段落已過時） |

### site-frontend（主專案外層 repo）

| 路徑 | 說明 |
|------|------|
| `src/router/site/route-login.js` | `/liff/:liffId` |
| `src/router/jtails/route-login.js` | jtails 版 |
| `src/router/jtails/route-login-wordpress.js` | `/shop/liff/:liffId` |
| `src/utils/line-get-profile-and-login.js` | LIFF 登入流程 |
| `src/store/modules/auth.js` | `lineLogIn` / `lineBind` |
| `src/components/Form/FormLogin.vue` | 綁定與 `forceSms` |

### 主專案 config

| 路徑 | 說明 |
|------|------|
| `config/app.php` → `dingsomething.domain` | `DINGSOMETHING_DOMAIN` |
| `config/app.php` → `lineNotify.*` | Notify（非 LIFF） |

---

## 10. 檢查清單（新商家上線 LIFF）

- [ ] 商家 `alias` 已建立，subdomain 可正常開啟前台
- [ ] LINE Login channel 已建立
- [ ] LIFF App Endpoint = `https://{alias}.{domain}/liff/{liffId}`
- [ ] 官方帳號或對外素材已放置 LIFF URL／連結
- [ ] 實機在 LINE 內開啟 Endpoint，確認 `liff.init` 成功
- [ ] 已綁定會員：可直接 login 取得 token
- [ ] 新會員：可完成手機驗證 → bind → login
- [ ] 確認未與 Bot Account Link（`line_user_id`）混用預期

---

## 參考連結

- [LINE LIFF 文件](https://developers.line.biz/en/docs/liff/)
- [Adding a LIFF app](https://developers.line.biz/en/docs/liff/registering-liff-apps/)
- [LIFF Server API](https://developers.line.biz/en/reference/liff-server/)
- [LIFF CLI](https://developers.line.biz/en/docs/liff/liff-cli/)
