feat(creator): 监控账号画像采集入库——粉丝/关注/获赞/作品数与作者介绍

抖音 profile/other 响应本就携带画像数据但被丢弃,同步链路也无回填。
- migration 1046: creator_competitor 增加 signature、total_favorited 列及非负约束
- douyin 解析层: TargetProfile 增加签名与计数,total_favorited 兼容字符串大数
- store: UpdateCompetitorProfile 改为画像回填(nil 计数保留原值)
- 同步链路: 作品采集成功后拉取 profile 回填,失败仅记日志不阻断
This commit is contained in:
2026-09-30 16:37:56 +08:00
parent a9358f5c96
commit 3e809c4fc7
11 changed files with 176 additions and 25 deletions
+9
View File
@@ -316,6 +316,7 @@ type identityEnvelope struct {
AvatarThumb *struct {
URLList []string `json:"url_list"`
} `json:"avatar_thumb"`
Signature string `json:"signature"`
FollowerCount *int64 `json:"follower_count"`
FollowingCount *int64 `json:"following_count"`
AwemeCount *int64 `json:"aweme_count"`
@@ -327,6 +328,14 @@ type identityEnvelope struct {
// flexibleInt 兼容抖音返回的数字与字符串两种计数形态(total_favorited 实测为大数字符串)。
type flexibleInt int64
func flexibleIntPtr(value *flexibleInt) *int64 {
if value == nil {
return nil
}
converted := int64(*value)
return &converted
}
func (value *flexibleInt) UnmarshalJSON(data []byte) error {
text := strings.TrimSpace(string(data))
if text == "null" {
+15 -6
View File
@@ -27,11 +27,16 @@ type CreatorCollector struct {
}
type TargetProfile struct {
UID string
SecUID string
UniqueID string
Nickname string
AvatarURL string
UID string
SecUID string
UniqueID string
Nickname string
AvatarURL string
Signature string
FollowerCount *int64
FollowingCount *int64
TotalFavorited *int64
AwemeCount *int64
}
// WorkStatistics 是作品详情接口返回的指标快照,用于增长曲线时序采集。
@@ -174,7 +179,11 @@ func (c CreatorCollector) ResolveTarget(ctx context.Context, expectedKey string)
if identity.User.AvatarThumb != nil && len(identity.User.AvatarThumb.URLList) > 0 {
avatarURL = identity.User.AvatarThumb.URLList[0]
}
return TargetProfile{UID: identity.User.UID, SecUID: identity.User.SecUID, UniqueID: identity.User.UniqueID, Nickname: identity.User.Nickname, AvatarURL: avatarURL}, nil
return TargetProfile{
UID: identity.User.UID, SecUID: identity.User.SecUID, UniqueID: identity.User.UniqueID, Nickname: identity.User.Nickname, AvatarURL: avatarURL,
Signature: identity.User.Signature, FollowerCount: identity.User.FollowerCount, FollowingCount: identity.User.FollowingCount,
TotalFavorited: flexibleIntPtr(identity.User.TotalFavorited), AwemeCount: identity.User.AwemeCount,
}, nil
}
func (c CreatorCollector) ResolveWork(ctx context.Context, workKey string) (TargetProfile, error) {
@@ -37,6 +37,33 @@ func TestResolveTargetReturnsVerifiedProfileFields(t *testing.T) {
}
}
func TestResolveTargetExtractsPublicProfileMetrics(t *testing.T) {
browser := &collectorBrowser{response: Response{Status: 200, Body: []byte(`{"status_code":0,"user":{"uid":"2328120603967913","sec_uid":"MS4wLjABAAAA9f_a7k0bzVizLYXlpC7R61EIaqJ8Ordug7yp7AB8fGKuuF8Fzqk5_DM-eutXnPIK","unique_id":"96332518739","nickname":"目标","signature":"第一行介绍\n第二行介绍","follower_count":123,"following_count":45,"aweme_count":7,"total_favorited":"98765"}}`)}}
profile, err := (CreatorCollector{Browser: browser}).ResolveTarget(context.Background(), "MS4wLjABAAAA9f_a7k0bzVizLYXlpC7R61EIaqJ8Ordug7yp7AB8fGKuuF8Fzqk5_DM-eutXnPIK")
if err != nil {
t.Fatal(err)
}
if profile.Signature != "第一行介绍\n第二行介绍" {
t.Fatalf("signature: %q", profile.Signature)
}
if profile.FollowerCount == nil || *profile.FollowerCount != 123 || profile.FollowingCount == nil || *profile.FollowingCount != 45 ||
profile.AwemeCount == nil || *profile.AwemeCount != 7 {
t.Fatalf("counts: %+v", profile)
}
// total_favorited 实测为大数字符串,必须经 flexibleInt 兼容解析。
if profile.TotalFavorited == nil || *profile.TotalFavorited != 98765 {
t.Fatalf("total_favorited: %+v", profile.TotalFavorited)
}
}
func TestResolveTargetToleratesMissingPublicMetrics(t *testing.T) {
browser := &collectorBrowser{response: Response{Status: 200, Body: []byte(`{"status_code":0,"user":{"uid":"2328120603967913","sec_uid":"MS4wLjABAAAA9f_a7k0bzVizLYXlpC7R61EIaqJ8Ordug7yp7AB8fGKuuF8Fzqk5_DM-eutXnPIK","nickname":"目标"}}`)}}
profile, err := (CreatorCollector{Browser: browser}).ResolveTarget(context.Background(), "MS4wLjABAAAA9f_a7k0bzVizLYXlpC7R61EIaqJ8Ordug7yp7AB8fGKuuF8Fzqk5_DM-eutXnPIK")
if err != nil || profile.Signature != "" || profile.FollowerCount != nil || profile.TotalFavorited != nil {
t.Fatalf("target profile without metrics: %+v err=%v", profile, err)
}
}
func TestCanonicalTargetSecUIDRejectsUniqueIDLookup(t *testing.T) {
browser := &collectorBrowser{}
_, err := (CreatorCollector{Browser: browser}).CanonicalTargetSecUID(context.Background(), "creator_handle")