thread-master/backend/internal/model/threads_account/usecase/post_performance.go

190 lines
5.6 KiB
Go
Raw Permalink Normal View History

package usecase
import (
"context"
"fmt"
"strings"
"time"
app "haixun-backend/internal/library/errors"
"haixun-backend/internal/library/errors/code"
libthreads "haixun-backend/internal/library/threadsapi"
domusecase "haixun-backend/internal/model/threads_account/domain/usecase"
)
func (u *threadsAccountUseCase) ListPostPerformance(
ctx context.Context,
tenantID, ownerUID, accountID string,
limit int,
) (*domusecase.PostPerformanceResult, error) {
account, err := u.assertOwned(ctx, tenantID, ownerUID, accountID)
if err != nil {
return nil, err
}
token, err := u.ResolveAccountAPIAccessToken(ctx, tenantID, ownerUID, accountID)
if err != nil {
return nil, err
}
client := libthreads.NewClient(token)
if !client.Enabled() {
return nil, app.For(code.ThreadsAccount).InputMissingRequired("Threads API 未連線")
}
if limit <= 0 {
limit = 15
}
if limit > 30 {
limit = 30
}
userID := strings.TrimSpace(account.ThreadsUserID)
username := strings.TrimSpace(account.Username)
if profile, perr := libthreads.GetMeProfile(ctx, token); perr == nil && profile != nil {
if userID == "" {
userID = profile.ID.String()
}
if username == "" {
username = profile.Username
}
}
result := &domusecase.PostPerformanceResult{
AccountID: account.ID,
Username: username,
ThreadsUserID: userID,
FetchedAt: time.Now().UnixNano(),
Capabilities: postPerformanceCapabilities(),
AccountInsights: domusecase.AccountInsightsBlock{
Status: "skip",
Message: "缺少 threads_user_id",
},
Posts: []domusecase.PostPerformanceItem{},
}
if userID != "" {
result.AccountInsights = fetchAccountInsights(ctx, client, userID)
posts, err := client.UserThreads(ctx, userID, limit)
if err != nil {
return nil, err
}
for _, post := range posts {
item := domusecase.PostPerformanceItem{
MediaID: post.ID,
Text: post.Text,
Permalink: post.Permalink,
Timestamp: post.Timestamp,
MediaType: post.MediaType,
MediaURL: post.MediaURL,
ThumbnailURL: post.ThumbnailURL,
TopicTag: post.TopicTag,
Shortcode: post.Shortcode,
LikeCount: post.LikeCount,
ReplyCount: post.ReplyCount,
}
if stats, serr := client.GetMediaStats(ctx, post.ID); serr == nil && stats != nil {
item.LikeCount = stats.LikeCount
item.ReplyCount = stats.ReplyCount
item.RepostCount = stats.RepostCount
item.QuoteCount = stats.QuoteCount
item.Views = stats.Views
item.Shares = stats.Shares
item.InsightsStatus = "ok"
} else {
item.InsightsStatus = "error"
if serr != nil {
item.InsightsMessage = serr.Error()
} else {
item.InsightsMessage = "無法讀取貼文洞察"
}
}
result.Posts = append(result.Posts, item)
}
}
return result, nil
}
func postPerformanceCapabilities() []domusecase.PostMetricCapability {
return []domusecase.PostMetricCapability{
{
Scope: "threads_manage_insights", Name: "貼文互動數",
Metrics: []string{"likes", "replies", "reposts", "quotes", "views", "shares"},
Trackable: true, Note: "GET /{media-id}/insights非 media 物件欄位)",
},
{
Scope: "threads_manage_insights", Name: "貼文洞察",
Metrics: []string{"views", "likes", "replies", "reposts", "quotes", "shares"},
Trackable: true, Note: "GET /{media-id}/insightslifetime",
},
{
Scope: "threads_manage_insights", Name: "帳號洞察",
Metrics: []string{"views", "likes", "replies", "reposts", "quotes", "followers_count", "clicks"},
Trackable: true, Note: "GET /{user-id}/threads_insights",
},
{
Scope: "threads_read_replies", Name: "留言內容",
Metrics: []string{"reply_list"},
Trackable: true, Note: "GET /{media-id}/replies非數字統計",
},
{
Scope: "threads_keyword_search", Name: "關鍵字搜尋",
Trackable: false, Note: "搜尋他人貼文,非你的發文成效",
},
{
Scope: "threads_profile_discovery", Name: "帳號查詢",
Trackable: false, Note: "查他人 profile非成效統計",
},
{
Scope: "threads_manage_mentions", Name: "提及",
Trackable: false, Note: "誰 @ 你,非單篇貼文成效",
},
{
Scope: "threads_location_tagging", Name: "地點",
Trackable: false, Note: "地點標籤搜尋",
},
{
Scope: "threads_content_publish", Name: "發文",
Trackable: false, Note: "寫入類;發文後用本頁追蹤成效",
},
{
Scope: "threads_delete", Name: "刪文",
Trackable: false, Note: "寫入類",
},
{
Scope: "threads_manage_replies", Name: "留言管理",
Trackable: false, Note: "隱藏留言,非統計",
},
{
Scope: "threads_share_to_instagram", Name: "同步 IG",
Trackable: false, Note: "crosspost 動作",
},
}
}
func fetchAccountInsights(ctx context.Context, client *libthreads.Client, userID string) domusecase.AccountInsightsBlock {
items, err := client.UserInsights(ctx, userID, nil)
if err != nil {
msg := err.Error()
if libthreads.IsPermissionError(err) {
msg += "(需 threads_manage_insights"
}
return domusecase.AccountInsightsBlock{Status: "error", Message: msg}
}
metrics := make([]domusecase.InsightMetricItem, 0, len(items))
for _, item := range items {
name := strings.TrimSpace(item.Name)
if name == "" || len(item.Values) == 0 {
continue
}
metrics = append(metrics, domusecase.InsightMetricItem{Name: name, Value: item.Values[0].Value})
}
if len(metrics) == 0 {
return domusecase.AccountInsightsBlock{Status: "ok", Message: "尚無帳號洞察資料"}
}
return domusecase.AccountInsightsBlock{
Status: "ok",
Message: fmt.Sprintf("%d 項指標", len(metrics)),
Metrics: metrics,
}
}