# 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 <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；任一步失敗不得假裝刪除成功 |

主要輸出形狀：

```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／使用者「批准」）
