124 lines
3.7 KiB
Go
124 lines
3.7 KiB
Go
|
|
package insightsapi
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"sort"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"apps/backend/internal/middleware"
|
||
|
|
"apps/backend/internal/module/growth/usecase"
|
||
|
|
"apps/backend/internal/svc"
|
||
|
|
"apps/backend/internal/types"
|
||
|
|
|
||
|
|
"github.com/zeromicro/go-zero/core/logx"
|
||
|
|
)
|
||
|
|
|
||
|
|
type GetInsightsSummaryLogic struct {
|
||
|
|
logx.Logger
|
||
|
|
ctx context.Context
|
||
|
|
svcCtx *svc.ServiceContext
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewGetInsightsSummaryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetInsightsSummaryLogic {
|
||
|
|
return &GetInsightsSummaryLogic{Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (l *GetInsightsSummaryLogic) GetInsightsSummary(req *types.InsightsSummaryReq) (*types.InsightsSummaryData, error) {
|
||
|
|
uid, ok := middleware.UIDFrom(l.ctx)
|
||
|
|
if !ok || uid <= 0 {
|
||
|
|
return nil, fmt.Errorf("unauthorized")
|
||
|
|
}
|
||
|
|
if l.svcCtx.Studio == nil {
|
||
|
|
return &types.InsightsSummaryData{Conclusion: "尚無創作資料", Suggestions: []string{"先同步已發貼文"}}, nil
|
||
|
|
}
|
||
|
|
posts, err := l.svcCtx.Studio.ListOwnPosts(l.ctx, uid, req.AccountId)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
months := req.Months
|
||
|
|
if months <= 0 {
|
||
|
|
months = 3
|
||
|
|
}
|
||
|
|
cutoff := time.Now().UTC().AddDate(0, -months, 0).UnixNano()
|
||
|
|
type agg struct{ posts, views, likes, replies int }
|
||
|
|
byMonth := map[string]*agg{}
|
||
|
|
var totalV, totalL, totalR, n int
|
||
|
|
var top []types.InsightsPostPublic
|
||
|
|
for _, p := range posts {
|
||
|
|
if p == nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
ts := p.PublishedAt
|
||
|
|
if ts > 0 && ts < cutoff {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
n++
|
||
|
|
totalV += p.ViewCount
|
||
|
|
totalL += p.LikeCount
|
||
|
|
totalR += p.ReplyCount
|
||
|
|
mk := time.Unix(0, ts).UTC().Format("2006-01")
|
||
|
|
if ts == 0 {
|
||
|
|
mk = time.Now().UTC().Format("2006-01")
|
||
|
|
}
|
||
|
|
a := byMonth[mk]
|
||
|
|
if a == nil {
|
||
|
|
a = &agg{}
|
||
|
|
byMonth[mk] = a
|
||
|
|
}
|
||
|
|
a.posts++
|
||
|
|
a.views += p.ViewCount
|
||
|
|
a.likes += p.LikeCount
|
||
|
|
a.replies += p.ReplyCount
|
||
|
|
eng := 0.0
|
||
|
|
if p.ViewCount > 0 {
|
||
|
|
eng = float64(p.LikeCount+p.ReplyCount) / float64(p.ViewCount)
|
||
|
|
}
|
||
|
|
prev := p.Text
|
||
|
|
if len([]rune(prev)) > 80 {
|
||
|
|
prev = string([]rune(prev)[:80]) + "…"
|
||
|
|
}
|
||
|
|
top = append(top, types.InsightsPostPublic{
|
||
|
|
Id: p.ID, TextPreview: prev, PublishedAt: ts,
|
||
|
|
ViewCount: p.ViewCount, LikeCount: p.LikeCount, ReplyCount: p.ReplyCount, EngRate: usecase.Round2(eng),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
sort.Slice(top, func(i, j int) bool { return top[i].EngRate > top[j].EngRate })
|
||
|
|
if len(top) > 5 {
|
||
|
|
top = top[:5]
|
||
|
|
}
|
||
|
|
avgEng := 0.0
|
||
|
|
if totalV > 0 {
|
||
|
|
avgEng = float64(totalL+totalR) / float64(totalV)
|
||
|
|
}
|
||
|
|
var buckets []types.InsightsMonthBucket
|
||
|
|
for mk, a := range byMonth {
|
||
|
|
e := 0.0
|
||
|
|
if a.views > 0 {
|
||
|
|
e = float64(a.likes+a.replies) / float64(a.views)
|
||
|
|
}
|
||
|
|
buckets = append(buckets, types.InsightsMonthBucket{
|
||
|
|
MonthKey: mk, Posts: a.posts, Views: a.views, Likes: a.likes, Replies: a.replies, AvgEngRate: usecase.Round2(e),
|
||
|
|
})
|
||
|
|
}
|
||
|
|
sort.Slice(buckets, func(i, j int) bool { return buckets[i].MonthKey < buckets[j].MonthKey })
|
||
|
|
conclusion := fmt.Sprintf("近 %d 個月同步貼文 %d 則,平均互動率 %.2f%%。", months, n, avgEng*100)
|
||
|
|
sugs := []string{"維持高互動題材節奏", "把表現佳貼文結構拿去仿寫"}
|
||
|
|
if n == 0 {
|
||
|
|
conclusion = "尚無足夠已同步貼文"
|
||
|
|
sugs = []string{"到創作 → 我的貼文按同步", "連好 Threads 帳號後再看成效"}
|
||
|
|
} else if avgEng < 0.02 {
|
||
|
|
sugs = append([]string{"互動率偏低:可加強開頭鉤子與提問 CTA"}, sugs...)
|
||
|
|
}
|
||
|
|
// also feed benchmark
|
||
|
|
if n > 0 {
|
||
|
|
_ = l.svcCtx.Growth.ContributeBenchmark(l.ctx, uid, "general", avgEng, float64(totalV)/float64(n))
|
||
|
|
}
|
||
|
|
return &types.InsightsSummaryData{
|
||
|
|
AccountId: req.AccountId, PostCount: n, TotalViews: totalV, TotalLikes: totalL, TotalReplies: totalR,
|
||
|
|
AvgEngRate: usecase.Round2(avgEng), TopPosts: top, MonthBuckets: buckets,
|
||
|
|
Conclusion: conclusion, Suggestions: sugs,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
|