獵人頭模組 PRD
產品名稱:獵人頭模組(Headhunter Module)
一句話:讓「誰帶來這份履歷」可追蹤、讓 KOC 能打包推薦、讓夥伴獵頭(奧德士)能獨佔曝光職缺——應徵仍走 WPORT 人才庫管線。
PRD 代號:pm_46(文件夾序號;Coda / Trello 建議用產品名「獵人頭模組」)
子能力:① 推薦歸因(ref/utm_source)② 職缺推薦清單 ③ 夥伴職缺綁定與官網同步
不重複定義:API 金鑰與企業人才庫 UI 沿用pm_41。
產出 persona:CEO with PM Background(SaaS / HR tech lens)。
1. 文件資訊
| 項目 | 內容 |
|---|---|
| 功能名稱 | 獵人頭模組(Headhunter Module) |
| 檔案路徑 | doc/feature/pm_46/headhunter-module-prd.md |
| 文件類型 | 產品需求規格書(歸因 + 清單 + 夥伴曝光分流) |
| 適用對象 | 前端、後端、產品、QA、BD(夥伴獵頭) |
| 最後更新 | 2026/07/03 |
| 版本 | 1.1.0 |
| PRD 代號 | pm_46 |
| Coda rowId | i-Hnzf5ItlAO |
| Coda / Backlog 名稱 | 獵人頭模組 |
| 關聯 PRD | pm_41(企業 API 金鑰自助 + 人才庫 UI)、pm_45(企業人才 CRM)、pm_37(Enterprise REST API 基建) |
| 對應 Storybook | N/A(v1 以歸因基建為主;模組 2 清單頁待後續 mockup) |
| 建立日期 | 2026/07/03 |
| 優先級 | 模組 1 P0 → 模組 2 P1 → 模組 3 P2 |
2. 專案背景與範圍
2.1 戰略背景:什麼是「獵人頭模組」?
獵人頭模組服務 WPORT 三類獵頭場景,共用同一條應徵與人才庫管線:
| 場景 | 角色 | 本模組提供的能力 |
|---|---|---|
| C2C 全民獵頭 | 學生、獨立獵頭、KOC | ?ref= 歸因、職缺蒐藏清單 |
| B2B 夥伴獵頭 | 奧德士(Odexus)等簽約夥伴公司 | 職缺綁定夥伴、API 同步至夥伴官網 |
| 企業 HR | 發職缺的雇主 | 可見性三態、綁定/解約夥伴、查看推薦來源 |
核心目標:每一筆應徵都能回答「是誰帶來的?」;夥伴獨佔職缺僅改變 職缺在哪裡曝光,不改變 應徵進哪條人才庫。
2.2 與 pm_41 的邊界(Eric 確認,2026/07/03)
pm_41 是企業端 API 金鑰自助(
wpk_live_)與 人才庫畫面 的 SoT;獵人頭模組 不另建 第二套 Key 或獨立機構後台。
| 能力 | 歸屬 PRD | 獵人頭模組是否重複定義 |
|---|---|---|
| 公司後台申請 / 輪替 API Key | pm_41 | ❌ 沿用 |
| 企業人才庫列表、應徵/來訪 UI | pm_41 / pm_45 | ❌ 沿用 |
?ref= / ?utm_source= 歸因 | pm_46 模組 1 | ✅ 本 PRD |
| KOC 職缺蒐藏清單 | pm_46 模組 2 | ✅ 本 PRD |
| 職缺綁定奧德士、曝光分流 | pm_46 模組 3 | ✅ 本 PRD |
綁定奧德士後的產品行為(關鍵):
| 面向 | WPORT 公開站(搜尋/大廳) | 發職缺企業後台(人才庫) | 奧德士官網 |
|---|---|---|---|
| 職缺 listing | ❌ 不出現 | ✅ HR 仍可管理該職缺 | ✅ 經 API 同步後露出 |
| 求職者應徵 | 僅能透過夥伴導流連結進入 | ✅ 照常出現在此職缺的人才庫 | ✅ 從官網投遞 |
| 推薦歸因 | — | ✅ 顯示獵頭/夥伴來源(模組 1) | utm_source 標記奧德士 |
換句話說:綁給奧德士的人才,發職缺的公司在 WPORT 人才庫裡一樣看得到;唯一差別是該職缺 不會再出現在 WPORT 公開求職站,改在 奧德士官網 曝光。人才庫體驗對 HR 不變(pm_41 畫面)。
企業 HR 可 主動解約(解除夥伴綁定)→ 職缺恢復 WPORT 公開曝光規則,奧德士 API 與官網同步停止收錄該職缺。
2.3 模組依賴架構
flowchart TB
subgraph pm41 [pm_41 沿用]
API[企業 API 金鑰 wpk_live_]
CRM[企業人才庫 UI]
end
subgraph hunter [獵人頭模組 pm_46]
M1[模組 1:UTM 歸因 P0]
M2[模組 2:職缺蒐藏清單 P1]
M3[模組 3:夥伴獨佔職缺綁定 P2]
end
M1 --> M2
M1 --> M3
M3 --> API
M3 --> CRM
| 模組 | 名稱 | 優先級 | 依賴 |
|---|---|---|---|
| 1 | UTM 歸因系統 | P0 | 無(地基) |
| 2 | 中間獵頭/KOC 職缺蒐藏清單 | P1 | 模組 1 |
| 3 | 夥伴獨佔職缺綁定與同步 | P2 | 模組 1、pm_41 API |
2.4 範圍界定
包含(v1)
- 職缺可見性三態:
PUBLIC/SECRET_URL/DRAFT - 前端歸因參數擷取與持久化(Cookie + LocalStorage,30 天);Cookie 橫幅照常顯示,歸因 Cookie 列功能性必要(§11 #2 已決)
- 應徵寫入時自動帶入推薦人 / 夥伴歸因;v1 僅在
applications加欄位,不建獨立referrals表(§11 #3 已決) - Last Click 歸因、防自推、防重複投遞作弊
- 獨立獵頭職缺蒐藏清單 CRUD 與分享 URL
- 清單內職缺連結自動注入
ref - 職缺 綁定夥伴獵頭公司(如奧德士):WPORT 公開站隱藏、夥伴官網 API 同步露出
- 企業 HR 主動解約(解除綁定)
- 夥伴透過 pm_41 公司後台
wpk_live_Key 拉取已綁定職缺(不另建wak_live_)
排除(v1 不做)
- 推薦人分潤 / 佣金結算(另案)
- 推薦人儀表板與即時報表(v2)
- 獨立
agency_users或機構專用登入站(夥伴 = WPORT 公司帳號,Key 走 pm_41) - 第二套 API Key 前綴(
wak_live_)或獨立機構api_keys表 - 點擊未投遞的
referral_click_events漏斗表(v1.1 再議) - 多點觸控歸因(First Click / Linear)— v1 僅 Last Click
- Webhook 推送應徵事件給夥伴(v2)
- 重複定義 pm_41 人才庫 UI / API 金鑰自助流程
2.5 成功條件(6 個月 KPI)
| 指標 | 目標 |
|---|---|
帶 ref / utm_source 的職缺點擊佔總點擊 | ≥ 15% |
| 可歸因應徵佔總應徵 | ≥ 10% |
| 活躍職缺蒐藏清單數 | ≥ 200 |
| 簽約夥伴獵頭公司數(含 Odexus) | ≥ 3 |
| 歸因資料與應徵紀錄一致率(抽樣稽核) | ≥ 99.5% |
| 夥伴 bound jobs API p95 latency | < 500ms |
3. User Stories(使用者故事)
3.1 企業 HR
| ID | 身為… | 我想要… | 以便… | 優先級 |
|---|---|---|---|---|
| US-HR-01 | 企業 HR | 將職缺設為 PUBLIC(公開大廳) | 求職者可在搜尋結果找到並投遞 | P0 |
| US-HR-02 | 企業 HR | 將職缺設為 SECRET_URL(僅連結可見) | 只讓指定獵頭或內推對象看到,不曝光於公開搜尋 | P0 |
| US-HR-03 | 企業 HR | 將職缺存為 DRAFT 草稿 | 完成內容後再上架,避免半成品外洩 | P0 |
| US-HR-04 | 企業 HR | 指派 SECRET_URL 職缺給特定獵頭用戶 | 該獵頭可將職缺加入自己的推薦清單 | P1 |
| US-HR-05 | 企業 HR | 將職缺綁定給夥伴獵頭公司(如奧德士) | 職缺不在 WPORT 公開站露出,改由夥伴官網曝光;應徵仍進我方人才庫 | P2 |
| US-HR-06 | 企業 HR | 在應徵名單 / 人才庫看到「推薦來源」(獵頭用戶 / 夥伴 / 直接投遞) | 評估各渠道人才品質;沿用 pm_41 人才庫畫面 | P0 |
| US-HR-07 | 企業 HR | 複製帶歸因參數的職缺分享連結 | 追蹤自家員工內推成效 | P0 |
| US-HR-08 | 企業 HR | 主動解約(解除夥伴綁定) | 職缺恢復 WPORT 公開曝光;夥伴官網停止同步 | P2 |
3.2 一般用戶 / 中間獵頭(Middle Hunter / KOC)
| ID | 身為… | 我想要… | 以便… | 優先級 |
|---|---|---|---|---|
| US-MH-01 | 獨立獵頭 | 取得個人專屬推薦連結 ?ref={my_user_id} | 分享後系統能歸因我帶來的應徵 | P0 |
| US-MH-02 | 獨立獵頭 | 建立「職涯推薦清單」並勾選多個職缺 | 一次打包推薦給求職者,像播放清單一樣方便 | P1 |
| US-MH-03 | 獨立獵頭 | 取得清單專屬 URL wport.co/list/{list_id}?ref={hunter_id} | 在社群一次分享多個機會 | P1 |
| US-MH-04 | 獨立獵頭 | 清單內每個職缺連結自動帶我的 ref | 不用手動逐條改 URL,降低操作失誤 | P1 |
| US-MH-05 | 獨立獵頭 | 只能加入 PUBLIC 或「已指派給我的 SECRET_URL」職缺 | 不會把未授權的私密職缺外洩 | P1 |
| US-MH-06 | 一般用戶(學生/KOC) | 分享單一職缺連結給同學 | 若同學應徵成功,平台能記錄是我推薦的 | P0 |
| US-MH-07 | 獨立獵頭 | 編輯 / 下架自己的推薦清單 | 職缺過期或已關閉時維護清單品質 | P1 |
3.3 夥伴獵頭公司(如奧德士 Odexus)
奧德士在 WPORT 上為一般公司帳號(夥伴獵頭公司);API Key 申請 / 輪替走 pm_41 公司後台,本模組不重複定義 Key UI。
| ID | 身為… | 我想要… | 以便… | 優先級 |
|---|---|---|---|---|
| US-AG-01 | 奧德士管理員 | 在公司後台(pm_41)申請 / 輪替 wpk_live_ Key | 與我方 ATS / 官網系統對接 | P2 |
| US-AG-02 | 奧德士工程師 | 用 Key 拉取已綁定給我們的職缺 | 同步至奧德士官網,不需手動篩選 | P2 |
| US-AG-03 | 奧德士工程師 | API 回傳的職缺含可對外投放的導流 URL | 求職者從官網投遞,歸因標記奧德士 | P2 |
| US-AG-04 | 奧德士管理員 | 在 pm_41 撤銷外洩 Key 並輪替 | 降低憑證外洩風險 | P2 |
| US-AG-05 | 奧德士營運 | 透過 ?utm_source={partner_company_id} 追蹤夥伴層級歸因 | 與模組 1 歸因系統對接 | P0 |
| US-AG-06 | 發職缺企業 HR | 綁定後仍能在 WPORT 人才庫看到該職缺的應徵者 | 招募流程不中斷;僅曝光渠道分流 | P2 |
3.4 求職者(附帶)
| ID | 身為… | 我想要… | 以便… | 優先級 |
|---|---|---|---|---|
| US-JS-01 | 求職者 | 透過獵頭分享的連結順利瀏覽並投遞 | 不會因為技術問題投遞失敗 | P0 |
| US-JS-02 | 求職者 | 在 30 天內從不同推薦連結進入後仍能正確投遞 | 最後點擊的推薦人獲得歸因(Last Click) | P0 |
4. Functional Requirements(功能性需求)
4.1 模組 1:UTM 歸因系統(P0)
FR-1.1 職缺可見性三態
| 狀態 | 代碼 | 搜尋可見 | URL 直達 | 可投遞 | 備註 |
|---|---|---|---|---|---|
| 公開大廳 | PUBLIC | ✅ | ✅ | ✅ | 對齊 BR-009 上架(Published) |
| 秘密連結 | SECRET_URL | ❌ | ✅(需有效 URL) | ✅ | 對齊 BR-009 私人(Private)正式落地 |
| 草稿 | DRAFT | ❌ | ❌(僅作者/公司成員) | ❌ | 對齊草稿態;未滿足 BR-008 不可上架 |
visibility與既有status(上架/下架/已關閉/已刪除)正交:終態職缺(已關閉/已刪除)一律不可投遞,不受visibility影響。PUBLIC職缺須滿足 BR-008(必填欄位 + 公司已驗證)方可生效。
FR-1.2 歸因參數擷取(前端)
進入 WPORT 職缺相關頁面時,若 URL 含以下任一參數,前端必須寫入瀏覽器:
| 參數 | 語意 | 儲存值 |
|---|---|---|
ref | C2C 個人推薦人 | user_id(WPORT 內部 ID,非 email) |
utm_source | B2B 夥伴獵頭公司來源 | partner_company_id(夥伴在 WPORT 的 companies.id) |
儲存策略
- 主要:
HttpOnly不可行(需前端讀取)→ 使用 第一方 Cookiewport_ref(JSON payload,Secure + SameSite=Lax)。 - 備援:
localStoragekeywport_ref_v1(Cookie 被阻擋時 fallback)。 - 效期:30 天(
max-age=2592000);每次有效點擊帶新參數時覆寫(Last Click)。 - 儲存結構(語意):
{
"referrer_type": "user | partner",
"referrer_id": "string",
"captured_at": "ISO8601",
"landing_url": "string",
"expires_at": "ISO8601"
}
FR-1.3 歸因寫入(後端)
建立 Applications 紀錄時:
- 讀取請求附帶的歸因 payload(由前端從 Cookie 注入 request header
X-Wport-Referral或 body 欄位referral)。 - 後端獨立驗證(不信任前端單方面宣告):
referrer_id存在且帳號狀態有效(未刪除,BR-012)。referrer_type=user時,referrer_id ≠ applicant_user_id(禁止自推,BR-039)。referrer_type=partner時,partner_company_id存在、為已認證之夥伴獵頭公司且status=active。- 歸因時間未超過 30 天。
- 驗證通過 → 寫入
Applications.referrer_type、Applications.referrer_id、Applications.referral_captured_at。 - 驗證失敗 → 應徵仍成功建立,但
referrer_*為null(歸因為增值能力,不阻斷核心投遞)。
FR-1.4 歸因原則:Last Click
- 30 天窗口內,最後一次帶有效
ref或utm_source的點擊覆寫先前歸因。 - 若最後一次點擊為無參數的直接進入 → 清除歸因(視為直接投遞)。
ref與utm_source同時存在於同一 URL 時,優先序:ref>utm_source(個人歸因優先於夥伴)。
FR-1.5 防作弊機制
| 規則 | 說明 | 對應 BR |
|---|---|---|
| 禁止自推 | 推薦人 ID = 應徵者 ID → 不寫入歸因 | BR-039(新) |
| 重複投遞 | 同一用戶同一職缺 72h 內不可再投(既有規則) | BR-011 |
| 無效推薦人 | 推薦人帳號已刪除 / 停權 → 不寫入歸因 | BR-012 |
| 過期歸因 | 超過 30 天 → 不寫入歸因 | BR-039 |
| 機器刷量 | 同一 IP + 同一 ref + 同一 job 每小時 > 50 次點擊不更新歸因 cookie(防刷) | BR-039 |
FR-1.6 與 SEO / Canonical 相容
- 對齊 pm_33:
ref/utm_source為追蹤參數,不影響 Canonical URL(Canonical 指向無查詢參數的乾淨職缺 URL)。 - 搜尋引擎爬蟲不寫入歸因 Cookie(辨識 bot User-Agent 跳過)。
4.2 模組 2:中間獵頭/KOC 職缺蒐藏清單(P1)
FR-2.1 清單生命週期
| 動作 | 說明 |
|---|---|
| 建立 | 登入用戶可建立清單;必填:標題;選填:描述、封面圖 |
| 編輯 | 僅擁有者可編輯標題 / 描述 / 職缺成員 |
| 刪除 | 軟刪除;已分享 URL 回 410 Gone |
| 上限 | 每用戶最多 20 組清單;每清單最多 50 個職缺(BR-041) |
FR-2.2 職缺加入條件
獵頭用戶僅可加入符合以下任一條件的職缺:
visibility = PUBLIC且status = 上架且bound_partner_company_id IS NULL(未綁定夥伴)visibility = SECRET_URL且status = 上架且存在job_hunter_assignments指派給該用戶
綁定夥伴的職缺不在 WPORT 公開站露出,亦不可加入 KOC 蒐藏清單(見 E-24)。
不符合條件的職缺在 UI 顯示為不可選,API 回傳 403 JOB_NOT_ELIGIBLE。
FR-2.3 分享 URL
- 格式:
https://wport.co/list/{list_id}?ref={hunter_user_id} list_id:不透明 UUID(非自增整數,防枚舉)- 公開頁:不需登入即可瀏覽清單與職缺摘要
- 清單頁載入時:將
ref寫入模組 1 歸因 Cookie(與直接職缺連結行為一致)
FR-2.4 動態注入 ref(核心)
當求職者在清單頁點擊任一職缺時,系統必須:
- 取清單擁有者的
hunter_user_id(來自清單owner_user_id,非 URL 參數—防篡改)。 - 生成職缺 URL:
/job/{id}/{slug}?ref={owner_user_id} - 若 URL 已有
ref,以清單擁有者覆寫(清單場景優先)。 - 同時更新歸因 Cookie(Last Click)。
設計理由:URL 的
ref可被使用者手動修改,但清單點擊行為以伺服器端/前端路由守衛強制注入,確保歸因正確。
FR-2.5 清單內容動態更新
- 職缺進入終態(已關閉/已刪除/下架)→ 清單內該項顯示 boundary「此職缺已關閉」,連結停用(對齊 BR-009 / pm_42)。
- 職缺從
SECRET_URL移除獵頭指派 → 下次清單擁有者儲存時自動剔除該項並通知。
4.3 模組 3:夥伴獨佔職缺綁定與同步(P2)
FR-3.1 夥伴獵頭公司(Partner Company)
- 夥伴(如奧德士)在 WPORT 上為一般 Company 實體,另標記
is_headhunter_partner = true(或等價旗標)。 - 不建獨立
agency_users登入體系;夥伴員工用 WPORT 統一帳號進入該公司企業後台。 - API Key 核發 / 輪替 / 撤銷 完全沿用 pm_41(公司後台「API 金鑰自助申請」或 AMS 軌道 B);本模組僅擴充 API 回傳範圍 與 職缺曝光規則。
FR-3.2 職缺綁定夥伴(原「獨佔」)
企業 HR 發布職缺時可選擇「綁定夥伴獵頭」(如奧德士)→ 寫入 bound_partner_company_id(FK → companies.id)。
綁定後行為:
| 面向 | 行為 |
|---|---|
| WPORT 公開站(搜尋 / 職缺大廳 / SEO) | ❌ 不出現(即使 visibility=PUBLIC) |
| 發職缺企業後台 | ✅ 職缺可正常管理;人才庫照常收到應徵(pm_41 / pm_45) |
| 奧德士官網 | ✅ 經 API 同步後露出;求職者從官網導流進入投遞 |
| 歸因 | 從夥伴導流進入時寫入 utm_source={partner_company_id} |
解約(企業主動解除綁定):
- HR 執行「解除夥伴綁定」→
bound_partner_company_id = NULL。 - 職缺恢復原本
visibility的 WPORT 曝光規則。 - 奧德士下次 API 同步不再收到該職缺;官網應下架(由奧德士端排程處理)。
- 既有應徵紀錄與人才庫資料保留,不刪除。
非綁定職缺:bound_partner_company_id = NULL;夥伴 API 不可取得其他企業的職缺。
FR-3.3 API Key:沿用 pm_41(不重複定義)
| 能力 | 說明 |
|---|---|
| Key 類型 | wpk_live_(pm_41 SoT) |
| 申請入口 | 夥伴公司企業後台 → API 金鑰(pm_41 DR-8) |
| 多把具名 Key | ≤ 10 把 / 公司(BR-API-CLI-004) |
| Scope 勾選 | 沿用 pm_41;新增 scope:jobs:partner_bound_read(僅夥伴公司可勾) |
| 到期 / 輪替 | 強制 30/60/90 天 + key-for-key 輪替(BR-API-CLI-008/009) |
| 權限 | 該 scope 下唯讀;僅 GET 已綁定給該夥伴公司的職缺 |
FR-3.4 夥伴拉取已綁定職缺(擴充 pm_41 Enterprise API)
Authorization: Bearer wpk_live_xxxx
GET /v1/jobs?filter=partner_bound
後端流程(Key 須含 jobs:partner_bound_read scope):
- 驗證
wpk_live_→ 解析company_id(奧德士的公司 ID) - 確認該
company_id為is_headhunter_partner = true - 查詢條件強制注入:
bound_partner_company_id = {company_id}ANDstatus = 上架 - 不回傳未綁定職缺、草稿、已關閉職缺
- 分頁:
page,page_size(max 100)
發職缺企業用自家
wpk_live_(職缺 scope)管自己的職缺;奧德士用自家wpk_live_(partner_boundscope)管別人綁給自己的職缺——同一套 Key 機制,不同 scope 與過濾邏輯。
FR-3.5 夥伴官網導流 URL
API 回傳的 job_url 供奧德士官網投放,格式語意:
https://wport.co/job/{job_id}/{slug}?token={signed_token}&utm_source={partner_company_id}
signed_token:HMAC-SHA256,payload 含{job_id, partner_company_id, bound_at};效期跟隨綁定生命週期(綁定存在即有效,解約即失效)。- 驗證失敗或已解約 → 職缺頁 403「此連結已失效」。
- 無 token 的 WPORT 公開直達:綁定中職缺 → 404(不在公開站露出)。
- Token 驗證通過 → 可投遞;寫入
utm_source歸因 Cookie;應徵進發職缺企業的人才庫。
FR-3.6 與 pm_41 的邊界(已決)
| 維度 | pm_41 | pm_46 模組 3 |
|---|---|---|
| API Key 機制 | wpk_live_、自助申請、scope、到期 | 沿用,僅新增 jobs:partner_bound_read scope |
| 人才庫 UI | 企業後台列表、應徵/來訪 | 沿用;綁定職缺應徵照常出現 |
| 職缺曝光 | 一般上架規則 | 綁定夥伴 → WPORT 公開隱藏、夥伴官網露出 |
| 夥伴帳號 | 公司帳號 + 企業後台 | 沿用,不建獨立機構後台 |
5. Non-Functional Requirements(非功能性需求)
5.1 資安(Security)
| 項目 | 要求 |
|---|---|
| API Key 儲存 | 僅存 bcrypt / SHA-256 hash;明文不落庫 |
| Key 傳輸 | 僅 HTTPS;禁止出現在 URL query |
| Signed Token | HMAC-SHA256 + 伺服器端 secret(KMS 管理);secret 每季輪替 |
| 歸因防篡改 | 後端驗證推薦人有效性;不信任客戶端單方面宣告 |
| IDOR 防護 | 清單/職缺/夥伴綁定操作皆驗證擁有權或指派關係 |
| Secret scanning | wpk_live_ 前綴(pm_41)支援 GitHub Secret Scanning |
| Audit Log | 沿用 pm_41 Key 使用紀錄;partner_bound 查詢另記 partner_company_id |
| PII | 歸因紀錄不額外收集求職者敏感資料;僅關聯既有 Applications |
5.2 效能(Performance)
| 項目 | 目標 |
|---|---|
| 歸因 Cookie 寫入 | 前端同步完成,不阻塞頁面渲染(< 5ms) |
| 應徵寫入含歸因驗證 | p95 < 100ms 額外開銷 |
GET /v1/jobs?filter=partner_bound | p95 < 500ms(100 筆/頁) |
| 清單公開頁 | LCP < 2.5s(50 職缺以內) |
| 歸因驗證查詢 | 推薦人有效性查詢走 Redis 快取(TTL 5min) |
5.3 API Rate Limiting
| 端點 | 限制 | 回應 |
|---|---|---|
GET /v1/jobs?filter=partner_bound | 300 req/min per API Key(沿用 pm_41) | 429 + Retry-After |
GET /v1/jobs/{id} | 600 req/min per API Key | 同上 |
| 歸因點擊寫入(前端 beacon) | 60 req/min per IP | 靜默丟棄 |
| 清單公開頁 | 120 req/min per IP | 429 |
| 月配額 | 沿用 pm_41 公司月配額 5000(夥伴查詢計入該公司 quota) | 429 + 用量 header |
回應 Header(沿用 pm_41):
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1720000000
5.4 可用性與可靠性
| 項目 | 要求 |
|---|---|
| 歸因系統可用性 | 歸因服務故障時,應徵主流程不受阻(降級為無歸因) |
| API Key 輪替 | 沿用 pm_41 key-for-key 輪替與過期換新(BR-API-CLI-009) |
| 資料一致性 | Applications.referrer_* 一經寫入不可修改(防事後篡改) |
| 跨裝置 | 歸因以 Cookie 為主;跨裝置不延續(v1 不支援) |
5.5 合規與隱私
- Cookie 橫幅:v1 顯示 Cookie 同意橫幅(業界常規);
wport_ref歸因 Cookie 列為功能性必要 Cookie,於 Cookie / 隱私政策揭露用途(記錄推薦來源以完成應徵歸因);使用者接受網站 Cookie 後寫入(§11 #2 已決)。 - 推薦人可見範圍:企業 HR 可看應徵的推薦來源(pm_41 人才庫);推薦人不可看到求職者 PII(v1)。
- 夥伴 API 回傳資料不含求職者個資(僅職缺資訊)。
5.6 可觀測性
- 指標:
referral_capture_rate,referral_apply_attribution_rate,partner_bound_api_error_rate - 告警:夥伴 API 5xx > 1% 持續 5 分鐘 → PagerDuty
- 歸因異常:單日自推嘗試 > 100 次 → 安全日誌
6. Data Models(資料結構模型)
本節為 RD 建表參考。外鍵關聯以
→標示。索引建議附於各表末。
6.1 ER 關聯總覽
erDiagram
Companies ||--o{ Jobs : publishes
Companies ||--o{ Jobs : "bound_partner"
Users ||--o{ JobLists : owns
JobLists ||--o{ JobListItems : contains
Jobs ||--o{ JobListItems : "in list"
Users ||--o{ JobHunterAssignments : "assigned to"
Jobs ||--o{ JobHunterAssignments : "assigned job"
Users ||--o{ Applications : applies
Jobs ||--o{ Applications : "applied to"
Users ||--o{ Applications : "referrer (user)"
Companies ||--o{ Applications : "referrer (partner)"
ApiKey 實體沿用 pm_41,不在此重複定義 ER。
6.2 jobs(升級)
在既有 jobs 表新增欄位:
| 欄位 | 型態 | 約束 | 說明 |
|---|---|---|---|
visibility | ENUM('PUBLIC','SECRET_URL','DRAFT') | NOT NULL, DEFAULT 'DRAFT' | 可見性三態 |
bound_partner_company_id | BIGINT UNSIGNED | NULL, FK → companies.id | 綁定之夥伴獵頭公司(如奧德士);NULL = 未綁定 |
bound_at | TIMESTAMP | NULL | 綁定生效時間 |
secret_url_token | VARCHAR(64) | NULL, UNIQUE | SECRET_URL 職缺的非猜測路徑 token |
索引
idx_jobs_visibility_status(visibility,status) — 搜尋過濾idx_jobs_bound_partner(bound_partner_company_id,status) — 夥伴 API 查詢idx_jobs_secret_token(secret_url_token) — URL 直達
遷移策略
- 既有上架職缺 →
visibility = 'PUBLIC' - 既有草稿 →
visibility = 'DRAFT' bound_partner_company_id預設 NULL(不影響既有職缺)
6.3 companies 擴充(夥伴獵頭公司)
在既有 companies 表新增(或等價設定表):
| 欄位 | 型態 | 約束 | 說明 |
|---|---|---|---|
is_headhunter_partner | BOOLEAN | NOT NULL, DEFAULT false | 是否為簽約夥伴獵頭(如奧德士) |
partner_slug | VARCHAR(100) | NULL, UNIQUE | 對外識別用(如 odexus) |
索引:idx_companies_partner (is_headhunter_partner) WHERE is_headhunter_partner = true
奧德士 = WPORT 上的一間 Company +
is_headhunter_partner=true;API Key 走 pm_41api_keys表,不另建agency_api_keys。
6.4 job_lists(蒐藏清單主表)
| 欄位 | 型態 | 約束 | 說明 |
|---|---|---|---|
id | BIGINT UNSIGNED | PK | |
enc_id | VARCHAR(32) | NOT NULL, UNIQUE | 對外 UUID |
owner_user_id | BIGINT UNSIGNED | NOT NULL, FK → users.id | 獵頭 / KOC |
title | VARCHAR(100) | NOT NULL | 清單標題 |
description | TEXT | NULL | |
cover_image_url | VARCHAR(500) | NULL | |
status | ENUM('active','archived','deleted') | NOT NULL, DEFAULT 'active' | |
created_at | TIMESTAMP | NOT NULL | |
updated_at | TIMESTAMP | NOT NULL |
索引:idx_job_lists_owner (owner_user_id, status)
約束:每 owner_user_id 的 active 清單 ≤ 20
6.5 job_list_items(清單職缺關聯表)
| 欄位 | 型態 | 約束 | 說明 |
|---|---|---|---|
id | BIGINT UNSIGNED | PK | |
job_list_id | BIGINT UNSIGNED | NOT NULL, FK → job_lists.id | |
job_id | BIGINT UNSIGNED | NOT NULL, FK → jobs.id | |
sort_order | INT UNSIGNED | NOT NULL, DEFAULT 0 | 手動排序 |
added_at | TIMESTAMP | NOT NULL |
索引
uniq_list_job(job_list_id,job_id) — UNIQUE,防重複加入idx_list_items_job(job_id)
約束:每 job_list_id 的 items ≤ 50
6.6 job_hunter_assignments(秘密職缺指派)
| 欄位 | 型態 | 約束 | 說明 |
|---|---|---|---|
id | BIGINT UNSIGNED | PK | |
job_id | BIGINT UNSIGNED | NOT NULL, FK → jobs.id | |
hunter_user_id | BIGINT UNSIGNED | NOT NULL, FK → users.id | |
assigned_by_user_id | BIGINT UNSIGNED | NOT NULL, FK → users.id | 企業 HR |
assigned_at | TIMESTAMP | NOT NULL | |
revoked_at | TIMESTAMP | NULL |
索引:uniq_job_hunter (job_id, hunter_user_id) — UNIQUE
6.7 applications(歸因欄位擴充)
在既有 applications 表新增:
| 欄位 | 型態 | 約束 | 說明 |
|---|---|---|---|
referrer_type | ENUM('user','partner') | NULL | 推薦人類型;NULL = 直接投遞 |
referrer_id | BIGINT UNSIGNED | NULL | user → users.id;partner → companies.id(夥伴公司) |
referral_captured_at | TIMESTAMP | NULL | 歸因點擊時間(非應徵時間) |
referral_landing_url | VARCHAR(500) | NULL | 首次著陸 URL(稽核用) |
referral_job_list_id | BIGINT UNSIGNED | NULL, FK → job_lists.id | 若來自清單,記錄清單 ID |
索引:idx_applications_referrer (referrer_type, referrer_id, created_at)
設計決策(Eric 已決):v1 不建獨立
referrals/referral_click_events表;僅在投遞成功時寫入applications歸因欄位。應徵者照常進入發職缺企業的人才庫(pm_41 UI)。
7. Edge Cases(異常邊界情況與防呆設計)
7.1 歸因相關
| # | 情境 | 系統行為 | UI / API 回饋 |
|---|---|---|---|
| E-01 | 使用者點擊 ?ref=自己 的連結 | 寫入 Cookie,但應徵時後端拒絕歸因 | 應徵成功,無推薦標記 |
| E-02 | 30 天後才投遞 | 歸因過期,不寫入 referrer_* | 應徵成功,顯示「直接投遞」 |
| E-03 | 先點獵頭 A,再點獵頭 B,再投遞 | Last Click → 歸因 B | HR 看到推薦人 B |
| E-04 | 先點獵頭連結,再從 Google 直接進入同職缺 | 若第二次無 ref → 清除歸因 | 直接投遞 |
| E-05 | 推薦人帳號在投遞前被刪除 | 驗證失敗,不寫入歸因 | 應徵成功 |
| E-06 | Cookie 被阻擋,僅 localStorage 可用 | fallback 讀取 localStorage | 對使用者透明 |
| E-07 | 跨瀏覽器投遞(手機點、桌機投) | v1 不延續歸因 | 直接投遞 |
| E-08 | ref 與 utm_source 同時存在 | 優先 ref(個人) | — |
| E-09 | 同一職缺 72h 內重複投遞 | 拒絕投遞(BR-011) | 顯示冷卻倒數 |
| E-10 | 機器刷 ref 點擊 | 同 IP+ref+job 每小時 > 50 次不更新 Cookie | 靜默 |
7.2 職缺可見性
| # | 情境 | 系統行為 | UI / API 回饋 |
|---|---|---|---|
| E-11 | 存取 DRAFT 職缺 URL | 403(非公司成員) | 「職缺不存在」 |
| E-12 | SECRET_URL 無 token 直達 | 404 | 「職缺不存在」 |
| E-13 | 職缺上架中改為 DRAFT | 已分享連結失效 | 404 |
| E-14 | 綁定夥伴的職缺無 Signed Token 從 WPORT 公開進入 | 404(不在公開站露出) | 「職缺不存在」 |
| E-15 | 企業 HR 主動解約(解除綁定) | bound_partner_company_id 清空;舊導流 URL / Token 立即失效 | 403「此連結已失效」;夥伴 API 下次同步撈不到 |
| E-16 | 綁定夥伴的職缺仍有應徵者 | 應徵紀錄保留於發職缺企業人才庫(pm_41) | HR 照常於人才庫查看 |
| E-17 | 職缺已關閉但清單仍含該項 | 清單顯示灰階 boundary | 「此職缺已關閉」,連結停用 |
7.3 職缺蒐藏清單
| # | 情境 | 系統行為 | UI / API 回饋 |
|---|---|---|---|
| E-18 | 加入未指派的 SECRET_URL 職缺 | API 拒絕 | 403 JOB_NOT_ELIGIBLE |
| E-19 | 清單擁有者被停權 | 清單 URL 回 410 | 「此清單已下架」 |
| E-20 | 清單超過 50 職缺上限 | 拒絕新增 | 「已達清單上限(50)」 |
| E-21 | 使用者已有 20 組清單 | 拒絕建立 | 「已達清單數量上限(20)」 |
| E-22 | URL ref 與清單擁有者不同 | 點擊職缺時以清單擁有者為準 | 對獵頭透明(防止搶歸因爭議) |
| E-23 | 職缺從公開改秘密且未指派 | 自動從所有清單移除 | 通知清單擁有者 |
| E-24 | 綁定夥伴的職缺被加入 KOC 清單 | 拒絕加入(不在 WPORT 公開可選範圍) | 403 JOB_NOT_ELIGIBLE |
7.4 夥伴 API(pm_41 擴充)
| # | 情境 | 系統行為 | UI / API 回饋 |
|---|---|---|---|
| E-25 | API Key 已撤銷(pm_41) | 401 | {"error":"invalid_api_key"} |
| E-26 | 超過 rate limit | 429 + Retry-After | 標準 header |
| E-27 | Key 無 jobs:partner_bound_read scope | 403 | insufficient_scope |
| E-28 | 非夥伴公司用 partner_bound 查詢 | 403 | not_a_headhunter_partner |
| E-29 | 夥伴請求未綁定給自己的職缺 ID | 404 | 不揭露職缺存在 |
| E-30 | Key 過期輪替(pm_41 BR-API-CLI-009) | 僅 rotate endpoint 接受過期 Key | — |
7.5 Edge Case Coverage Matrix
| 類別 | 情境 | 系統行為 | UI 回饋 | 可重試 | 資料一致性 |
|---|---|---|---|---|---|
| Network | 應徵 API timeout | 不寫入 application | 逾時提示 + 重試 | Yes | 冪等鍵防重複 |
| Network | 夥伴 API 高延遲 | 超時 30s 斷開 | 429/504 | Yes | 唯讀,無副作用 |
| Data/Input | 無效 ref 非數字 | 忽略,不寫 Cookie | 正常瀏覽 | N/A | N/A |
| Data/Input | ref 指向不存在用戶 | 不寫 Cookie | 正常瀏覽 | N/A | N/A |
| Interruption | 投遞中離開頁面 | 以 server 應徵結果為準 | 回訪顯示結果 | Yes | 冪等 |
| Auth | Session 過期後投遞 | 導登入,保留歸因 Cookie | 登入後回跳 | Yes | Cookie 30 天內有效 |
| Logical | 歸因服務掛掉 | 降級:應徵成功無歸因 | 無感知 | N/A | 事後補償不允許 |
8. 商業規則對齊與新增提案
8.1 沿用的既有規則
| 規則 ID | 摘要 | 關聯模組 |
|---|---|---|
| BR-001 | Email 驗證後才能登入 | 建立清單、投遞 |
| BR-008 | 職缺發布必填欄位 | PUBLIC 上架 |
| BR-009 | 職缺狀態與可見性 | 三態擴充 |
| BR-011 | 72h 再投冷卻 | 防重複投遞 |
| BR-012 | 帳號刪除檢查 | 推薦人有效性 |
| BR-015 | 搜尋僅顯示上架職缺 | PUBLIC 且未綁定夥伴 |
| BR-016 | 角色權限 | HR 指派秘密職缺、解約夥伴綁定 |
| BR-API-CLI-004~010 | pm_41 API 金鑰規則 | 夥伴 Key 沿用 |
8.2 建議新增規則(待回寫 business-rules.md)
| 規則 ID | 摘要 |
|---|---|
| BR-039 | 應徵歸因:Last Click、30 天窗口、禁止自推、歸因寫入後不可修改;v1 僅寫 applications |
| BR-040 | 職缺可見性三態;綁定夥伴的職缺不進 WPORT 公開搜尋,改由夥伴官網曝光 |
| BR-041 | 職缺蒐藏清單:每用戶 ≤ 20 組、每組 ≤ 50 職缺 |
| BR-042 | 夥伴綁定:HR 可綁定/解約;綁定期間僅夥伴可 API 同步;應徵仍進發職缺企業人才庫 |
| BR-043 | 夥伴拉取職缺:沿用 pm_41 wpk_live_ + scope jobs:partner_bound_read(不另建 Key 體系) |
9. Flowcharts
9.1 歸因 User Flow
flowchart TD
A[求職者點擊帶 ref/utm 的連結] --> B{URL 參數有效?}
B -->|否| C[正常瀏覽,不寫歸因]
B -->|是| D[寫入 Cookie/LocalStorage 30天]
D --> E[瀏覽職缺/清單]
E --> F{點擊投遞?}
F -->|否| G[離開或繼續瀏覽]
F -->|是| H{已登入且履歷就緒?}
H -->|否| I[導登入/補資料]
I --> F
H -->|是| J[後端驗證歸因]
J --> K{通過?}
K -->|是| L[建立 Application + referrer]
K -->|否| M[建立 Application,referrer=null]
L --> N[投遞成功]
M --> N
9.2 清單 → 歸因 System Flow
sequenceDiagram
participant Seeker as 求職者
participant ListPage as 清單頁
participant Cookie as 歸因 Cookie
participant JobPage as 職缺頁
participant API as 後端 API
Seeker->>ListPage: GET /list/{id}?ref={hunter}
ListPage->>Cookie: 寫入 ref=hunter(Last Click)
Seeker->>ListPage: 點擊職缺 A
ListPage->>JobPage: 導向 /job/A?ref={owner_id}(強制注入)
JobPage->>Cookie: 覆寫 ref=owner_id
Seeker->>API: POST 應徵 + referral payload
API->>API: 驗證 ref≠self、未過期、推薦人有效
API-->>Seeker: 201 應徵成功
9.3 夥伴職缺同步 System Flow
sequenceDiagram
participant Odexus as 奧德士官網/ATS
participant API as WPORT API(pm_41)
participant DB as 資料庫
participant HR as 發職缺企業人才庫
Odexus->>API: GET /v1/jobs?filter=partner_bound(wpk_live_)
API->>API: 驗證 scope + is_headhunter_partner
API->>DB: SELECT jobs WHERE bound_partner_company_id=奧德士 AND status=上架
DB-->>API: 職缺列表(含導流 URL)
API-->>Odexus: 200 JSON
Note over Odexus: 職缺露出於奧德士官網(非 WPORT 公開站)
Odexus->>API: 求職者經導流 URL 投遞
API->>HR: 應徵寫入 applications + 人才庫(pm_41 UI)
10. 版本凍結(v1 / v2)
| 項目 | v1 | v2 | Blocking? |
|---|---|---|---|
| 歸因原則 | Last Click | First Click / Multi-touch | No |
| 跨裝置歸因 | 不支援 | 登入後帳號級歸因 | No |
| 推薦人儀表板 | 無 | 有 | No |
| 佣金結算 | 無 | 有 | No |
| 夥伴 API 寫入職缺 | 無 | 視合約 | No |
| Webhook | 無 | 應徵事件推送 | No |
referral_click_events | 不做 | 點擊漏斗(v2) | No |
| 清單 Kanban / 排序拖拉 | 手動 sort_order | 拖拉 UI | No |
11. PRD 問題清單
| # | PRD 章節 | 問題 | 決議 | 狀態 |
|---|---|---|---|---|
| 1 | FR-3.1 | 機構管理員帳號體系 | 公司帳號 + pm_41 公司後台自助 Key;奧德士 = is_headhunter_partner 的 Company | ✅ 已決(Eric 2026/07/03) |
| 2 | FR-1.2 | Cookie 同意橫幅 | 要顯示;wport_ref 列功能性必要 Cookie,接受後寫入 | ✅ 已決 |
| 3 | §6.7 | 獨立 referrals 表 | v1 不建;僅 applications 歸因欄位 | ✅ 已決 |
| 4 | FR-3.5 | Signed Token 效期 | 跟隨綁定生命週期;解約即失效(非固定 90 天) | ✅ 已決 |
| 5 | §2.2 | 綁定後人才庫行為 | 應徵仍進發職缺企業 pm_41 人才庫;僅職缺 listing 不在 WPORT 公開露出 | ✅ 已決(Eric 2026/07/03) |
12. 下一步
- 回寫 BR-039 ~ BR-043 至
business-rules.md - 模組 1(P0)後端 + 前端 Cookie 邏輯先行開發
- 與 pm_41 RD 對齊:新增 scope
jobs:partner_bound_read與GET /v1/jobs?filter=partner_bound - 模組 2 清單頁 Storybook mockup(
gen-storybook-mockup-gen) - 與奧德士 BD 對齊官網同步與解約流程
文件結束