SPEC · v3
B2B 銷售管理系統
技術規格文件
一技術架構
1-1 Monorepo 結構
/
├── packages/
│ ├── backend/
│ │ ├── src/
│ │ │ ├── routes/
│ │ │ │ └── admin.routes.ts
│ │ │ ├── controllers/
│ │ │ │ └── admin.controller.ts
│ │ │ ├── services/
│ │ │ │ ├── notification.service.ts
│ │ │ │ ├── google-docs.service.ts
│ │ │ │ ├── bob.service.ts
│ │ │ │ ├── cs.service.ts
│ │ │ │ └── token.service.ts
│ │ │ ├── middleware/
│ │ │ │ ├── auth.ts
│ │ │ │ └── rbac.ts
│ │ │ ├── lib/
│ │ │ │ ├── s3.ts
│ │ │ │ └── cron.ts // prisma.ts 已移除,改用 Drizzle
│ │ │ └── index.ts
│ │ ├── src/db/ // Drizzle ORM(取代 prisma/)
│ │ └── package.json
│ └── dashboard/
│ ├── src/
│ │ ├── views/
│ │ │ ├── web/
│ │ │ │ ├── Dashboard.vue
│ │ │ │ ├── Contracts.vue
│ │ │ │ ├── ContractList.vue
│ │ │ │ ├── ContractDetail.vue
│ │ │ │ ├── ContractNew.vue
│ │ │ │ ├── ExpiringContracts.vue
│ │ │ │ ├── CsWorkspace.vue
│ │ │ │ ├── Settings.vue
│ │ │ │ └── AdminPanel.vue
│ │ │ ├── mobile/
│ │ │ │ ├── MobileDashboard.vue
│ │ │ │ └── MobileContracts.vue
│ │ │ └── token/
│ │ │ └── AdminTokenPage.vue
│ │ ├── components/
│ │ │ └── ContractForm.vue
│ │ ├── stores/
│ │ │ ├── auth.ts
│ │ │ ├── contracts.ts
│ │ │ └── notifications.ts
│ │ ├── router/
│ │ │ └── index.ts
│ │ └── composables/
│ └── package.json
├── package.json
└── railway.toml
1-2 技術棧
| 層 | 技術 |
|---|---|
| 後端 | Node.js + Express + TypeScript |
| ORM | Drizzle + PostgreSQL |
| 前端 | Vue 3 + Composition API + TypeScript + Vite |
| UI 元件 | NativUI |
| 狀態管理 | Pinia |
| 樣式 | Tailwind CSS |
| 表單驗證 | Zod + VeeValidate |
| 認證 | JWT(httpOnly Cookie) |
| 檔案儲存 | AWS S3(發票、錄音檔) |
| 文件產生 | Google Apps Script Web App |
| Email 服務 | Mailgun |
| Line 通知 | Line Messaging API |
| 排程 | node-cron |
| 部署 | Railway(兩個 Service) |
1-3 Railway 部署設定
# railway.toml
[[services]]
name = "backend"
source = "packages/backend"
startCommand = "node dist/index.js"
[[services]]
name = "dashboard"
source = "packages/dashboard"
startCommand = "npx serve dist"
二角色系統
enum Role {
ADMIN // 系統管理員,最高權限
MANAGER // 主管
INSIDE_SALES // 內勤業務
CUSTOMER_SUCCESS // 客戶成功(CS)
FIELD_SALES // 外勤業務,手機版
OFFICE_SUPPORT // 行政支援,可登入 + 一次性 token
}
路由守衛邏輯
// router/index.ts
router.beforeEach((to) => {
const auth = useAuthStore()
if (!auth.isLoggedIn) return '/login'
if (auth.role === 'FIELD_SALES' && to.path.startsWith('/web')) return '/mobile/dashboard'
if (auth.role !== 'FIELD_SALES' && to.path.startsWith('/mobile')) return '/web/dashboard'
if (auth.role !== 'ADMIN' && to.path.startsWith('/web/admin')) return '/web/dashboard'
})
三資料模型(Drizzle ORM Schema)
新增 Model
model Team {
id String @id @default(cuid())
name String
created_at DateTime @default(now())
users User[]
}
model OneTimeToken {
id String @id @default(cuid())
token String @unique @default(cuid())
contract_id String
action AdminAction
expires_at DateTime
used_at DateTime?
created_at DateTime @default(now())
contract Contract @relation(fields: [contract_id], references: [id])
}
enum AdminAction {
SEAL_CLIENT // 客戶回簽時間登記
SEAL_ADMIN // 管理部用印時間登記
REGISTER_KT // Kintone 登記(觸發結案)
}
model NotificationLog {
id String @id @default(cuid())
type NotificationType
channel NotificationChannel
recipient String
contract_id String?
sent_at DateTime @default(now())
status String // SENT | FAILED
error String?
}
enum NotificationType {
ADMIN_ACTION // 行政審核通知
CS_OPEN // CS 開通通知
CONTRACT_RETURN // 合約被退回
STUCK_ALERT // 卡關提醒
EXPIRY_WARNING // 到期預警
}
enum NotificationChannel {
EMAIL
LINE
}
model CsChecklist {
id String @id @default(cuid())
contract_id String @unique
payment_confirmed Boolean @default(false)
line_permission Boolean @default(false)
member_registered Boolean @default(false)
line_api_missing Boolean @default(false)
member_data_confirmed Boolean @default(false)
bob_created_at DateTime?
onboarding_type OnboardingType?
teaching_scheduled_at DateTime?
doc_provided_at DateTime?
created_at DateTime @default(now())
updated_at DateTime @updatedAt
contract Contract @relation(fields: [contract_id], references: [id])
}
enum OnboardingType {
PACKAGE // 安心上線包
TEACHING // 購買教學
DOCUMENT // 僅提供文件
}
Contract 擴充欄位
model Contract {
// ... 現有欄位保留 ...
sent_date DateTime? // 合約寄送日期(行政登記)
client_seal_date DateTime? // 客戶回簽日期
admin_seal_date DateTime? // 管理部用印日期
kt_date DateTime? // Kintone 登記日期
kt_link String? // Kintone 連結
gdrive_link String? // Google Drive 歸檔連結
google_docs_link String? // Google Docs 合約連結(系統產生)
// 合約文件額外欄位(Google Docs 模板填充用)
software_months Integer? // 軟體服務授權期(月份)
plan_label String? // 方案名稱(季繳/半年/一年期/二年期/三年期)
plan_amount Integer? // 方案金額
software_fee Integer? // 軟體費用
addon_fee Integer? // 加購費用
setup_fee Integer? // 導入設定費
items_purchased String? // 本次購買項目(逗號分隔)
main_module_fee Integer? // 模組總購費用(生活歐巴)
addon_module_fee Integer? // 加購總共費用(生活歐巴)
addon_items String? // 加購項目(美業歐巴)
secure_package_fee Integer? // 安心上線包費用(美業歐巴)
auth_start_date DateTime? // 軟體授權起始日(生活歐巴)
shipping_address String? // 合約紙本寄送地址
secondary_sales_id String? // 副業務
payment_link String? // 付款連結
contract_file_link String? // 合約電子檔連結
}
四前端路由結構
// 公開路由(無需登入)
/login
/token/:token
// Web(ADMIN / MANAGER / INSIDE_SALES / CUSTOMER_SUCCESS / OFFICE_SUPPORT) /web/dashboard
/web/contracts
/web/contracts/list
/web/contracts/new
/web/contracts/:id
/web/contracts/:id/cs
/web/expiring
/web/settings
/web/admin ← 後台管理(ADMIN 限定)
// Mobile(FIELD_SALES) /mobile/dashboard
/mobile/contracts
/mobile/contracts/:id
/token/:token
// Web(ADMIN / MANAGER / INSIDE_SALES / CUSTOMER_SUCCESS / OFFICE_SUPPORT) /web/dashboard
/web/contracts
/web/contracts/list
/web/contracts/new
/web/contracts/:id
/web/contracts/:id/cs
/web/expiring
/web/settings
/web/admin ← 後台管理(ADMIN 限定)
// Mobile(FIELD_SALES) /mobile/dashboard
/mobile/contracts
/mobile/contracts/:id
路由守衛:非 ADMIN 造訪
/web/admin 將導回 /web/dashboard。
五API 規格
5-1 認證
POST /api/auth/login # Email/密碼登入
POST /api/auth/logout # 登出(Token 黑名單)
GET /api/auth/me # 取得當前使用者
PATCH /api/users/:id/password # 密碼重設(ADMIN)
5-2 合約
GET /api/contracts # 列出合約(看板格式)
GET /api/contracts/list # 列出合約(表格格式)
GET /api/contracts/expiring?days=90 # 到期合約清單
POST /api/contracts/generate # 產生合約(含 Google Docs)
PATCH /api/contracts/:id # 更新合約
POST /api/contracts/:id/cancel # 取消合約
POST /api/contracts/:id/refund # 退款合約
GET /api/contracts/:id # 取得單一合約
5-3 一次性 Token(行政快速操作)
POST /api/admin-tokens/generate
body: { contract_id: string, action: AdminAction }
auth: requireRole(MANAGER, INSIDE_SALES, OFFICE_SUPPORT)
回傳: { token, expires_at, url }
POST /api/admin-tokens/verify
body: { token: string }
auth: 無需登入
回傳: { valid: boolean, action, contract }
POST /api/admin-tokens/consume
body: { token: string, date: string, extra?: object }
auth: 無需登入
行為: 驗證 token → 更新 Contract 對應欄位 → 標記 used_at → 觸發通知
5-4 CS 作業
GET /api/contracts/:id/cs-checklist
POST /api/contracts/:id/cs-checklist
body: { payment_confirmed, line_permission, member_registered, line_api_missing }
auth: requireRole(CUSTOMER_SUCCESS)
POST /api/contracts/:id/cs/bob-sync
auth: requireRole(CUSTOMER_SUCCESS)
行為: 呼叫 Bob API 建立組織,記錄 bob_created_at
PATCH /api/contracts/:id/cs/onboarding
body: { onboarding_type, teaching_scheduled_at?, doc_provided_at? }
auth: requireRole(CUSTOMER_SUCCESS)
PATCH /api/contracts/:id/cs/member-data
body: { member_data_confirmed: boolean }
auth: requireRole(CUSTOMER_SUCCESS)
5-5 通知
手動觸發通知已由 Admin 端點取代(見 5-8)。
GET /api/notifications/logs # 通知記錄(MANAGER / ADMIN)
| 觸發時機 | 類型 | 對象 | 內容 |
|---|---|---|---|
| 合約進入行政處理階段 | ADMIN_ACTION | OFFICE_SUPPORT | Email/Line 含一次性 token link |
| 客戶付款確認後 | CS_OPEN | CUSTOMER_SUCCESS | 通知開始 CS 作業 |
| 合約被退回 | CONTRACT_RETURN | 業務 | 退回原因 |
| 某階段 N 天未推進 | STUCK_ALERT | 負責單位 | 卡關提醒 |
| 合約到期前 90/60/30 天 | EXPIRY_WARNING | 業務、主管 | 續約提醒 |
5-6 業績目標
GET /api/quotas?year=&month=
POST /api/quotas
body: { user_id, year, month, quota_amount }
auth: requireRole(MANAGER, ADMIN)
PATCH /api/quotas/:id
auth: requireRole(MANAGER, ADMIN)
5-7 收款期數
GET /api/contracts/:id/periods
POST /api/contracts/:id/periods
PATCH /api/contracts/:id/periods/:periodId
POST /api/contracts/:id/periods/:periodId/invoice # 發票上傳(S3)
5-8 Admin 後台
POST /api/admin/trigger/stuck # 手動觸發卡關合約通知(ADMIN)
POST /api/admin/trigger/expiry # 手動觸發到期警告通知(ADMIN)
GET /api/admin/cron-logs # 查詢通知執行紀錄最近200筆(ADMIN)
六Google Docs 整合
實作方式:Google Apps Script Web App
後端不使用 Service Account,改以 Google Apps Script Web App 作為文件產生服務。
後端不使用 Service Account,改以 Google Apps Script Web App 作為文件產生服務。
流程
1. 後端組裝
2. POST 到 Google Apps Script Web App URL
3. Apps Script 複製模板 → 替換 placeholder → 回傳文件 URL
record 物件(key 對應 docx 模板中的 {{placeholder}})2. POST 到 Google Apps Script Web App URL
3. Apps Script 複製模板 → 替換 placeholder → 回傳文件 URL
環境變數
GOOGLE_APPS_SCRIPT_URL_BEAUTY= # 美業歐巴 Apps Script URL
GOOGLE_APPS_SCRIPT_URL_LIFESTYLE= # 生活歐巴 Apps Script URL
Placeholder Keys
Placeholder 格式:{{key}}
| 分類 | Keys |
|---|---|
| 共用 | contract_id, sales_name, contract_user_name, partya_reprsentative_name, partya_contact_phone, partya_enterprise_address, partya_enterprise_uniform_no |
| 合約期間 | contract_start_year/month/day, contract_end_year/month/day |
| 費用 | total_price, software_price, addon_price, import_setup_price |
| 付款方案 | quarter_plan/price, helf_year_plan/price, one_year_plan/price, two_year_plan/price, three_year_plan/price |
| 美業專用 | startup_plan, mini_shop_plan, expand_business_plan, flagship_plan, addon_plan_1/2, secure_online_plan/price, software_access_month |
| 生活專用 | main_system_module/price, addon_module/price, auth_start_year/month/day, auth_end_year/month/day |
七行政一次性 Token 頁面規格
路由:/token/:token(無需登入)
流程
1. 頁面載入 → 呼叫
2. Token 有效 → 顯示合約摘要 + 操作介面
3. Token 無效/過期 → 顯示錯誤頁,提示聯繫業務重新發送
4. 行政選擇日期 → 點擊確認 → 呼叫
5. 成功 → 顯示完成頁,token 失效,觸發下一步通知
POST /api/admin-tokens/verify2. Token 有效 → 顯示合約摘要 + 操作介面
3. Token 無效/過期 → 顯示錯誤頁,提示聯繫業務重新發送
4. 行政選擇日期 → 點擊確認 → 呼叫
POST /api/admin-tokens/consume5. 成功 → 顯示完成頁,token 失效,觸發下一步通知
Action 對應
| Action | 標題 | 操作 | 更新欄位 |
|---|---|---|---|
SEAL_CLIENT | 登記客戶回簽日期 | DatePicker | client_seal_date |
SEAL_ADMIN | 登記管理部用印日期 | DatePicker | admin_seal_date |
REGISTER_KT | 登記 Kintone 寫入日期 | DatePicker + KT連結輸入 | kt_date、kt_link,觸發結案(WON) |
八通知服務實作規格
Email(Mailgun)
import formData from 'form-data';
import Mailgun from 'mailgun.js';
const mailgun = new Mailgun(formData);
const client = mailgun.client({ username: 'api', key: process.env.MAILGUN_API_KEY });
const mg = client.domains.domain(process.env.MAILGUN_DOMAIN);
// 行政審核通知範本
await mg.messages.create(process.env.MAILGUN_DOMAIN, {
from: `B2B Sales System <noreply@${process.env.MAILGUN_DOMAIN}>`,
to: admin_email,
subject: `[合約審核] ${contract_number} — ${action_label} 待處理`,
html: `...`
});
環境變數
MAILGUN_API_KEY= # Mailgun API Key
MAILGUN_DOMAIN= # 您的 Mailgun Domain(例:sandboxXXX.mailgun.org)
Line Messaging API
// 訊息格式
`[合約審核] ${contract_number}
店家:${store_name}
待處理:${action_label}
請點擊連結操作(48小時內有效):
${token_url}`
排程(node-cron)
// 每日 09:00 執行
cron.schedule('0 9 * * *', async () => {
await checkStuckContracts() // 掃描卡關合約
await checkExpiringContracts() // 掃描到期合約
})
// 卡關定義:各階段超過 N 天未推進(N 由系統設定決定)
// 到期預警:合約結束日前 90 / 60 / 30 天各發一次
九前端關鍵元件規格
9-1 AdminTokenPage.vue(/token/:token)
狀態機:
LOADING → 驗證 token
VALID → 顯示操作介面
SUCCESS → 顯示完成畫面
ERROR → 顯示錯誤(過期/已使用/無效)
9-2 Contracts.vue(看板)
- 每欄顯示合約卡片(w-64 flex-shrink-0)
- 點擊卡片展開 Drawer 顯示摘要
- Drawer 底部依狀態顯示對應操作 Modal(不再是 inline 按鈕)
- 支援左右橫向滑動(h-screen flex-col layout,overflow-x-auto)
- 各欄可獨立縱向捲動(overflow-y-auto)
9-3 ContractForm.vue(新增合約表單)
分區:
① 業務資訊:主業務(必填)、副業務(選填)
② 店家資訊:統編、店家名稱、聯絡人、電話、信箱、合約紙本寄送地址(必填)
③ 合約基本資訊:產品(美業歐巴/生活歐巴 下拉)、合約類型
④ 合約期間:起始日、結束日、軟體授權月數(必填)
⑤ 購買方案(依 product 動態顯示):
- 美業歐巴:Radio 起步/微型/擴大/旗艦 + 加購項目輸入 + 安心上線包費用
- 生活歐巴:Checkbox 主系統/加購模組 + 軟體授權起始日 + 加購模組項目
⑥ 費用明細:付款方案 Radio + 各費用欄位(生活歐巴額外顯示主系統/加購模組費用)
備註
9-4 ContractDetail.vue
- 合約詳情頁,恆顯示所有區塊(不再 v-if 隱藏空值,空值顯示 "-")
- 區塊:合約進度(視覺化步驟條)、業務資訊、店家資訊、合約基本資訊、
購買方案、費用明細、相關連結、備註
- 相關連結整合所有外部連結(Google Docs、Kintone、合約掃描檔、付款連結)
- 移除舊有 ContractAdminActions 區塊(行政動作已在 Contracts.vue 看板 Modal 處理)
- 產生文件按鈕在相關連結卡片內
9-5 AdminPanel.vue(/web/admin)
ADMIN 限定頁面,包含:
① 通知測試:
- 手動執行卡關合約通知(POST /api/admin/trigger/stuck)
- 手動執行到期警告通知(POST /api/admin/trigger/expiry)
- 顯示執行結果(送出封數 + 明細)
② 通知執行紀錄:
- 顯示最近 200 筆通知 log
- 欄位:時間、類型、頻道、收件人、合約連結、狀態、錯誤
9-6 CsWorkspace.vue(/web/contracts/:id/cs)
區塊一:四項確認 Checklist
- 付款確認 Checkbox
- Line@ 權限 Checkbox
- 會員註冊 Checkbox
- Line API 缺乏標記 Checkbox(橘色警示)
區塊二:會員資料確認
- 使用者在小幫手 Line@ 註冊後才可確認
- 確認後可填入會員相關備註
區塊三:店家開帳號資料(業務填寫)
- 顯示業務填寫的服務項目
- 若尚未填寫,顯示警示「請業務先填寫服務項目」
- CS 無法進行 Bob 建立組織,直到業務完成填寫
區塊四:Bob 建立組織
- 按鈕:呼叫 /cs/bob-sync
- 顯示 bob_created_at 時間戳
區塊五:安心上線包 / 教學 / 文件
- Radio:PACKAGE / TEACHING / DOCUMENT
- 選 TEACHING → 顯示教學時間 DatePicker
- 選 DOCUMENT → 顯示文件已提供按鈕
9-7 ExpiringContracts.vue
篩選:30天內 / 60天內 / 90天內
欄位:店家名稱、合約結束日、主業務、剩餘天數(依剩餘天數高亮)
快捷:點擊跳轉合約詳情
9-8 QuotaProgress.vue(Dashboard 內嵌)
顯示:本月目標金額 / 已達成金額 / 達成率進度條
MANAGER 可 Inline 編輯目標金額
十開發優先順序
P0 — MVP 核心流程 P0
| 項目 | 狀態 | 說明 |
|---|---|---|
| 合約建立介面 | ✓ 完成 | 取代 Google Sheet,含完整店家資訊欄位 |
| Google Docs 整合(Apps Script) | ✓ 完成 | 產生合約文件,連結回存系統 |
| 行政一次性 Token 機制 | ✓ 完成 | AdminTokenPage + token API |
| 通知服務 Email(Mailgun) | ✓ 完成 | 行政 token link 發送 |
P1 — 使用者功能完整 P1
| 項目 | 狀態 | 說明 |
|---|---|---|
| CS 作業介面 | ✓ 完成 | 四項確認 + Bob API + 教學分流 |
| Mobile 合約查詢 | ✓ 完成 | 外勤業務手機端唯讀 |
| 業績目標(SalesQuota API + UI) | ✓ 完成 | 主管設定目標,儀表板顯示達成率 |
| 到期合約清單 | ✓ 完成 | 取代 Google Sheet 分頁 |
| 管理員後台(通知測試 + Cron Log) | ✓ 完成 | AdminPanel + Admin API |
P2 — 強化 P2
| 項目 | 說明 |
|---|---|
| 排程卡關偵測 | 需通知服務先完成 |
| Google Drive PDF 歸檔 | Kintone 寫入後自動匯出 |
| 密碼重設 | ADMIN 操作 |
| 經銷商管理前端完整 UI | 補齊現有後端 |
P3 — Phase 2(名單與商機) P3
| 項目 | 說明 |
|---|---|
| 名單 CRUD | 開放現有隱藏功能 |
| 商機追蹤 | 追蹤紀錄、潛力分級 |
| 錄音檔上傳 | S3 擴充 |
| Excel 批次匯入 | 名單批次建立 |
| 官網填單 API 串接 | 需官網配合 |
| 筋斗雲整合 | 視訪談結果決定 |
P4 — Phase 3(外部系統自動化)
| 項目 | 說明 |
|---|---|
| Kintone API 自動同步 | 取代人工登打 |
| Bob API 完整整合 | 需 Bob API 文件 |
| 金流串接 | 視付款期數訪談結果決定 |