US-301: 客戶基本資料管理
User Story
作為 店員 我想要 能夠新增、編輯、查看和停用客戶資料 以便 管理客戶資訊並避免重複建檔
驗收標準 (Acceptance Criteria)
Scenario 1: 快速新增個人客戶(僅必填欄位)
- Given 我是已登入的店員
- And 我在「新增客戶」頁面
- When 我選擇客戶類型為「個人客戶」
- And 我填寫姓名「王小明」
- And 我填寫電話「0912-345-678」
- And 我點擊「儲存」按鈕
- Then 系統應創建客戶並自動分配客戶編號(格式:
{租戶代碼}-CUST-{流水號}) - And 我應看到客戶創建成功的通知
- And 系統應自動導航至客戶詳情頁
- And 客戶狀態應為「啟用」
Scenario 2: 新增企業客戶(含聯絡人資訊)
- Given 我是已登入的店員
- And 我在「新增客戶」頁面
- When 我選擇客戶類型為「企業客戶」
- And 我填寫公司名稱「XX 科技股份有限公司」
- And 我填寫統一編號「12345678」
- And 我填寫公司電話「02-1234-5678」
- And 我添加聯絡人「張經理」,電話「0912-111-222」,設為主要聯絡人
- And 我點擊「儲存」按鈕
- Then 系統應創建企業客戶
- And 聯絡人資訊應正確儲存
- And 主要聯絡人應有特殊標記
Scenario 3: 重複電話檢測(新增時)
- Given 我是已登入的店員
- And 系統中已存在電話為「0912-345-678」的客戶「王小明」
- When 我嘗試新增新客戶
- And 我填寫電話「0912-345-678」
- And 我填寫姓名「王大明」
- And 我點擊「儲存」按鈕
- Then 系統應顯示警告訊息「此電話已被客戶『王小明』使用」
- And 系統應詢問「是否繼續創建?」
- And 若我選擇「取消」,則不創建新客戶
- And 若我選擇「繼續」,則允許創建(可能是家人共用電話)
Scenario 4: 編輯客戶基本資訊
- Given 我是已登入的店員
- And 我在客戶「王小明」的詳情頁
- When 我點擊「編輯」按鈕
- And 我修改電話為「0912-999-888」
- And 我添加 Email「wang@example.com」
- And 我添加生日「1990-05-15」
- And 我點擊「儲存」按鈕
- Then 系統應更新客戶資訊
- And 我應看到「儲存成功」的通知
- And 更新應立即反映在詳情頁
Scenario 5: 停用客戶
- Given 我是已登入的管理者
- And 我在客戶「王小明」的詳情頁
- When 我點擊「停用客戶」按鈕
- And 系統要求我填寫停用原因
- And 我選擇停用原因為「黑名單客戶(惡意退貨)」
- And 我填寫備註「多次無理由退貨」
- And 我點擊「確認停用」按鈕
- Then 系統應將客戶狀態改為「停用」
- And 停用原因與備註應記錄在審計日誌
- And 此客戶不應再出現在創建訂單時的客戶選擇列表
- And 但歷史訂單仍可正常查看
Scenario 6: 重新啟用客戶
- Given 我是已登入的管理者
- And 客戶「王小明」目前狀態為「停用」
- When 我點擊「重新啟用」按鈕
- And 系統要求我確認
- And 我點擊「確認」按鈕
- Then 系統應將客戶狀態改為「啟用」
- And 此客戶應重新出現在客戶選擇列表
Scenario 7: 新增客戶時缺少必填欄位(錯誤場景)
- Given 我是已登入的店員
- And 我在「新增客戶」頁面
- When 我僅填寫姓名「王小明」(未填寫電話)
- And 我點擊「儲存」按鈕
- Then 系統應顯示錯誤訊息「請填寫電話」
- And 電話欄位應以紅色邊框標示
- And 客戶不應被創建
- And 表單應保留我已填寫的資料
業務規則 (Business Rules)
-
客戶編號規則
- 格式:
{租戶代碼}-CUST-{流水號} - 範例:
ABC-CUST-000001 - 流水號從 000001 開始,每次創建新客戶遞增
- 格式:
-
個人客戶必填欄位
- 姓名
- 電話
-
企業客戶必填欄位
- 公司名稱
- 公司電話
- 至少一位聯絡人(含姓名與電話)
-
電話格式驗證
- 支援格式:
09XX-XXX-XXX,0912345678,02-XXXX-XXXX - 自動移除空格與連字號後儲存
- 查詢時不區分格式(統一比對純數字)
- 支援格式:
-
重複檢測規則
- 新增客戶時檢查電話是否重複
- 編輯客戶時若修改電話,也需檢查重複
- 允許用戶忽略警告並強制創建(可能是家人共用電話)
-
停用/啟用權限
- 僅
ROLE_MANAGER或更高權限可停用/啟用客戶 - 停用時必須填寫停用原因
- 停用客戶的歷史訂單仍保留且可查看
- 僅
-
多租戶隔離
- 客戶自動關聯到當前用戶的
tenantId - 客戶列表僅顯示當前租戶的客戶
- 客戶自動關聯到當前用戶的
-
客戶等級計算
- 累計消費 < 5,000: 普通客戶
- 累計消費 >= 5,000: VIP
- 累計消費 >= 20,000: VVIP
- 等級自動計算,無法手動修改
UI/UX 需求 (UI/UX Requirements)
新增/編輯客戶表單佈局
個人客戶表單
-
基本資訊區:
- 姓名(必填)
- 性別(選填,單選:男/女/其他)
- 生日(選填,日期選擇器)
- 電話(必填)
- Email(選填)
-
地址資訊區 (可摺疊):
- 地址列表(可多筆)
- 「新增地址」按鈕
- 設定預設地址
-
附加資訊區 (可摺疊):
- 客戶來源(下拉選單)
- 喜好標籤(多選)
- 重要日期(如:結婚紀念日)
企業客戶表單
-
基本資訊區:
- 公司名稱(必填)
- 統一編號(選填,8 位數字驗證)
- 產業類別(下拉選單)
- 公司電話(必填)
- 公司地址(選填)
- Email(選填)
-
聯絡人資訊區:
- 聯絡人列表(表格顯示)
- 每個聯絡人包含:姓名、職稱、電話、Email、是否主要聯絡人
- 「新增聯絡人」按鈕
- 至少需要一位聯絡人
-
附加資訊區 (可摺疊):
- 合作開始日期(日期選擇器)
- 月結帳期(下拉選單:無/月結 15 天/月結 30 天)
客戶詳情頁佈局
-
客戶資訊卡片:
- 客戶編號(唯讀)
- 客戶等級徽章(VIP/VVIP)
- 客戶狀態(啟用/停用)
- 註冊日期
- 最後消費日期
- 累計消費金額
-
操作按鈕:
- 編輯(所有用戶)
- 停用/啟用(僅管理者)
- 合併客戶(僅管理者,P2 功能)
互動行為
-
重複檢測: 輸入電話後自動檢查(防抖 500ms)
- 若重複,顯示警告訊息與現有客戶連結
- 用戶可選擇「查看現有客戶」或「繼續創建」
-
聯絡人管理 (企業客戶):
- 「新增聯絡人」按鈕開啟內嵌表單
- 「設為主要聯絡人」單選按鈕(僅一位)
- 刪除聯絡人時若為主要聯絡人,需先設定其他人為主要聯絡人
-
停用確認:
- 點擊「停用客戶」按鈕開啟 Modal
- Modal 包含停用原因選擇與備註輸入
- 需二次確認
錯誤訊息
- 缺少必填欄位: 紅色邊框 + 欄位下方顯示錯誤訊息
- 電話格式錯誤: 「請輸入正確的電話格式」
- 統一編號格式錯誤: 「請輸入 8 位數字」
- 重複電話警告: 黃色警告框 + 「此電話已被客戶『XXX』使用」
成功反饋
- 客戶創建成功: Toast 通知「客戶 {編號} 創建成功」
- 客戶更新成功: Toast 通知「儲存成功」
- 客戶停用成功: Toast 通知「客戶已停用」
響應式設計
- 桌面: 雙欄佈局(左側表單,右側客戶資訊預覽)
- 平板/手機: 單欄佈局(垂直排列)
技術規格 (Technical Specifications)
API 端點
1. 創建客戶
- 端點:
POST /api/v1/customers - 權限要求:
ROLE_STAFF或更高 - 多租戶隔離: 自動附加
tenantId
請求 Payload (個人客戶):
{
"type": "individual", // 'individual' | 'corporate'
"name": "王小明",
"gender": "male", // 'male' | 'female' | 'other' (選填)
"birthday": "1990-05-15", // ISO 8601 (選填)
"phone": "0912345678",
"email": "wang@example.com", // (選填)
"addresses": [ // (選填)
{
"address": "台北市信義區信義路五段 7 號",
"isDefault": true,
"label": "住家" // (選填)
}
],
"source": "google_search", // 客戶來源 (選填)
"preferences": ["喜歡玫瑰", "不要百合"], // 喜好標籤 (選填)
"importantDates": [ // 重要日期 (選填)
{
"date": "2020-06-01",
"label": "結婚紀念日"
}
]
}
請求 Payload (企業客戶):
{
"type": "corporate",
"companyName": "XX 科技股份有限公司",
"taxId": "12345678", // 統一編號 (選填)
"industry": "technology", // 產業類別 (選填)
"phone": "02-1234-5678",
"address": "台北市信義區信義路五段 100 號", // (選填)
"email": "contact@xxtech.com", // (選填)
"contacts": [
{
"name": "張經理",
"title": "業務經理",
"phone": "0912-111-222",
"email": "chang@xxtech.com",
"isPrimary": true
}
],
"cooperationStartDate": "2025-01-01", // (選填)
"paymentTerms": "net30" // 'none' | 'net15' | 'net30' (選填)
}
響應 Payload:
{
"id": "abc123",
"customerNumber": "ABC-CUST-000001",
"type": "individual",
"name": "王小明",
"phone": "0912345678",
"email": "wang@example.com",
"status": "active", // 'active' | 'inactive'
"tier": "regular", // 'regular' | 'vip' | 'vvip'
"totalSpent": 0,
"totalOrders": 0,
"lastOrderDate": null,
"createdAt": "2025-11-04T10:00:00Z",
"updatedAt": "2025-11-04T10:00:00Z"
}
2. 檢查電話是否重複
- 端點:
GET /api/v1/customers/check-duplicate?phone={phone} - 權限要求:
ROLE_STAFF或更高
響應 Payload:
{
"isDuplicate": true,
"existingCustomer": {
"id": "abc123",
"customerNumber": "ABC-CUST-000001",
"name": "王小明",
"phone": "0912345678"
}
}
3. 更新客戶
- 端點:
PUT /api/v1/customers/{id} - 權限要求:
ROLE_STAFF或更高
請求 Payload: 同創建客戶(部分欄位更新)
4. 停用/啟用客戶
- 端點:
PATCH /api/v1/customers/{id}/status - 權限要求:
ROLE_MANAGER或更高
請求 Payload (停用):
{
"status": "inactive",
"reason": "blacklist", // 'blacklist' | 'duplicate' | 'other'
"reasonNote": "多次無理由退貨"
}
請求 Payload (啟用):
{
"status": "active"
}
前端驗證規則
// Zod Schema (個人客戶)
const IndividualCustomerSchema = z.object({
type: z.literal('individual'),
name: z.string().min(1, '請填寫姓名').max(50, '姓名過長'),
gender: z.enum(['male', 'female', 'other']).optional(),
birthday: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
phone: z.string()
.min(1, '請填寫電話')
.regex(/^[0-9\-\s]+$/, '電話格式錯誤'),
email: z.string().email('Email 格式錯誤').optional().or(z.literal('')),
addresses: z.array(z.object({
address: z.string().min(1, '請填寫地址'),
isDefault: z.boolean(),
label: z.string().optional(),
})).optional(),
source: z.string().optional(),
preferences: z.array(z.string()).optional(),
importantDates: z.array(z.object({
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
label: z.string(),
})).optional(),
});
// Zod Schema (企業客戶)
const CorporateCustomerSchema = z.object({
type: z.literal('corporate'),
companyName: z.string().min(1, '請填寫公司名稱').max(100, '公司名稱過長'),
taxId: z.string().regex(/^\d{8}$/, '統一編號必須為 8 位數字').optional(),
industry: z.string().optional(),
phone: z.string()
.min(1, '請填寫公司電話')
.regex(/^[0-9\-\s]+$/, '電話格式錯誤'),
address: z.string().optional(),
email: z.string().email('Email 格式錯誤').optional().or(z.literal('')),
contacts: z.array(z.object({
name: z.string().min(1, '請填寫聯絡人姓名'),
title: z.string().optional(),
phone: z.string().regex(/^[0-9\-\s]+$/, '電話格式錯誤'),
email: z.string().email('Email 格式錯誤').optional().or(z.literal('')),
isPrimary: z.boolean(),
})).min(1, '至少需要一位聯絡人'),
cooperationStartDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
paymentTerms: z.enum(['none', 'net15', 'net30']).optional(),
});
元件架構
src/applets/customer-applet/
├── CustomerApplet.tsx # 容器組件
├── CustomerForm.tsx # 客戶表單(新增/編輯)
├── CustomerDetailPage.tsx # 客戶詳情頁
├── components/
│ ├── IndividualCustomerForm.tsx # 個人客戶表單
│ ├── CorporateCustomerForm.tsx # 企業客戶表單
│ ├── ContactList.tsx # 聯絡人列表
│ ├── AddressList.tsx # 地址列表
│ └── CustomerStatusBadge.tsx # 客戶狀態徽章
├── hooks/
│ ├── useCustomerForm.ts # 客戶表單邏輯
│ └── useDuplicateCheck.ts # 重複檢測邏輯
└── types.ts # TypeScript 類型定義
測試需求 (Test Requirements)
單元測試
- 表單驗證邏輯(必填欄位、格式驗證)
- 電話重複檢測邏輯
- 客戶等級計算邏輯
- 聯絡人管理邏輯(新增/刪除/設為主要)
整合測試
- 創建個人客戶 API 整合
- 創建企業客戶 API 整合
- 重複檢測 API 整合
- 停用/啟用客戶 API 整合
E2E 測試
- Scenario 1: 快速新增個人客戶
- Scenario 2: 新增企業客戶(含聯絡人)
- Scenario 3: 重複電話檢測
- Scenario 4: 編輯客戶資訊
- Scenario 5: 停用客戶
- Scenario 7: 表單驗證錯誤
驗收檢查清單 (Acceptance Checklist)
- 可創建個人客戶(必填欄位)
- 可創建企業客戶(含聯絡人)
- 電話重複檢測功能正常
- 可編輯客戶資訊
- 管理者可停用/啟用客戶
- 停用客戶不出現在選擇列表
- 表單驗證錯誤訊息正確顯示
- 響應式設計正常(桌面/手機)
- 審計日誌記錄正確
- 多租戶隔離正常
Story Points
估算: 8 points
理由:
- 需要處理個人客戶與企業客戶兩種類型
- 包含複雜的表單驗證(必填、格式、重複檢測)
- 聯絡人管理邏輯較複雜
- 停用/啟用客戶需要權限檢查
相關文檔
最後更新: 2025-11-04 撰寫者: Product Team