177 lines
5.4 KiB
Markdown
177 lines
5.4 KiB
Markdown
# 全 Phase 新增功能說明
|
||
|
||
## Phase 1 — TA 精準度
|
||
|
||
### 1. Semantic Embedding Reranker
|
||
|
||
**ScanPost entity 新增欄位**(`internal/model/scan_post/domain/entity/post.go`):
|
||
|
||
| 欄位 | 型別 | 用途 |
|
||
|------|------|------|
|
||
| `Embedding` | `[]float32` | OpenAI text-embedding-3-small 向量 |
|
||
| `SemanticScore` | `int` | 語意相似度分數(0–100) |
|
||
| `EngagementPredicted` | `int` | 預測互動率 |
|
||
| `AudienceQualityScore` | `int` | 受眾品質分數(0–100) |
|
||
| `AuthorID` | `string` | 作者 ID(Threads user ID) |
|
||
| `AuthorAvatar` | `string` | 作者頭像 URL |
|
||
| `AuthorFollowers` | `int` | 作者粉絲數 |
|
||
|
||
`ScanCandidate` struct 同步新增對應欄位(`internal/library/placement/dual_track.go`)。
|
||
|
||
### 2. Embedding Client
|
||
|
||
```go
|
||
// internal/library/embedding/client.go
|
||
func NewClient(apiKey, model string) *Client
|
||
func (c *Client) Generate(ctx context.Context, text string) ([]float32, error)
|
||
```
|
||
|
||
- 預設使用 OpenAI `text-embedding-3-small`(可降級 local BGE-M3)
|
||
- Scorer: `CosineSimilarity(a, b []float32) float64`
|
||
|
||
### 3. Placement Score V2
|
||
|
||
```go
|
||
// internal/library/placement/dual_track.go
|
||
func computePlacementScore(post *ScanCandidate, member productContext, brand productMeta,
|
||
semanticScore, predictedEngagement, audienceQuality *int) int
|
||
```
|
||
|
||
整合三項 pointer 參數(nil 表示欄位不存在,跳過該項):
|
||
|
||
- **語意相似度**(`SemanticScore`):Embedding cosine → 0–100
|
||
- **預測互動率**(`EngagementPredicted`):like+reply → 對數正規化 0–100
|
||
- **受眾品質**(`AudienceQualityScore`):`computeAudienceQuality()` heuristic
|
||
|
||
權重:語意 20% / 互動 30% / 品質 10% / 舊有 productFit + solvedBy + recency 40%。
|
||
|
||
```go
|
||
func computeAudienceQuality(followers int) int
|
||
```
|
||
|
||
Heuristic:followers 在 500–50k 之間品質最佳,過低或過高遞減。
|
||
|
||
### 4. Creator Lookalike
|
||
|
||
**端點**: `POST /api/v1/scan-post/lookalike`(需 JWT)
|
||
|
||
```json
|
||
{ "source_post_id": "uuid", "brand_id": "...", "limit": 10 }
|
||
|
||
→ { "list": [{ "post_id", "author_id", "author_name", "similarity", ... }] }
|
||
```
|
||
|
||
**演算法**: cosine similarity(門檻 ≥0.3),同 brand 內找 embedding 最接近的貼文回傳。
|
||
|
||
| 檔案 | |
|
||
|------|--|
|
||
| Handler | `internal/handler/scan_post/lookalike_handler.go` |
|
||
| Logic | `internal/logic/scan_post/lookalike_logic.go` |
|
||
| `cosineSimilarity` | `internal/logic/scan_post/lookalike_logic.go` — 全 Go 實作 |
|
||
|
||
---
|
||
|
||
## Phase 2 — 營運流程
|
||
|
||
### 2.1 Creator CRM
|
||
|
||
**完整四層 model** `crm_contact`:
|
||
|
||
```
|
||
internal/model/crm_contact/
|
||
domain/entity/entity.go
|
||
domain/repository/repository.go
|
||
domain/usecase/usecase.go
|
||
usecase/usecase.go
|
||
repository/mongo.go
|
||
```
|
||
|
||
**端點**: `/api/v1/crm/contacts`(需 JWT)
|
||
|
||
| Method | Path | 用途 |
|
||
|--------|------|------|
|
||
| GET | `/` | List(`page`/`pageSize`/`brand_id`/`status`/`tag`) |
|
||
| POST | `/` | 新增聯絡人(`author_id` + `author_name`) |
|
||
| GET | `/:id` | 取得單筆 |
|
||
| PATCH | `/:id` | 更新(`notes`/`tags`/`status`) |
|
||
| DELETE | `/:id` | 刪除 |
|
||
|
||
**CRMContact 狀態機**: `lead` → `contacted` → `responded` → `converted` / `archived`
|
||
|
||
### 2.2 Publish Analytics
|
||
|
||
**完整四層 model** `publish_analytics`:
|
||
|
||
**Checkpoint 常數**(`internal/model/publish_analytics/usecase/usecase.go`): `+1h`、`+24h`、`+7d`
|
||
|
||
**方法**:
|
||
```go
|
||
RecordCheckpoint(ctx, tenantID, ownerUID, summary) (*PublishAnalyticsSummary, error)
|
||
ListByMedia(ctx, tenantID, ownerUID, mediaID) ([]PublishAnalyticsSummary, error)
|
||
```
|
||
|
||
**Worker handler** `publish-analytics`(step `check`):
|
||
|
||
呼叫 Threads API `GET /{media-id}?fields=like_count,reply_count,repost_count,quote_count` → 存入 `publish_analytics`。
|
||
|
||
### 2.3 Threads API Token 刷新
|
||
|
||
**底層 API**(`internal/library/threadsapi/media.go`):
|
||
```go
|
||
RefreshAccessToken(ctx, currentToken) (*TokenRefreshResult, error)
|
||
// GET /v1.0/refresh_access_token?grant_type=th_refresh_token
|
||
```
|
||
|
||
**儲存 Token**:
|
||
```go
|
||
// Repository
|
||
SaveAPIAccessToken(ctx, accountID, accessToken, expiresAt) (*entity.Secrets, error)
|
||
// Usecase
|
||
SaveAPIAccessToken(ctx, accountID, accessToken, expiresAt) (*SecretsData, error)
|
||
```
|
||
|
||
**Worker handler** `refresh-threads-token`(step `refresh`):讀取 secrets → refresh → 儲存。
|
||
|
||
### 2.4 Rate Limiter
|
||
|
||
```go
|
||
// internal/library/ratelimit/token_bucket.go
|
||
func NewTokenBucket(rate float64, burst int) *TokenBucket
|
||
func (tb *TokenBucket) Allow() bool
|
||
func (tb *TokenBucket) AllowN(n int) bool
|
||
```
|
||
|
||
- In-memory token bucket(非 Redis),thread-safe
|
||
- `rate` = tokens/sec,`burst` = 最大 burst
|
||
|
||
---
|
||
|
||
## Infrastructure
|
||
|
||
### Job Templates
|
||
|
||
`internal/model/job/usecase/usecase.go` 新增兩個 template constants + 對應 `Ensure*` methods + `template()` functions:
|
||
|
||
| Template Type | Steps | Timeout | Retry |
|
||
|---------------|-------|---------|-------|
|
||
| `refresh-threads-token` | `refresh`(go) | 120s | 3 次(30/120/300s) |
|
||
| `publish-analytics` | `check`(go) | 120s | 2 次(60s) |
|
||
|
||
### Worker Handlers 註冊
|
||
|
||
`internal/svc/service_context.go` 中註冊:
|
||
|
||
```go
|
||
jobworker.RegisterRefreshThreadsTokenHandler(runner, RefreshThreadsTokenDeps{...})
|
||
jobworker.RegisterPublishAnalyticsHandler(runner, PublishAnalyticsDeps{...})
|
||
```
|
||
|
||
### Worker Helper
|
||
|
||
```go
|
||
// internal/worker/job/expand_graph.go
|
||
func int64Field(payload map[string]any, key string) int64
|
||
```
|
||
|
||
從 job payload 讀 `int64`,支援 `float64` / `int64` / `int` / `json.Number`。
|