92 lines
4.4 KiB
Markdown
92 lines
4.4 KiB
Markdown
# T501 — mongo-indexes-radar-crm
|
||
|
||
> Status: `done`(2026-07-31)
|
||
> Milestone: `M0`
|
||
> Kind: `feat`
|
||
> Est. change: `~120 lines`
|
||
|
||
## Goal
|
||
|
||
完成後系統應:`make init` 可重複執行並建立本 run 全部 collection 索引,含去重唯一鍵。
|
||
|
||
## Depends on
|
||
|
||
- —
|
||
|
||
## Inputs
|
||
|
||
- Spec:`../spec.md` §8(實體與索引)
|
||
- 既有索引初始化:`apps/backend/cmd/init/main.go`
|
||
|
||
## Outputs
|
||
|
||
### 程式變更(預期路徑)
|
||
|
||
| 路徑 | 動作 | 說明 |
|
||
|------|------|------|
|
||
| `apps/backend/generate/database/mongo/000014_demand_radar_indexes.up.json` | add | 全部 15 個索引,含兩個 unique |
|
||
| `apps/backend/generate/database/mongo/000014_demand_radar_indexes.down.json` | add | 對應 dropIndexes |
|
||
| `apps/backend/cmd/init/main.go` | edit | 七個 collection 的**非 unique** 索引(名稱與 spec 與 migration 完全一致) |
|
||
|
||
### 行為變更
|
||
|
||
- 建立索引:
|
||
- `radar_service_profiles`:`_id = owner_uid`(不需額外索引)
|
||
- `radar_watches`:`(owner_uid, status)`、`(owner_uid, created_at)`
|
||
- `radar_sweeps`:`(owner_uid, created_at)`、`(watch_id, created_at)`、`job_id`
|
||
- `radar_opportunities`:`(owner_uid, created_at)`、`(owner_uid, intent_band, status)`、`(owner_uid, external_id)` **unique**
|
||
- `radar_replies`:`(owner_uid, opportunity_id, variant)`
|
||
- `crm_contacts`:`(owner_uid, source_platform, author_handle)` **unique**、`(owner_uid, stage, last_touch_at)`、`(owner_uid, needs_follow_up)`
|
||
- `crm_touches`:`(owner_uid, contact_id, created_at)`
|
||
- `crm_followups`:`(status, due_at)`、`(owner_uid, contact_id)`
|
||
- 重複執行不報錯(沿用既有 idempotent 建法)。
|
||
|
||
## Out of scope
|
||
|
||
- domain struct 與 repository(各 M 的 model task)
|
||
- 資料遷移(本 run 無既有資料需搬)
|
||
|
||
## Acceptance
|
||
|
||
- [x] `make init` 連跑兩次皆成功(乾淨 DB 驗證,兩次都 `database indexes initialized`)
|
||
- [x] Mongo `getIndexes()` 可見上列索引:migrate 路徑 15 個且 unique 恰為 `radar_opportunities.owner_opportunity_external`、`crm_contacts.owner_contact_identity`;init 路徑 13 個非 unique
|
||
- [x] 唯一鍵語意實測:同 owner 同 `external_id` 第二筆被擋(11000)、不同 owner 可共存;同 owner 同平台同 handle 第二筆被擋、跨平台可共存
|
||
- [x] `migrate-down 1` → 再 `up` 可逆
|
||
- [x] 指令(radar/crm 部分;完整鏈路受既有缺陷阻塞,見下):
|
||
|
||
```bash
|
||
cd apps/backend && make migrate-up MONGO_URL=<scratch> && MONGO_DATABASE=<scratch> make init
|
||
```
|
||
|
||
## Notes
|
||
|
||
`(owner_uid, external_id)` 是 SW-06 去重的硬保證,不能只靠應用層判斷。
|
||
|
||
### 落點分工(沿用 growth-loop 先例)
|
||
|
||
唯一鍵放 migration、operational 索引放 `cmd/init`。**radar/crm 兩邊的索引名稱與 spec 完全一致**,因此同一顆 DB 先 migrate 再 init 不會衝突(已實測)。
|
||
|
||
Mongo 的衝突規則(實測 Mongo 7):
|
||
|
||
| 情境 | 結果 |
|
||
|------|------|
|
||
| 同名同鍵、`unique` 不同 | `IndexKeySpecsConflict`(86) |
|
||
| 同名同鍵同 spec 重建 | OK(idempotent) |
|
||
| **異名同鍵同 spec** | `IndexOptionsConflict`(85)"already exists with a different name" |
|
||
| 異名同鍵但 spec 不同 | 允許(會多一個索引) |
|
||
|
||
### 既有缺陷(**非本 task 造成,未修**)
|
||
|
||
`make migrate-up && make init` 在**新 DB** 上無法連跑:多個既有 collection 的 migration 與 init 用同鍵不同名,init 會在第一個衝突處 panic。已確認的衝突點:
|
||
|
||
| Collection | 鍵 | migration 名 | init 名 |
|
||
|-----------|-----|-------------|---------|
|
||
| `notifications` | `(owner_uid, created_at)` | `owner_notifications_recent` | `owner_notifications_created` |
|
||
| `growth_outcomes` | `(owner_uid, source_type, source_id)` | `owner_outcome_source`(unique) | 同名但非 unique |
|
||
| `growth_checkups` | `(owner_uid, created_at)` | `owner_checkups_recent` | `owner_checkups_created` |
|
||
| `growth_workspaces` | `(owner_uid, archived)` | `owner_workspaces` | `owner_workspaces_archived` |
|
||
| `growth_invite_rewards` | `(inviter_uid, created_at)` | `inviter_rewards` | `inviter_rewards_created` |
|
||
| `growth_invite_rewards` | `(invitee_uid)` | `invitee_reward_once` | `invitee_reward` |
|
||
|
||
未修的原因:要決定「以哪邊的名字為準」會影響**既有部署**——若某環境的索引是 init 建的,把 init 改成 migration 的名字會讓該環境從此報 85。這是需要產品/維運決定的收斂方向,不該塞在本 task 裡。建議另開 task 處理(含各環境現況盤點)。
|