客戶 API
概述
客戶 API 提供客戶資訊的完整 CRUD 功能,支援個人客戶與企業客戶管理、重複檢測、訂單歷史查詢、消費統計及備註管理。
相關 User Stories:
端點列表
| 方法 | 路徑 | 說明 | 權限 |
|---|---|---|---|
| GET | /api/v1/customers | 列出客戶(支援搜尋、分頁) | ROLE_SALES 或更高 |
| GET | /api/v1/customers/:id | 取得客戶詳情 | ROLE_SALES 或更高 |
| POST | /api/v1/customers | 創建客戶 | ROLE_SALES 或更高 |
| PATCH | /api/v1/customers/:id | 更新客戶(部分更新) | ROLE_SALES 或更高 |
| GET | /api/v1/customers/check-duplicate | 檢查電話是否重複 | ROLE_SALES 或更高 |
| PATCH | /api/v1/customers/:id/status | 停用/啟用客戶 | ROLE_OWNER 或 ROLE_MANAGER |
| GET | /api/v1/customers/:id/orders | 取得客戶訂單歷史 | ROLE_SALES 或更高 |
| GET | /api/v1/customers/:id/stats | 取得客戶消費統計 | ROLE_SALES 或更高 |
| GET | /api/v1/customers/:id/notes | 取得客戶備註列表 | ROLE_SALES 或更高 |
| POST | /api/v1/customers/:id/notes | 新增客戶備註 | ROLE_SALES 或更高 |
資料模型
客戶類型 (CustomerType)
| 值 | 說明 |
|---|---|
individual | 個人客戶 |
corporate | 企業客戶 |
客戶等級 (CustomerTier)
| 值 | 說明 | 累計消費門檻 |
|---|---|---|
regular | 普通客戶 | < NT$5,000 |
vip | VIP 客戶 | >= NT$5,000 |
vvip | VVIP 客戶 | >= NT$20,000 |
客戶狀態 (CustomerStatus)
| 值 | 說明 |
|---|---|
active | 啟用(可下單) |
inactive | 停用(不可下單) |
性別 (Gender)
| 值 | 說明 |
|---|---|
male | 男性 |
female | 女性 |
other | 其他 |
個人客戶實體 (IndividualCustomer)
interface IndividualCustomer {
id: string; // 客戶 ID(UUID)
customerNumber: string; // 客戶編號(自動生成)
tenantId: string; // 租戶 ID
type: 'individual'; // 客戶類型
status: CustomerStatus; // 客戶狀態
tier: CustomerTier; // 客戶等級(自動計算)
name: string; // 姓名(必填)
gender?: Gender; // 性別
birthday?: string; // 生日(ISO 8601 date)
phone: string; // 電話(必填)
email?: string; // Email
addresses?: Address[]; // 地址列表
source?: string; // 客戶來源
preferences?: string[]; // 喜好標籤
importantDates?: ImportantDate[]; // 重要日期
totalSpent: number; // 累計消費金額
totalOrders: number; // 總訂單數
lastOrderDate: string | null; // 最後消費日期(ISO 8601)
createdAt: string; // 創建時間(ISO 8601)
updatedAt: string; // 更新時間(ISO 8601)
}
企業客戶實體 (CorporateCustomer)
interface CorporateCustomer {
id: string; // 客戶 ID(UUID)
customerNumber: string; // 客戶編號(自動生成)
tenantId: string; // 租戶 ID
type: 'corporate'; // 客戶類型
status: CustomerStatus; // 客戶狀態
tier: CustomerTier; // 客戶等級(自動計算)
companyName: string; // 公司名稱(必填)
taxId?: string; // 統一編號(8 位數字)
industry?: string; // 產業類別
phone: string; // 公司電話(必填)
address?: string; // 公司地址
email?: string; // Email
contacts: Contact[]; // 聯絡人列表(至少一位)
cooperationStartDate?: string; // 合作開始日期(ISO 8601 date)
paymentTerms?: PaymentTerms; // 月結帳期
totalSpent: number; // 累計消費金額
totalOrders: number; // 總訂單數
lastOrderDate: string | null; // 最後消費日期(ISO 8601)
createdAt: string; // 創建時間(ISO 8601)
updatedAt: string; // 更新時間(ISO 8601)
}
地址 (Address)
interface Address {
address: string; // 地址
isDefault: boolean; // 是否為預設地址
label?: string; // 標籤(如「住家」、「公司」)
}
聯絡人 (Contact)
interface Contact {
name: string; // 姓名
title?: string; // 職稱
phone: string; // 電話
email?: string; // Email
isPrimary: boolean; // 是否為主要聯絡人
}
重要日期 (ImportantDate)
interface ImportantDate {
date: string; // 日期(ISO 8601 date)
label: string; // 標籤(如「結婚紀念日」)
}
月結帳期 (PaymentTerms)
| 值 | 說明 |
|---|---|
none | 無月結(現金交易) |
net15 | 15 天月結 |
net30 | 30 天月結 |
客戶編號生成規則
格式:{租戶代碼}-CUST-{流水號}
範例:FS01-CUST-0001
端點詳細規格
GET /api/v1/customers
描述: 列出客戶,支援搜尋和分頁
權限: ROLE_SALES 或更高
Query Parameters:
| 參數 | 類型 | 必填 | 說明 | 預設值 |
|---|---|---|---|---|
| search | string | No | 搜尋關鍵字(姓名、公司名、電話) | - |
| type | string | No | 客戶類型過濾(individual, corporate) | - |
| status | string | No | 客戶狀態過濾(active, inactive) | - |
| tier | string | No | 客戶等級過濾(regular, vip, vvip) | - |
| page | integer | No | 頁碼(從 1 開始) | 1 |
| limit | integer | No | 每頁數量 | 20 |
| sortBy | string | No | 排序欄位(name, createdAt, totalSpent) | createdAt |
| sortOrder | string | No | 排序方向(asc, desc) | desc |
請求範例:
GET /api/v1/customers?search=李&type=individual&status=active&page=1&limit=20
Authorization: Bearer {access_token}
響應範例 (200 OK):
Headers:
X-Total-Count: 45
X-Page: 1
X-Per-Page: 20
Link: </api/v1/customers?page=1&limit=20>; rel="first", </api/v1/customers?page=2&limit=20>; rel="next", </api/v1/customers?page=3&limit=20>; rel="last"
Body:
[
{
"id": "cust-001",
"customerNumber": "FS01-CUST-0001",
"tenantId": "tenant-001",
"type": "individual",
"status": "active",
"tier": "vip",
"name": "李大華",
"phone": "0912-345-678",
"email": "lihua@example.com",
"totalSpent": 15000,
"totalOrders": 12,
"lastOrderDate": "2025-12-15T10:30:00Z",
"createdAt": "2025-06-01T08:00:00Z",
"updatedAt": "2025-12-15T10:30:00Z"
}
]
GET /api/v1/customers/:id
描述: 取得單一客戶詳情
權限: ROLE_SALES 或更高
Path Parameters:
| 參數 | 類型 | 說明 |
|---|---|---|
| id | string | 客戶 ID |
請求範例:
GET /api/v1/customers/cust-001
Authorization: Bearer {access_token}
響應範例 (200 OK) - 個人客戶:
{
"id": "cust-001",
"customerNumber": "FS01-CUST-0001",
"tenantId": "tenant-001",
"type": "individual",
"status": "active",
"tier": "vip",
"name": "李大華",
"gender": "male",
"birthday": "1985-03-15",
"phone": "0912-345-678",
"email": "lihua@example.com",
"addresses": [
{
"address": "台北市信義區信義路五段 7 號",
"isDefault": true,
"label": "公司"
}
],
"source": "網路廣告",
"preferences": ["玫瑰", "紅色系"],
"importantDates": [
{ "date": "2020-06-15", "label": "結婚紀念日" }
],
"totalSpent": 15000,
"totalOrders": 12,
"lastOrderDate": "2025-12-15T10:30:00Z",
"createdAt": "2025-06-01T08:00:00Z",
"updatedAt": "2025-12-15T10:30:00Z"
}
響應範例 (200 OK) - 企業客戶:
{
"id": "cust-002",
"customerNumber": "FS01-CUST-0002",
"tenantId": "tenant-001",
"type": "corporate",
"status": "active",
"tier": "vvip",
"companyName": "台灣科技股份有限公司",
"taxId": "12345678",
"industry": "科技業",
"phone": "02-2345-6789",
"address": "台北市內湖區瑞光路 123 號",
"email": "contact@taiwantech.com",
"contacts": [
{
"name": "王小明",
"title": "總務經理",
"phone": "0922-333-444",
"email": "wang@taiwantech.com",
"isPrimary": true
}
],
"cooperationStartDate": "2024-01-15",
"paymentTerms": "net30",
"totalSpent": 85000,
"totalOrders": 25,
"lastOrderDate": "2025-12-20T14:00:00Z",
"createdAt": "2024-01-15T09:00:00Z",
"updatedAt": "2025-12-20T14:00:00Z"
}
錯誤響應:
| 狀態碼 | 錯誤訊息 | 說明 |
|---|---|---|
| 404 | 客戶不存在 | 找不到指定的客戶 |
POST /api/v1/customers
描述: 創建新客戶
權限: ROLE_SALES 或更高
請求體 - 個人客戶 (CreateIndividualCustomerRequest):
| 欄位 | 類型 | 必填 | 說明 |
|---|---|---|---|
| type | string | Yes | 固定為 individual |
| name | string | Yes | 姓名 |
| phone | string | Yes | 電話 |
| gender | string | No | 性別(male, female, other) |
| birthday | string | No | 生日(YYYY-MM-DD) |
| string | No | ||
| addresses | Address[] | No | 地址列表 |
| source | string | No | 客戶來源 |
| preferences | string[] | No | 喜好標籤 |
| importantDates | ImportantDate[] | No | 重要日期 |
請求體 - 企業客戶 (CreateCorporateCustomerRequest):
| 欄位 | 類型 | 必填 | 說明 |
|---|---|---|---|
| type | string | Yes | 固定為 corporate |
| companyName | string | Yes | 公司名稱 |
| phone | string | Yes | 公司電話 |
| contacts | Contact[] | Yes | 聯絡人列表(至少一位) |
| taxId | string | No | 統一編號(8 位數字) |
| industry | string | No | 產業類別 |
| address | string | No | 公司地址 |
| string | No | ||
| cooperationStartDate | string | No | 合作開始日期(YYYY-MM-DD) |
| paymentTerms | string | No | 月結帳期(none, net15, net30) |
請求範例 (個人客戶):
POST /api/v1/customers
Authorization: Bearer {access_token}
Content-Type: application/json
{
"type": "individual",
"name": "張小美",
"phone": "0933-456-789",
"gender": "female",
"email": "mei@example.com",
"addresses": [
{
"address": "台北市大安區忠孝東路四段 100 號",
"isDefault": true,
"label": "住家"
}
],
"source": "朋友推薦"
}
請求範例 (企業客戶):
POST /api/v1/customers
Authorization: Bearer {access_token}
Content-Type: application/json
{
"type": "corporate",
"companyName": "美麗花園有限公司",
"taxId": "87654321",
"phone": "02-8765-4321",
"address": "新北市板橋區文化路一段 50 號",
"contacts": [
{
"name": "陳經理",
"title": "採購經理",
"phone": "0955-666-777",
"isPrimary": true
}
],
"paymentTerms": "net15"
}
響應範例 (201 Created):
{
"id": "cust-003",
"customerNumber": "FS01-CUST-0003",
"type": "individual",
"status": "active",
"tier": "regular",
"name": "張小美",
"phone": "0933-456-789",
"totalSpent": 0,
"totalOrders": 0,
"lastOrderDate": null,
"createdAt": "2025-12-22T10:00:00Z",
"updatedAt": "2025-12-22T10:00:00Z"
}
錯誤響應:
| 狀態碼 | 錯誤訊息 | 說明 |
|---|---|---|
| 400 | 請填寫姓名和電話 | 個人客戶必填欄位缺失 |
| 400 | 請填寫公司名稱和公司電話 | 企業客戶必填欄位缺失 |
| 400 | 至少需要一位聯絡人 | 企業客戶缺少聯絡人 |
PATCH /api/v1/customers/:id
描述: 部分更新客戶(僅更新提供的欄位)
權限: ROLE_SALES 或更高
請求體: 所有欄位皆為可選,僅更新提供的欄位
請求範例:
PATCH /api/v1/customers/cust-001
Authorization: Bearer {access_token}
Content-Type: application/json
{
"phone": "0912-999-888",
"email": "newemail@example.com",
"preferences": ["百合", "白色系"]
}
響應範例 (200 OK):
返回更新後的完整客戶資料
錯誤響應:
| 狀態碼 | 錯誤訊息 | 說明 |
|---|---|---|
| 404 | 客戶不存在 | 找不到指定的客戶 |
GET /api/v1/customers/check-duplicate
描述: 檢查電話號碼是否已被其他客戶使用
權限: ROLE_SALES 或更高
Query Parameters:
| 參數 | 類型 | 必填 | 說明 |
|---|---|---|---|
| phone | string | Yes | 要檢查的電話號碼 |
| excludeId | string | No | 排除的客戶 ID(編輯模式時排除自身) |
請求範例:
GET /api/v1/customers/check-duplicate?phone=0912-345-678
Authorization: Bearer {access_token}
響應範例 (200 OK) - 無重複:
{
"isDuplicate": false
}
響應範例 (200 OK) - 有重複:
{
"isDuplicate": true,
"existingCustomer": {
"id": "cust-001",
"customerNumber": "FS01-CUST-0001",
"name": "李大華",
"phone": "0912-345-678"
}
}
錯誤響應:
| 狀態碼 | 錯誤訊息 | 說明 |
|---|---|---|
| 400 | 請提供電話號碼 | 缺少 phone 參數 |
PATCH /api/v1/customers/:id/status
描述: 停用或啟用客戶
權限: ROLE_OWNER 或 ROLE_MANAGER
請求體 - 停用客戶 (DeactivateCustomerRequest):
| 欄位 | 類型 | 必填 | 說明 |
|---|---|---|---|
| status | string | Yes | 固定為 inactive |
| reason | string | Yes | 停用原因(blacklist, duplicate, other) |
| reasonNote | string | No | 停用備註說明 |
請求體 - 啟用客戶 (ActivateCustomerRequest):
| 欄位 | 類型 | 必填 | 說明 |
|---|---|---|---|
| status | string | Yes | 固定為 active |
停用原因對照:
| 值 | 說明 |
|---|---|
blacklist | 黑名單(惡意客戶) |
duplicate | 重複帳號 |
other | 其他原因 |
請求範例 (停用):
PATCH /api/v1/customers/cust-001/status
Authorization: Bearer {access_token}
Content-Type: application/json
{
"status": "inactive",
"reason": "blacklist",
"reasonNote": "多次惡意取消訂單"
}
請求範例 (啟用):
PATCH /api/v1/customers/cust-001/status
Authorization: Bearer {access_token}
Content-Type: application/json
{
"status": "active"
}
響應範例 (200 OK):
返回更新後的完整客戶資料
錯誤響應:
| 狀態碼 | 錯誤訊息 | 說明 |
|---|---|---|
| 400 | 停用客戶時必須填寫停用原因 | 停用時缺少 reason |
| 404 | 客戶不存在 | 找不到指定的客戶 |
GET /api/v1/customers/:id/orders
描述: 取得客戶的訂單歷史(排除已取消訂單)
權限: ROLE_SALES 或更高
Path Parameters:
| 參數 | 類型 | 說明 |
|---|---|---|
| id | string | 客戶 ID |
Query Parameters:
| 參數 | 類型 | 必填 | 說明 | 預設值 |
|---|---|---|---|---|
| page | integer | No | 頁碼(從 1 開始) | 1 |
| limit | integer | No | 每頁數量 | 10 |
請求範例:
GET /api/v1/customers/cust-001/orders?page=1&limit=10
Authorization: Bearer {access_token}
響應範例 (200 OK):
Headers:
X-Total-Count: 12
X-Page: 1
X-Per-Page: 10
Body:
[
{
"id": "order-001",
"orderNumber": "FS01-20251215-0001",
"status": "completed",
"total": 2500,
"deliveryDate": "2025-12-15",
"createdAt": "2025-12-14T10:00:00Z"
},
{
"id": "order-002",
"orderNumber": "FS01-20251210-0003",
"status": "delivered",
"total": 1800,
"deliveryDate": "2025-12-10",
"createdAt": "2025-12-09T14:30:00Z"
}
]
錯誤響應:
| 狀態碼 | 錯誤訊息 | 說明 |
|---|---|---|
| 404 | 客戶不存在 | 找不到指定的客戶 |
GET /api/v1/customers/:id/stats
描述: 取得客戶的消費統計資料
權限: ROLE_SALES 或更高
Path Parameters:
| 參數 | 類型 | 說明 |
|---|---|---|
| id | string | 客戶 ID |
請求範例:
GET /api/v1/customers/cust-001/stats
Authorization: Bearer {access_token}
響應範例 (200 OK):
{
"totalOrders": 12,
"totalSpent": 45000,
"averageOrderAmount": 3750,
"lastOrderDate": "2025-12-15T10:30:00Z",
"topProducts": [
{
"productId": "prod-001",
"productName": "經典紅玫瑰花束",
"purchaseCount": 5,
"percentage": 42
},
{
"productId": "prod-003",
"productName": "百合盆栽",
"purchaseCount": 3,
"percentage": 25
},
{
"productId": "prod-007",
"productName": "向日葵花束",
"purchaseCount": 2,
"percentage": 17
}
],
"monthlyTrend": [
{ "month": "2025-01", "amount": 0 },
{ "month": "2025-02", "amount": 2500 },
{ "month": "2025-03", "amount": 0 },
{ "month": "2025-04", "amount": 5000 },
{ "month": "2025-05", "amount": 3500 },
{ "month": "2025-06", "amount": 0 },
{ "month": "2025-07", "amount": 8000 },
{ "month": "2025-08", "amount": 2500 },
{ "month": "2025-09", "amount": 0 },
{ "month": "2025-10", "amount": 6000 },
{ "month": "2025-11", "amount": 12000 },
{ "month": "2025-12", "amount": 5500 }
]
}
響應欄位說明:
| 欄位 | 類型 | 說明 |
|---|---|---|
| totalOrders | integer | 總訂單數(排除已取消) |
| totalSpent | number | 累計消費金額(僅計算已完成訂單) |
| averageOrderAmount | number | 平均訂單金額 |
| lastOrderDate | string | 最後消費日期(ISO 8601) |
| topProducts | array | 最常購買的前 3 名商品 |
| monthlyTrend | array | 近 12 個月的消費趨勢 |
錯誤響應:
| 狀態碼 | 錯誤訊息 | 說明 |
|---|---|---|
| 404 | 客戶不存在 | 找不到指定的客戶 |
GET /api/v1/customers/:id/notes
描述: 取得客戶的備註列表
權限: ROLE_SALES 或更高
Path Parameters:
| 參數 | 類型 | 說明 |
|---|---|---|
| id | string | 客戶 ID |
請求範例:
GET /api/v1/customers/cust-001/notes
Authorization: Bearer {access_token}
響應範例 (200 OK):
[
{
"id": "note-001",
"content": "客戶偏好粉色系花材,送花時請附上手寫卡片",
"createdAt": "2025-12-15T10:30:00Z",
"createdBy": {
"id": "user-001",
"name": "王小明"
}
},
{
"id": "note-002",
"content": "每年母親節都會訂購康乃馨花束",
"createdAt": "2025-11-20T14:00:00Z",
"createdBy": {
"id": "user-002",
"name": "李小華"
}
}
]
錯誤響應:
| 狀態碼 | 錯誤訊息 | 說明 |
|---|---|---|
| 404 | 客戶不存在 | 找不到指定的客戶 |
POST /api/v1/customers/:id/notes
描述: 新增客戶備註
權限: ROLE_SALES 或更高
Path Parameters:
| 參數 | 類型 | 說明 |
|---|---|---|
| id | string | 客戶 ID |
請求體 (CreateCustomerNoteRequest):
| 欄位 | 類型 | 必填 | 說明 |
|---|---|---|---|
| content | string | Yes | 備註內容 |
請求範例:
POST /api/v1/customers/cust-001/notes
Authorization: Bearer {access_token}
Content-Type: application/json
{
"content": "客戶反映上次配送時間太早,下次請安排下午時段"
}
響應範例 (201 Created):
{
"id": "note-003",
"content": "客戶反映上次配送時間太早,下次請安排下午時段",
"createdAt": "2025-12-22T10:00:00Z",
"createdBy": {
"id": "user-001",
"name": "王小明"
}
}
錯誤響應:
| 狀態碼 | 錯誤訊息 | 說明 |
|---|---|---|
| 400 | 備註內容不能為空 | content 為空 |
| 404 | 客戶不存在 | 找不到指定的客戶 |
業務規則
電話號碼規範化
- 系統會自動移除電話號碼中的空白、括號、連字號
- 搜尋時同樣會規範化後比對
- 範例:
0912-345-678和0912345678視為相同
客戶等級自動計算
- 基於累計消費金額(totalSpent)自動計算
- 僅計算已完成訂單的金額
- 等級門檻見上方 CustomerTier 定義
停用客戶
- 停用的客戶無法創建新訂單
- 停用時必須填寫原因(reason)
- 停用記錄會寫入審計日誌
電話重複檢測
- 同一租戶內,電話號碼不可重複
- 創建/編輯客戶前應先呼叫 check-duplicate 端點
- 僅為警告,不強制阻擋(允許家庭成員共用電話)
多租戶隔離
- 所有客戶資料自動附加
tenantId - 查詢自動過濾為當前租戶的客戶
- 跨租戶訪問返回 404 或空列表
錯誤碼總覽
| HTTP 狀態碼 | 錯誤碼 | 說明 |
|---|---|---|
| 400 | BAD_REQUEST | 請求格式錯誤或缺少必填欄位 |
| 401 | AUTH_TOKEN_INVALID | Token 無效或過期 |
| 403 | FORBIDDEN | 權限不足 |
| 404 | NOT_FOUND | 資源不存在 |
| 500 | INTERNAL_ERROR | 伺服器內部錯誤 |
最後更新: 2025-12-22