feat(recommendations): collect demographics and advance age cohorts
ci / ts / ts (packages + web) (push) Successful in 3m19s
ci / db / db (push) Successful in 53s
ci / module boundaries / boundaries (push) Successful in 7s
ci / api / api (push) Successful in 51s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m3s
ci / recommender / recommender (push) Failing after 16s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m15s

This commit is contained in:
mrleo1nid committed 2026-10-22 14:19:52 +03:00
1 parent e158558da3
commit fff91c446b
24 files changed
+778 -324

No files matched your search

+32 -10
View File
@@ -8,6 +8,7 @@ import (
"github.com/google/uuid"
"pipu/api/internal/api/gen"
"pipu/api/internal/platform/apperr"
)
// Онбординг вкуса (docs/recommendations.md#7): новый пользователь отмечает исполнителей и жанры — рекомендатель
@@ -16,8 +17,6 @@ import (
const (
onboardingArtists = 40
onboardingGenres = 24
// История, после которой онбординг уже не предлагается: вкус и так виден по прослушиваниям.
onboardingMinHistory = 10
)
// GetOnboarding реализует gen.StrictServerInterface.
@@ -27,13 +26,18 @@ func (h *Handler) GetOnboarding(ctx context.Context, _ gen.GetOnboardingRequestO
return nil, err
}
out := gen.GetOnboarding200JSONResponse{}
var done bool
var plays int
var raw []byte
if err := h.pool.QueryRow(ctx, `
SELECT EXISTS (SELECT 1 FROM user_profiles_rec WHERE user_id = $1 AND onboarding IS NOT NULL),
(SELECT count(*) FROM user_track_stats WHERE user_id = $1)`, p.UserID).Scan(&done, &plays); err != nil {
SELECT (SELECT onboarding || jsonb_build_object('age_group', rec_age_group(onboarding))
FROM user_profiles_rec WHERE user_id = $1)`, p.UserID).Scan(&raw); err != nil {
return nil, fmt.Errorf("wave: onboarding state: %w", err)
}
if len(raw) > 0 {
out.Choice = &gen.OnboardingChoice{}
if err := json.Unmarshal(raw, out.Choice); err != nil {
return nil, err
}
}
rows, err := h.pool.Query(ctx, `
SELECT a.id, a.name, count(*) AS n,
@@ -112,7 +116,7 @@ func (h *Handler) GetOnboarding(ctx context.Context, _ gen.GetOnboardingRequestO
TrackCount int `json:"track_count"`
}{}
}
out.Required = !done && plays < onboardingMinHistory && len(out.Artists) > 0
out.Required = out.Choice == nil
return out, nil
}
@@ -122,6 +126,19 @@ func (h *Handler) SaveOnboarding(ctx context.Context, req gen.SaveOnboardingRequ
if err != nil {
return nil, err
}
if req.Body == nil {
return nil, apperr.Validation("body", "common.bad_request", "body required", nil)
}
if req.Body.Gender != nil && *req.Body.Gender != "male" && *req.Body.Gender != "female" && *req.Body.Gender != "other" {
return nil, apperr.Validation("gender", "common.bad_request", "invalid gender", nil)
}
if req.Body.AgeGroup != nil {
switch *req.Body.AgeGroup {
case "under_13", "13_19", "20_29", "30_39", "40_49", "50_59", "60_plus":
default:
return nil, apperr.Validation("age_group", "common.bad_request", "invalid age group", nil)
}
}
ids := req.Body.ArtistIds
if ids == nil {
ids = []uuid.UUID{}
@@ -130,10 +147,15 @@ func (h *Handler) SaveOnboarding(ctx context.Context, req gen.SaveOnboardingRequ
if genres == nil {
genres = []string{}
}
raw, _ := json.Marshal(map[string]any{"artist_ids": ids, "genres": genres})
raw, _ := json.Marshal(map[string]any{"artist_ids": ids, "genres": genres, "gender": req.Body.Gender, "age_group": req.Body.AgeGroup})
if _, err := h.pool.Exec(ctx, `
INSERT INTO user_profiles_rec (user_id, onboarding, updated_at) VALUES ($1, $2, now())
ON CONFLICT (user_id) DO UPDATE SET onboarding = EXCLUDED.onboarding, updated_at = now()`,
INSERT INTO user_profiles_rec (user_id, onboarding, updated_at) VALUES ($1, $2::jsonb || jsonb_build_object('age_group_year', EXTRACT(YEAR FROM CURRENT_DATE)::int), now())
ON CONFLICT (user_id) DO UPDATE SET onboarding = CASE
WHEN rec_age_group(user_profiles_rec.onboarding) = EXCLUDED.onboarding->>'age_group'
THEN EXCLUDED.onboarding || jsonb_build_object(
'age_group', user_profiles_rec.onboarding->'age_group',
'age_group_year', user_profiles_rec.onboarding->'age_group_year')
ELSE EXCLUDED.onboarding END, updated_at = now()`,
p.UserID, raw); err != nil {
return nil, fmt.Errorf("wave: save onboarding: %w", err)
}