# 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 上下文 ```text generic --(使用者補選合法 Brand + Product)--> product product --X--> generic product(A) --X--> product(B) ``` - `context_mode` 為 `generic | product`。 - `generic` 時 `brand_id`、`product_id` 皆空;`product` 時兩者皆必填,且 Product 必須屬於該 Brand 與目前會員。 - `generic` active watch 維持既有 ServiceProfile 必填;`product` active watch 不要求 ServiceProfile,Brand/Product 才是產品適配的必要上下文。 - product watch 有自己的 `regions[]` 時優先使用;留空且 ServiceProfile 存在時沿用其地區/remote,兩者都沒有時視為「未限制地區」,不得把未知地區硬判為不符。 - 新前端流程一律建立 `product` watch。既有 API 呼叫未帶兩個識別欄位時仍可建立 `generic`,維持相容性,但 UI 不主動提供此入口。 - 舊 `generic` watch 可做一次性補綁;第一次成功巡邏後,產品關聯不可原地更換。要改產品時封存或暫停舊 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`。 ```text 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`: ```text 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_unavailable` watch 可複製搜尋設定建立新產品 watch,不能原地換產品後恢復。 - 排程發現無效產品上下文時,必須 guarded 暫停 watch、記錄原因並通知,不建立成功空 Sweep。 ## 4. 使用者流程 ### 4.1 建立產品型每日雷達 1. 使用者進入雷達訂閱頁,先選 Brand;產品選單只列該 Brand 下、目前會員可用的 Product。 2. 選 Product 後顯示上下文完整度:品牌受眾、產品情境、痛點至少一項、能力詞。缺品牌或產品本體直接阻擋;內容不足可繼續,但提示結果信心會降低。 3. 使用者要求建議詞。系統只讀所選 Brand/Product,回傳 include/exclude 建議、理由與依據種類。 4. 使用者增刪建議,沿用 Threads 短詞規則驗證後啟用。 5. 建立成功後沿用既有首巡與每日 UTC 22:00 排程;卡片顯示 Brand、Product、狀態與最近巡邏時間。 ### 4.2 每日巡邏 1. worker 取得 active watch 後,重新讀取同 owner 的 Brand/Product,驗證歸屬。 2. 找不到或關聯錯誤時按 §3.5 暫停並通知。 3. 讀取最新 Brand/Product 組成 ProductContextSnapshot,再以 watch 搜尋詞 fan-out 取得候選。 4. 對全新公開內容執行既有商機五問與產品適配判定;對已存在 Opportunity、但尚無此 ProductMatch 的內容,只新增該產品適配判定,不重建商機。 5. 對已存在且已有同 ProductMatch 的內容,只合併 watch/term;不重複計 AI 判定用量。 6. 新 ProductMatch 不占用「每日新商機筆數」;新 Opportunity 才占用。AI 適配判定仍受既有點數 gate。 7. Sweep 另外記錄適配評估數、合併數與因產品適配不足未進今日頁的數量。 ### 4.3 立即探索 1. 使用者先選 Brand/Product,再輸入或採用 1–6 組短詞。 2. 立即探索使用與每日 watch 相同的上下文、去重、評分、配額與 ProductMatch 合併規則。 3. 沒選 Brand/Product 時,舊 API 相容路徑可繼續做 generic explore;新 UI 不提供 generic 預設。 4. 結果回報搜尋命中、判定、新增商機、合併產品匹配、截斷與用量,不把「合併」誤算成新商機。 ### 4.4 查看與篩選商機 1. 今日頁預設跨所有產品、去重後按 §3.4 排序;提供 Brand、Product、適配程度與商機分級篩選。 2. 選定 Product 篩選時,以「是否存在該 ProductMatch」決定是否入列,卡片優先展示被篩選產品的證據與適配分數,但不暗中更改儲存的 PrimaryProduct。 3. 卡片顯示主推 Brand/Product、商機分數、產品適配分數、需求摘要、至少一個證據、風險、其他匹配產品數與原文。 4. 展開「為什麼適合」顯示四維理由;展開其他產品可比較分數與證據。 5. 使用者可把任一 eligible ProductMatch 設為主推產品;weak/excluded 不可設為主推,除非先以既有人工覆寫機制確認並留下理由。 6. 「全部結果」提供 stale/weak/excluded/未指定產品篩選,結果多但不混淆可立即聯絡的今日商機。 ### 4.5 回覆、CRM 與成交 1. 生成回覆時使用 Opportunity 的 PrimaryProduct 快照、Brand 語境及 ServiceProfile 的營運限制。 2. 沒有 PrimaryProduct 的舊商機沿用既有 ServiceProfile 回覆;畫面明確標示「未指定產品」。 3. 加入名單後,ContactTouch 保存該次主推 `brand_id`、`product_id` 與 opportunity 關聯;同一聯絡人可在不同時間對應不同產品。 4. 主推產品後續改變,不改寫已送出回覆或既有 ContactTouch;新的回覆與接觸使用新主推產品。 5. 成交回報保存成交當下主推產品;既有 OutcomeEvent 行為不變,額外攜帶產品歸屬供後續 P1 統計。 ### 4.6 舊資料過渡 1. 缺 `context_mode` 的既有 watch 讀作 `generic`;缺 ProductMatch 的既有 Opportunity 讀作「未指定產品」。不需一次性猜測回填。 2. watch 列表提供「補上產品」;只允許為 generic watch 執行一次。 3. 舊商機可繼續接受、回覆、進 CRM 與成交;不因未指定產品而變成不可用。 4. `matched_service` 保留給 generic 與舊資料;產品型商機不再以它代表產品,欄位暫不移除。 ## 5. 介面契約 ### 5.1 共通規則與 Auth matrix - 所有下列 `/api/v1/radar/**` 與既有 Brand/Product 操作只接受會員 `Authorization: Bearer `。 - 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;任一步失敗不得假裝刪除成功 | 主要輸出形狀: ```text 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 一起保存、可追查 | 必須成立: 1. ProductMatch 的 Brand 必須與 Product 所屬 Brand 相同,且兩者 owner 與 Opportunity owner 相同。 2. PrimaryProduct 必須指向 eligible ProductMatch;若人工覆寫 weak/excluded,則必須同時存在理由與稽核紀錄。 3. 同一產品 match 的 `watch_ids`、`matched_terms` 去重;同一 Opportunity 的 ProductMatch 不重複。 4. owner+external identity 的既有唯一性不變;多 worker 競態時結果仍是一筆 Opportunity。 5. 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) 1. **Given** 會員有 Brand A 下的 Product A1 與 A2,**When** 建立 A1 watch,**Then** watch 回傳 product context,且不能綁 A2 所屬以外的 Brand。 2. **Given** Product 有痛點但沒有品牌名出現在貼文,**When** 貼文明確描述該痛點,**Then** 可形成含原文 excerpt 的 ProductMatch,不要求命中品牌名。 3. **Given** 貼文只提到產品同名詞但無需求/痛點,**When** 判定,**Then** 不得只憑詞命中成為 eligible match。 4. **Given** 同一貼文先被 A1 watch 命中、後被 A2 watch 命中,**When** 兩次巡邏完成,**Then** 只有一筆 Opportunity、兩筆 ProductMatch,且不重複 Contact。 5. **Given** A2 分數高於 A1 且沒有人工 override,**When** A2 match 合併,**Then** A2 成為 PrimaryProduct,intent fit 維度按 A2 換算。 6. **Given** 使用者手動指定 A1,**When** 後續加入更高分 A2 match,**Then** PrimaryProduct 仍是 A1,並保留 override 紀錄。 7. **Given** Product 更新名稱與痛點,**When** 查看舊商機,**Then** 顯示判定時快照與舊證據;下一次新商機才使用新版資料。 8. **Given** 一筆 20 天前但產品高度適配的內容,**When** 巡邏完成,**Then** 今日頁不顯示、全部結果可見 stale 與 ProductMatch 理由。 9. **Given** Product 被刪除,**When** 刪除完成,**Then** 相關 active watch 已暫停並通知,歷史 Opportunity/Contact/OutcomeEvent 仍可看。 10. **Given** 舊 generic watch 與舊 Opportunity,**When** 升級部署,**Then** 原巡邏、回覆與 CRM 可繼續,且 UI 顯示未指定產品而非猜測關聯。 11. **Given** 今日沒有 eligible match 但有 weak/stale 結果,**When** 開今日頁,**Then** 顯示 `no_eligible_product_match` 與前往全部結果的動作,不顯示模糊空白頁。 12. **Given** 兩個 worker 同時處理同一貼文與同一 Product,**When** 都完成,**Then** 只存在一筆 Opportunity 與一筆該 ProductMatch,watch/term 集合無重複。 13. **Given** 另一會員的 Brand/Product ID,**When** 嘗試建立 watch 或改主推產品,**Then** 請求失敗且不洩漏對方名稱與內容。 14. **Given** 產品型商機生成回覆,**When** 檢查 prompt 與輸出,**Then** 使用 PrimaryProduct 快照與正確 Brand,且不引用其他品牌資料。 ## 10. 開放規格問題 P0 無阻擋問題。以下保留到 P1/P2,不影響本規格批准: 1. 產品回饋訊號要採明確正負樣本,或由 CRM 行為推導權重。 2. 產品自己的理想使用者欄位何時取代/覆蓋 Brand `target_audience`。 3. 跨來源同一人的商機是否允許人工合併;P0 仍不做身分推測。 ## 11. 批准 - [x] Spec 已批准(2026-08-10/使用者「批准」)