24 KiB
Spec: 品牌 × 產品商機雷達
Status:
approved
Status note: 使用者 2026-08-10「批准」
Source:docs/product/brand-product-radar/requirements.md(approved 2026-08-10)
Parent spec:docs/product/demand-radar/spec.md
Last updated:2026-08-10
1. 摘要
本規格把既有 RadarWatch、每日/立即巡邏、Opportunity、回覆與 CRM 串上 Brand/Product。產品型雷達以一個品牌下的一個產品為上下文;同一公開內容仍只建立一筆 Opportunity,但可累積多個產品匹配並選出唯一主推產品。
P0 不另建第二套商機系統:保留既有商機五問與 0–100 分數,新增可獨立解釋的產品適配分數、證據與風險;產品型結果預設按商機分數排序。舊通用雷達與舊商機保持可用,逐步補上產品關聯。
2. 名詞與實體
| 名詞 | 定義 |
|---|---|
| 通用雷達(generic watch) | 舊式 RadarWatch;沒有 Brand/Product 關聯,沿用 ServiceProfile 判定服務匹配 |
| 產品型雷達(product watch) | 綁定一個 Brand 與該 Brand 下的一個 Product 的 RadarWatch |
| ProductContextSnapshot | 判定當下使用的品牌/產品識別與版本快照,只保留解釋歷史所需資訊 |
| ProductMatch | 一筆 Opportunity 對一項 Product 的適配結果;同產品在同一商機中最多一筆 |
| ProductFitReason | 產品適配的一個維度分數、理由與公開貼文證據 |
| PrimaryProduct | 商機目前主推的唯一產品;必須存在於該商機的 ProductMatch 中 |
| 產品適配分數 | product_fit_score,0–100,與既有 intent_score 分開呈現 |
| 可跟進產品匹配 | 適配分數至少 45、有痛點或使用情境的正向證據,且沒有產品硬排除 |
沿用且不重新定義:Brand、Product、ServiceProfile、RadarWatch 狀態、RadarSweep、Opportunity 狀態、Contact、FollowUp、ReplyVariant、Job、Usage、Notification。
3. 核心行為契約
3.1 RadarWatch 上下文
generic --(使用者補選合法 Brand + Product)--> product
product --X--> generic
product(A) --X--> product(B)
context_mode為generic | product。generic時brand_id、product_id皆空;product時兩者皆必填,且 Product 必須屬於該 Brand 與目前會員。genericactive watch 維持既有 ServiceProfile 必填;productactive watch 不要求 ServiceProfile,Brand/Product 才是產品適配的必要上下文。- product watch 有自己的
regions[]時優先使用;留空且 ServiceProfile 存在時沿用其地區/remote,兩者都沒有時視為「未限制地區」,不得把未知地區硬判為不符。 - 新前端流程一律建立
productwatch。既有 API 呼叫未帶兩個識別欄位時仍可建立generic,維持相容性,但 UI 不主動提供此入口。 - 舊
genericwatch 可做一次性補綁;第一次成功巡邏後,產品關聯不可原地更換。要改產品時封存或暫停舊 watch,再以相同搜尋詞建立新 watch,避免歷史統計混義。 - watch 保存品牌名稱、產品名稱與綁定時間快照。列表優先顯示目前名稱;來源已刪除時顯示快照。
3.2 產品適配評分
產品型判定除既有五問外,必須輸出以下四個維度;四維滿分合計 100:
| 維度 | 滿分 | 判定來源 |
|---|---|---|
pain |
35 | 貼文問題是否對應 Product pain_points |
scenario |
25 | 貼文使用/購買情境是否對應 product_context 或 match_tags |
audience |
20 | 公開文字是否支持 Brand target_audience;不確定給 0,不得猜測 |
capability |
20 | Product provider_capability_terms 是否確實能處理該需求 |
每個維度都產生一筆 ProductFitReason:
dimension:上述四值之一。score:0 到該維度滿分。reason:人話說明。candidate_excerpt:正分時必填,必須是候選公開文字中的短片段;0 分可留空。product_basis:對應的品牌受眾、產品痛點、情境、標籤或能力詞;不得生成來源資料中不存在的產品承諾。
另輸出 risks[]。若候選文字明確命中 Product provider_exclude_terms,或判定為同業叫賣/品牌公告/雜訊,該 ProductMatch 設 excluded=true 並附 exclude_reason。
product_fit_score = pain + scenario + audience + capability
product_fit_band = strong if score >= 75
possible if score 45..74
weak if score < 45
eligible = score >= 45
AND (pain > 0 OR scenario > 0)
AND excluded = false
- 單純命中搜尋詞或產品名,不算
pain/scenario證據。 - 產品型 Opportunity 的既有五問
fit維度,取主推 ProductMatch 分數等比例換算為 0–10;其他四問及 80/50 分級門檻不變。 intent_score仍是今日排序的主要分數;product_fit_score獨立顯示,作為同分排序與解釋依據。
3.3 同貼文多產品合併與主推產品
- Opportunity 的唯一語意仍是「同一會員+同一來源內容識別」;不因 watch 或 product 不同而複製商機。
- ProductMatch 的唯一語意是「同一 Opportunity+同一 Product」。同產品再次命中只合併
watch_ids[]與matched_terms[],不新增重複項目。 - 首個 eligible ProductMatch 成為 PrimaryProduct;後續出現更高適配分數時,自動改成較高者。
- 分數相同時依
strong優先、首次匹配時間較早、最後以product_id穩定排序。 - 使用者手動選定主推產品後寫入 override;之後新增 ProductMatch 不再自動改主推,直到使用者再次選擇。
- PrimaryProduct 改變時,以該 ProductMatch 重新換算既有
fit維度與intent_score,其他四維理由不重跑;變更須留操作者與時間。 - ProductMatch 使用判定當下的品牌/產品版本快照。產品日後修改不回寫舊 match;同貼文重複命中也只合併來源,不重算舊快照。只有先前尚無該 ProductMatch 時才執行一次適配判定。
3.4 時效與顯示
- 既有時效分數不變:24 小時內最高、24–72 小時遞減、72 小時至 14 天低分。
- 超過 14 天仍以
stale原因保留為rejected,但產品型判定仍保存 ProductMatch 與證據;不得只因過舊就完全不入庫。 - 「今日商機」只顯示未逾 14 天、既有狀態允許顯示,且至少有一個 eligible ProductMatch 的產品型 Opportunity。
- 「全部結果」可看 stale、weak、excluded 與通用商機;預設仍依
intent_score高到低,其次主推產品適配分數高到低、posted_at新到舊。 - rejected、weak、excluded 預設不混入今日高/中/低統計,但篩選後可檢查其理由。
3.5 Watch 與來源生命週期
沿用 active ↔ paused → archived,新增 pause_reason:
active --> paused(product_unavailable) : 綁定 Brand/Product 不存在或不可用
paused(product_unavailable) --X--> active : 不允許直接恢復
paused(product_unavailable) --> archived : 保留歷史
- 使用者手動暫停使用
pause_reason=user。 - 刪除 Product 時,同一會員下所有綁定該 Product 的 active watch 先轉 paused,歷史保留;禁止自動改綁。
- Brand 仍沿用「有產品不可刪」;若因資料異常找不到 Brand,相關 watch 依同樣規則暫停。
product_unavailablewatch 可複製搜尋設定建立新產品 watch,不能原地換產品後恢復。- 排程發現無效產品上下文時,必須 guarded 暫停 watch、記錄原因並通知,不建立成功空 Sweep。
4. 使用者流程
4.1 建立產品型每日雷達
- 使用者進入雷達訂閱頁,先選 Brand;產品選單只列該 Brand 下、目前會員可用的 Product。
- 選 Product 後顯示上下文完整度:品牌受眾、產品情境、痛點至少一項、能力詞。缺品牌或產品本體直接阻擋;內容不足可繼續,但提示結果信心會降低。
- 使用者要求建議詞。系統只讀所選 Brand/Product,回傳 include/exclude 建議、理由與依據種類。
- 使用者增刪建議,沿用 Threads 短詞規則驗證後啟用。
- 建立成功後沿用既有首巡與每日 UTC 22:00 排程;卡片顯示 Brand、Product、狀態與最近巡邏時間。
4.2 每日巡邏
- worker 取得 active watch 後,重新讀取同 owner 的 Brand/Product,驗證歸屬。
- 找不到或關聯錯誤時按 §3.5 暫停並通知。
- 讀取最新 Brand/Product 組成 ProductContextSnapshot,再以 watch 搜尋詞 fan-out 取得候選。
- 對全新公開內容執行既有商機五問與產品適配判定;對已存在 Opportunity、但尚無此 ProductMatch 的內容,只新增該產品適配判定,不重建商機。
- 對已存在且已有同 ProductMatch 的內容,只合併 watch/term;不重複計 AI 判定用量。
- 新 ProductMatch 不占用「每日新商機筆數」;新 Opportunity 才占用。AI 適配判定仍受既有點數 gate。
- Sweep 另外記錄適配評估數、合併數與因產品適配不足未進今日頁的數量。
4.3 立即探索
- 使用者先選 Brand/Product,再輸入或採用 1–6 組短詞。
- 立即探索使用與每日 watch 相同的上下文、去重、評分、配額與 ProductMatch 合併規則。
- 沒選 Brand/Product 時,舊 API 相容路徑可繼續做 generic explore;新 UI 不提供 generic 預設。
- 結果回報搜尋命中、判定、新增商機、合併產品匹配、截斷與用量,不把「合併」誤算成新商機。
4.4 查看與篩選商機
- 今日頁預設跨所有產品、去重後按 §3.4 排序;提供 Brand、Product、適配程度與商機分級篩選。
- 選定 Product 篩選時,以「是否存在該 ProductMatch」決定是否入列,卡片優先展示被篩選產品的證據與適配分數,但不暗中更改儲存的 PrimaryProduct。
- 卡片顯示主推 Brand/Product、商機分數、產品適配分數、需求摘要、至少一個證據、風險、其他匹配產品數與原文。
- 展開「為什麼適合」顯示四維理由;展開其他產品可比較分數與證據。
- 使用者可把任一 eligible ProductMatch 設為主推產品;weak/excluded 不可設為主推,除非先以既有人工覆寫機制確認並留下理由。
- 「全部結果」提供 stale/weak/excluded/未指定產品篩選,結果多但不混淆可立即聯絡的今日商機。
4.5 回覆、CRM 與成交
- 生成回覆時使用 Opportunity 的 PrimaryProduct 快照、Brand 語境及 ServiceProfile 的營運限制。
- 沒有 PrimaryProduct 的舊商機沿用既有 ServiceProfile 回覆;畫面明確標示「未指定產品」。
- 加入名單後,ContactTouch 保存該次主推
brand_id、product_id與 opportunity 關聯;同一聯絡人可在不同時間對應不同產品。 - 主推產品後續改變,不改寫已送出回覆或既有 ContactTouch;新的回覆與接觸使用新主推產品。
- 成交回報保存成交當下主推產品;既有 OutcomeEvent 行為不變,額外攜帶產品歸屬供後續 P1 統計。
4.6 舊資料過渡
- 缺
context_mode的既有 watch 讀作generic;缺 ProductMatch 的既有 Opportunity 讀作「未指定產品」。不需一次性猜測回填。 - watch 列表提供「補上產品」;只允許為 generic watch 執行一次。
- 舊商機可繼續接受、回覆、進 CRM 與成交;不因未指定產品而變成不可用。
matched_service保留給 generic 與舊資料;產品型商機不再以它代表產品,欄位暫不移除。
5. 介面契約
5.1 共通規則與 Auth matrix
- 所有下列
/api/v1/radar/**與既有 Brand/Product 操作只接受會員Authorization: Bearer <member JWT>。 - AI provider token/BYOK key 只能由既有 provider 設定與內部 AI 呼叫使用,不得當會員 JWT,也不得回傳至前端。
- 成功 envelope:
code=102000、message、data、error;失敗不得回 102000 空資料。 - 列表:
page/pageSize,回pagination+list。 - 所有時間為 UTC unix nanoseconds
int64。
5.2 HTTP / API
既有路徑原地擴充;欄位均以 generate/api/*.api 為契約真相,再由 goctl 產生。表內只列本 run 有變化的介面。
| Method | Path | 用途 | 本 run 主要 request/response 變更 |
|---|---|---|---|
| GET | /api/v1/radar/watches |
列 watch | filter 加 context_mode、brand_id、product_id;item 加上下文、名稱快照、pause_reason |
| POST | /api/v1/radar/watches |
建 watch | req 可帶成對 brand_id+product_id;response 加 context_mode 與上下文 |
| PUT | /api/v1/radar/watches/:id |
改搜尋設定 | 只改 terms/exclude/regions;不得更換產品 |
| POST | /api/v1/radar/watches/:id/assign-product |
舊 watch 一次性補綁 | req brand_id、product_id;只允許 generic 且尚未補綁 |
| POST | /api/v1/radar/watches/suggest |
產品需求詞建議 | req 加 brand_id、product_id;item 加 basis_kind、basis_text |
| POST | /api/v1/radar/watches/:id/sweep |
手動巡 | 行為加產品上下文驗證;無效時明確失敗/暫停 |
| GET | /api/v1/radar/today |
今日商機 | query 可帶 brand_id、product_id、fit_band;Opportunity item 加產品欄位 |
| GET | /api/v1/radar/opportunities |
全部結果 | filter 加 brand_id、product_id、fit_band、match_state、sort |
| GET | /api/v1/radar/opportunities/:id |
商機詳情 | 回完整 product_matches[]、PrimaryProduct 與 override |
| PUT | /api/v1/radar/opportunities/:id/primary-product |
改主推產品 | req product_id、reason;回更新後 Opportunity |
| POST | /api/v1/radar/explore |
立即探索 | req 加成對 brand_id+product_id;response 加 matched_count、merged_count |
| POST | /api/v1/radar/import |
手動匯入 | req 可帶成對 Brand/Product,使用同一適配判定 |
| GET | /api/v1/radar/sweeps |
巡邏紀錄 | item 加 match_evaluated_count、match_merged_count、fit_rejected_count |
| DELETE | /api/v1/scout/products/:id |
刪 Product | 刪除前同步暫停相關 active watch;任一步失敗不得假裝刪除成功 |
主要輸出形狀:
RadarWatchPublic += {
context_mode, brand_id?, product_id?,
brand_name_snapshot?, product_label_snapshot?, context_bound_at?, pause_reason?
}
OpportunityPublic += {
primary_brand_id?, primary_product_id?,
primary_brand_name?, primary_product_label?,
primary_product_fit_score?, primary_product_fit_band?,
primary_product_overridden?, product_matches[]
}
ProductMatchPublic = {
brand_id, product_id, brand_name_snapshot, product_label_snapshot,
brand_updated_at, product_updated_at,
product_fit_score, product_fit_band, eligible, excluded,
exclude_reason?, reasons[4], risks[], watch_ids[], matched_terms[], matched_at
}
5.3 錯誤語意
| 情境 | 行為 |
|---|---|
| 只帶 Brand 或只帶 Product | validation error;不得降級成 generic |
| Brand/Product 不屬於會員或關係不符 | 對外使用 not found/forbidden 的既有安全語意,不洩漏他人資料 |
| 對 product watch 更換 Brand/Product | validation error;提示建立新 watch |
| 重複替 generic watch 補綁 | conflict;回目前綁定,不覆寫 |
| product_unavailable watch 恢復 | conflict;提示封存/複製到新產品 |
| 主推產品不在 ProductMatch | validation error |
| 主推 weak/excluded match 且無覆寫理由 | validation error |
| ProductMatch 判定缺四維理由 | 判定失敗,不保存不完整 match;Sweep 記失敗數 |
| 產品來源失效 | watch 暫停+通知;不得成功空巡 |
| 全為 stale/weak/excluded | today 正常回 0,empty_reason=no_eligible_product_match;全部結果仍可查 |
5.4 前端頁面與互動
| 路由 | 行為變更 |
|---|---|
/app/radar/watches |
Brand → Product 串接選擇、資料完整度、產品詞建議、generic 補綁、產品失效提示 |
/app/radar/today |
Brand/Product/適配篩選、產品證據卡、多匹配展開、改主推產品 |
/app/radar/opportunities |
新增全部結果視圖,包含 stale/weak/excluded/未指定產品與分數排序 |
/app/brands |
Product 刪除前顯示會暫停的 radar 數量與後果;不重做產品編輯器 |
/app/crm |
聯絡人與接觸時間軸顯示來源 Brand/Product;既有階段操作不變 |
- mock 與 live 必須實作同一 Radar repository 介面與相同錯誤/空狀態。
- 沿用 Harbor Desk、既有 UI 元件與
radar.css;不新增 UI framework,不用 emoji 作主 icon。 - Brand/Product 名稱只是導覽資訊;證據與風險才是商機卡的主要解釋。
5.5 Job / 背景任務
| Job | Retain/變更 | 成功結果 |
|---|---|---|
radar_sweep |
保留每日 UTC 22:00、手動首巡、guarded 更新與 Redis workerID lock;加入產品上下文與 ProductMatch 合併 |
新 Opportunity 或合併 ProductMatch;Sweep 統計完整 |
radar_followup_scan |
Retain | 行為不變;通知與 Contact 歷史可帶產品歸屬 |
scout_scan |
Retain | 不改 Scout 掃描行為;只復用 Brand/Product 真相 |
6. 資料保存與不變量
| 實體 | 新增/保留欄位 | 生命週期與不變量 |
|---|---|---|
| RadarWatch | context mode、Brand/Product IDs、名稱快照、綁定時間、pause reason | product 關聯不可原地更換;封存保留 |
| Opportunity | PrimaryProduct、override、product_matches[];保留 matched_service |
同 owner+external identity 唯一;舊資料可無產品 |
| ProductMatch | 來源版本快照、四維理由、分數、band、eligible、exclude、watch/term 來源 | 同 Opportunity+Product 唯一;判定後不可因產品編輯回寫 |
| ContactTouch | brand_id、product_id、opportunity_id | 寫入後不隨主推產品改變 |
| OutcomeEvent | product_id(可空,向後相容) | 以成交當下主推產品保存 |
| RadarSweep | match evaluated/merged/fit rejected counts | 與既有 Sweep 一起保存、可追查 |
必須成立:
- ProductMatch 的 Brand 必須與 Product 所屬 Brand 相同,且兩者 owner 與 Opportunity owner 相同。
- PrimaryProduct 必須指向 eligible ProductMatch;若人工覆寫 weak/excluded,則必須同時存在理由與稽核紀錄。
- 同一產品 match 的
watch_ids、matched_terms去重;同一 Opportunity 的 ProductMatch 不重複。 - owner+external identity 的既有唯一性不變;多 worker 競態時結果仍是一筆 Opportunity。
- API 列表預設排序穩定:
intent_score desc、primary fit desc、posted_at desc、id asc。
7. 與現況對照(Retain / Replace / Remove)
| 能力 | Retain | Replace | Remove | 備註 |
|---|---|---|---|---|
| 每日排程、首巡、立即探索 | ✓ | 同一 pipeline 加產品上下文 | ||
| RadarWatch 狀態與配額 | ✓ | 新增 context 與 pause reason | ||
| Threads 短詞 fan-out | ✓ | 建議詞改由選定產品產生 | ||
| ServiceProfile 地區/remote/forbidden | ✓ | product watch 可選用;generic watch 仍必填 | ||
| active watch 一律要求 ServiceProfile | ✓ | 只對 generic 保留;product 改以 Brand/Product 為門檻 | ||
| ServiceProfile 作為產品 fit 來源 | ✓ | product watch 改用 Brand/Product;generic 保留舊行為 | ||
matched_service |
✓ | 舊/generic 相容,產品型不再用它冒充 Product | ||
| 重複貼文只合併 matched terms | ✓ | 改為同產品合併來源、不同產品新增 ProductMatch | ||
| Opportunity owner+external identity 去重 | ✓ | 核心去重不變 | ||
| 五問分數與高/中/低門檻 | ✓ | fit 10 分由 PrimaryProduct 換算 | ||
| >14 天不進今日頁 | ✓ | 仍保存產品適配供全部結果查看 | ||
| 今日頁只看即時商機 | ✓ | 加 eligible product gate | ||
| 全部結果頁 | ✓ | 從只有 API 查詢補成可操作前端視圖 | ||
| 回覆、CRM、FollowUp、成交 | ✓ | 附加產品歸屬,不重造狀態機 | ||
| Brand/Product CRUD | ✓ | Product 刪除增加 radar 暫停副作用 | ||
| 既有欄位/舊資料 | ✓ | P0 無硬刪欄位、無猜測回填 |
本 run 不移除任何既有對外路徑或持久欄位。
8. 非功能契約
- 權限: 每次 watch、opportunity、product match、primary product 變更都以 JWT owner 驗證;不可只信 request 中的 brand_id/product_id。
- 一致性: Product 刪除與 watch 暫停必須具備可重試的一致結果;部分失敗不得回刪除成功。
- 併發: 同一貼文被多 watch/worker 同時命中時,需以 guarded/atomic 語意保證單 Opportunity、單 ProductMatch。
- 效能: 列表解析 Brand/Product 名稱不得逐筆查詢;
pageSize沿用既有限制,產品篩選與排序在合理索引下完成。 - 成本: 同一
(opportunity, product)不重複計適配判定;新增 ProductMatch 可計一次既有ai_research,source=radar.product_fit,不新增 meter。Product 更新不觸發舊商機重算。 - 可觀測性: Sweep 可區分搜尋命中、商機判定、新商機、適配評估、合併、適配不足、截斷與失敗。
- 隱私: prompt 與 UI 只使用公開貼文、會員自己的 Brand/Product/ServiceProfile;禁止輸出推測的敏感屬性。
- 降級: AI 適配不可用時可用明確的規則結果,但仍須四維理由與證據;無法產出完整契約時該 match 失敗,不以空理由成功。
9. 驗收場景(Given / When / Then)
- Given 會員有 Brand A 下的 Product A1 與 A2,When 建立 A1 watch,Then watch 回傳 product context,且不能綁 A2 所屬以外的 Brand。
- Given Product 有痛點但沒有品牌名出現在貼文,When 貼文明確描述該痛點,Then 可形成含原文 excerpt 的 ProductMatch,不要求命中品牌名。
- Given 貼文只提到產品同名詞但無需求/痛點,When 判定,Then 不得只憑詞命中成為 eligible match。
- Given 同一貼文先被 A1 watch 命中、後被 A2 watch 命中,When 兩次巡邏完成,Then 只有一筆 Opportunity、兩筆 ProductMatch,且不重複 Contact。
- Given A2 分數高於 A1 且沒有人工 override,When A2 match 合併,Then A2 成為 PrimaryProduct,intent fit 維度按 A2 換算。
- Given 使用者手動指定 A1,When 後續加入更高分 A2 match,Then PrimaryProduct 仍是 A1,並保留 override 紀錄。
- Given Product 更新名稱與痛點,When 查看舊商機,Then 顯示判定時快照與舊證據;下一次新商機才使用新版資料。
- Given 一筆 20 天前但產品高度適配的內容,When 巡邏完成,Then 今日頁不顯示、全部結果可見 stale 與 ProductMatch 理由。
- Given Product 被刪除,When 刪除完成,Then 相關 active watch 已暫停並通知,歷史 Opportunity/Contact/OutcomeEvent 仍可看。
- Given 舊 generic watch 與舊 Opportunity,When 升級部署,Then 原巡邏、回覆與 CRM 可繼續,且 UI 顯示未指定產品而非猜測關聯。
- Given 今日沒有 eligible match 但有 weak/stale 結果,When 開今日頁,Then 顯示
no_eligible_product_match與前往全部結果的動作,不顯示模糊空白頁。 - Given 兩個 worker 同時處理同一貼文與同一 Product,When 都完成,Then 只存在一筆 Opportunity 與一筆該 ProductMatch,watch/term 集合無重複。
- Given 另一會員的 Brand/Product ID,When 嘗試建立 watch 或改主推產品,Then 請求失敗且不洩漏對方名稱與內容。
- Given 產品型商機生成回覆,When 檢查 prompt 與輸出,Then 使用 PrimaryProduct 快照與正確 Brand,且不引用其他品牌資料。
10. 開放規格問題
P0 無阻擋問題。以下保留到 P1/P2,不影響本規格批准:
- 產品回饋訊號要採明確正負樣本,或由 CRM 行為推導權重。
- 產品自己的理想使用者欄位何時取代/覆蓋 Brand
target_audience。 - 跨來源同一人的商機是否允許人工合併;P0 仍不做身分推測。
11. 批准
- Spec 已批准(2026-08-10/使用者「批准」)