feat(ECR-012–016): 合规、题库、时辰刷新、头像、MBTI OEJTS 与埋点
落地输入合规、探索题库、报告日/时辰刷新、账号头像、OEJTS 量表,并补齐 H5 埋点与 Admin 漏斗;同步 ESS 工件、切至自建 Git、清理 GitHub Actions。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -21,38 +21,35 @@ type AccountRow struct {
|
||||
Phone string
|
||||
PasswordHash string
|
||||
Nickname string
|
||||
AvatarURL string
|
||||
Status string
|
||||
}
|
||||
|
||||
// GetByPhone loads a registered user by phone.
|
||||
func (r *AuthRepo) GetByPhone(ctx context.Context, phone string) (*AccountRow, error) {
|
||||
row := &AccountRow{}
|
||||
var nick *string
|
||||
err := r.Pool.QueryRow(ctx, `
|
||||
SELECT id, phone, password_hash, COALESCE(nickname,''), status
|
||||
SELECT id, phone, password_hash, COALESCE(nickname,''), COALESCE(avatar_url,''), status
|
||||
FROM users
|
||||
WHERE phone=$1 AND deleted_at IS NULL`, phone,
|
||||
).Scan(&row.ID, &row.Phone, &row.PasswordHash, &nick, &row.Status)
|
||||
).Scan(&row.ID, &row.Phone, &row.PasswordHash, &row.Nickname, &row.AvatarURL, &row.Status)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nick != nil {
|
||||
row.Nickname = *nick
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
|
||||
// GetAccount loads account fields for a user id.
|
||||
func (r *AuthRepo) GetAccount(ctx context.Context, userID uuid.UUID) (*AccountRow, error) {
|
||||
row := &AccountRow{}
|
||||
var phone, hash, nick *string
|
||||
var phone, hash *string
|
||||
err := r.Pool.QueryRow(ctx, `
|
||||
SELECT id, phone, password_hash, nickname, status
|
||||
SELECT id, phone, password_hash, COALESCE(nickname,''), COALESCE(avatar_url,''), status
|
||||
FROM users WHERE id=$1 AND deleted_at IS NULL`, userID,
|
||||
).Scan(&row.ID, &phone, &hash, &nick, &row.Status)
|
||||
).Scan(&row.ID, &phone, &hash, &row.Nickname, &row.AvatarURL, &row.Status)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -62,9 +59,6 @@ func (r *AuthRepo) GetAccount(ctx context.Context, userID uuid.UUID) (*AccountRo
|
||||
if hash != nil {
|
||||
row.PasswordHash = *hash
|
||||
}
|
||||
if nick != nil {
|
||||
row.Nickname = *nick
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
|
||||
@@ -105,6 +99,14 @@ func (r *AuthRepo) UpdateNickname(ctx context.Context, userID uuid.UUID, nicknam
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateAvatarURL sets users.avatar_url.
|
||||
func (r *AuthRepo) UpdateAvatarURL(ctx context.Context, userID uuid.UUID, url string) error {
|
||||
_, err := r.Pool.Exec(ctx, `
|
||||
UPDATE users SET avatar_url=$2, updated_at=now()
|
||||
WHERE id=$1 AND deleted_at IS NULL`, userID, url)
|
||||
return err
|
||||
}
|
||||
|
||||
// TouchPassword updates stored password hash (open-login record).
|
||||
func (r *AuthRepo) TouchPassword(ctx context.Context, userID uuid.UUID, hash string) error {
|
||||
_, err := r.Pool.Exec(ctx, `
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// HomeDailyTipsRepo persists per-时辰 homepage tips.
|
||||
type HomeDailyTipsRepo struct {
|
||||
Pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
// GetTips returns cached tips JSON for user + shichen start.
|
||||
func (r *HomeDailyTipsRepo) GetTips(ctx context.Context, userID uuid.UUID, start time.Time) (json.RawMessage, string, error) {
|
||||
var raw json.RawMessage
|
||||
var source string
|
||||
err := r.Pool.QueryRow(ctx, `
|
||||
SELECT tips, source FROM home_daily_tips
|
||||
WHERE user_id=$1 AND shichen_start=$2`, userID, start,
|
||||
).Scan(&raw, &source)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return raw, source, nil
|
||||
}
|
||||
|
||||
// UpsertTips stores tips for user × shichen.
|
||||
func (r *HomeDailyTipsRepo) UpsertTips(
|
||||
ctx context.Context,
|
||||
userID uuid.UUID,
|
||||
start time.Time,
|
||||
shichen int,
|
||||
tips json.RawMessage,
|
||||
source string,
|
||||
) error {
|
||||
_, err := r.Pool.Exec(ctx, `
|
||||
INSERT INTO home_daily_tips(user_id, shichen_start, shichen, tips, source)
|
||||
VALUES ($1,$2,$3,$4,$5)
|
||||
ON CONFLICT (user_id, shichen_start) DO UPDATE
|
||||
SET tips=EXCLUDED.tips, source=EXCLUDED.source, shichen=EXCLUDED.shichen`,
|
||||
userID, start, shichen, tips, source,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// ErrNoTips is pgx.ErrNoRows alias for callers.
|
||||
var ErrNoTips = pgx.ErrNoRows
|
||||
@@ -68,6 +68,21 @@ func (r *ReportRepo) UpsertWithPeer(ctx context.Context, userID, profileID uuid.
|
||||
return r.CreateWithPeer(ctx, userID, profileID, peer, typ, summary, detail)
|
||||
}
|
||||
|
||||
// UpdateContent patches summary/detail in place (keeps report id for deep access).
|
||||
func (r *ReportRepo) UpdateContent(ctx context.Context, userID, reportID uuid.UUID, summary, detail json.RawMessage) error {
|
||||
tag, err := r.Pool.Exec(ctx, `
|
||||
UPDATE growth_reports SET summary=$3, detail=$4, updated_at=now()
|
||||
WHERE id=$1 AND user_id=$2 AND deleted_at IS NULL`,
|
||||
reportID, userID, summary, detail)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return pgx.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLatest returns newest non-deleted report for profile+type(+peer).
|
||||
func (r *ReportRepo) GetLatest(ctx context.Context, userID, profileID uuid.UUID, typ string, peer *uuid.UUID) (*model.GrowthReport, error) {
|
||||
rep := &model.GrowthReport{}
|
||||
|
||||
@@ -28,6 +28,8 @@ type ScaleDetail struct {
|
||||
Slug string `json:"slug"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Access string `json:"access,omitempty"` // free | membership
|
||||
Locked bool `json:"locked,omitempty"`
|
||||
Questions []ScaleQuestion `json:"questions"`
|
||||
}
|
||||
|
||||
@@ -171,6 +173,24 @@ func (r *ScaleRepo) SaveResult(ctx context.Context, userID, scaleID, profileID u
|
||||
return id, err
|
||||
}
|
||||
|
||||
// LatestResult returns the newest result for user + published slug.
|
||||
func (r *ScaleRepo) LatestResult(ctx context.Context, userID uuid.UUID, slug string) (*ScaleResultRow, error) {
|
||||
var row ScaleResultRow
|
||||
err := r.Pool.QueryRow(ctx, `
|
||||
SELECT sr.id, s.slug, sr.result
|
||||
FROM scale_results sr
|
||||
JOIN scales s ON s.id = sr.scale_id
|
||||
WHERE sr.user_id = $1 AND s.slug = $2 AND s.deleted_at IS NULL
|
||||
AND sr.deleted_at IS NULL
|
||||
ORDER BY sr.created_at DESC
|
||||
LIMIT 1`, userID, slug,
|
||||
).Scan(&row.ID, &row.ScaleSlug, &row.Result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// ScaleIDBySlug resolves id for a published scale.
|
||||
func (r *ScaleRepo) ScaleIDBySlug(ctx context.Context, slug string) (uuid.UUID, error) {
|
||||
var id uuid.UUID
|
||||
@@ -179,3 +199,20 @@ func (r *ScaleRepo) ScaleIDBySlug(ctx context.Context, slug string) (uuid.UUID,
|
||||
WHERE slug=$1 AND status='published' AND deleted_at IS NULL`, slug).Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
// EnsurePublished upserts scale metadata (curated bank stubs; questions served from embed).
|
||||
func (r *ScaleRepo) EnsurePublished(ctx context.Context, slug, title, description string) (uuid.UUID, error) {
|
||||
var id uuid.UUID
|
||||
err := r.Pool.QueryRow(ctx, `
|
||||
INSERT INTO scales (slug, title, description, status)
|
||||
VALUES ($1,$2,$3,'published')
|
||||
ON CONFLICT (slug) DO UPDATE SET
|
||||
title=EXCLUDED.title,
|
||||
description=EXCLUDED.description,
|
||||
status='published',
|
||||
updated_at=now(),
|
||||
deleted_at=NULL
|
||||
RETURNING id`, slug, title, description,
|
||||
).Scan(&id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user