feat(creator): 监控账号画像采集入库——粉丝/关注/获赞/作品数与作者介绍
抖音 profile/other 响应本就携带画像数据但被丢弃,同步链路也无回填。 - migration 1046: creator_competitor 增加 signature、total_favorited 列及非负约束 - douyin 解析层: TargetProfile 增加签名与计数,total_favorited 兼容字符串大数 - store: UpdateCompetitorProfile 改为画像回填(nil 计数保留原值) - 同步链路: 作品采集成功后拉取 profile 回填,失败仅记日志不阻断
This commit is contained in:
@@ -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" {
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user