feat(ops): ECR-007 行为分析与 ECR-008 内容运营后台
落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -18,16 +18,98 @@ type ReportRepo struct {
|
||||
Pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
// Create inserts a growth report.
|
||||
// Create inserts a growth report (optional peer for pair reports).
|
||||
func (r *ReportRepo) Create(ctx context.Context, userID, profileID uuid.UUID, typ string, summary, detail json.RawMessage) (*model.GrowthReport, error) {
|
||||
return r.CreateWithPeer(ctx, userID, profileID, nil, typ, summary, detail)
|
||||
}
|
||||
|
||||
// CreateWithPeer inserts a report with optional peer_profile_id.
|
||||
func (r *ReportRepo) CreateWithPeer(ctx context.Context, userID, profileID uuid.UUID, peer *uuid.UUID, typ string, summary, detail json.RawMessage) (*model.GrowthReport, error) {
|
||||
rep := &model.GrowthReport{}
|
||||
err := r.Pool.QueryRow(ctx, `
|
||||
INSERT INTO growth_reports(user_id, profile_id, type, summary, detail)
|
||||
VALUES ($1,$2,$3,$4,$5)
|
||||
INSERT INTO growth_reports(user_id, profile_id, peer_profile_id, type, summary, detail)
|
||||
VALUES ($1,$2,$3,$4,$5,$6)
|
||||
RETURNING id, user_id, profile_id, type, summary, detail, created_at`,
|
||||
userID, profileID, typ, summary, detail,
|
||||
userID, profileID, peer, typ, summary, detail,
|
||||
).Scan(&rep.ID, &rep.UserID, &rep.ProfileID, &rep.Type, &rep.Summary, &rep.Detail, &rep.CreatedAt)
|
||||
return rep, err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rep.PeerProfileID = peer
|
||||
return rep, nil
|
||||
}
|
||||
|
||||
// SoftDeleteMatching soft-deletes prior reports for overwrite semantics.
|
||||
func (r *ReportRepo) SoftDeleteMatching(ctx context.Context, userID, profileID uuid.UUID, peer *uuid.UUID, typ string) error {
|
||||
if peer == nil {
|
||||
_, err := r.Pool.Exec(ctx, `
|
||||
UPDATE growth_reports SET deleted_at=now(), updated_at=now()
|
||||
WHERE user_id=$1 AND profile_id=$2 AND type=$3
|
||||
AND peer_profile_id IS NULL AND deleted_at IS NULL`,
|
||||
userID, profileID, typ)
|
||||
return err
|
||||
}
|
||||
_, err := r.Pool.Exec(ctx, `
|
||||
UPDATE growth_reports SET deleted_at=now(), updated_at=now()
|
||||
WHERE user_id=$1 AND type=$2 AND deleted_at IS NULL
|
||||
AND (
|
||||
(profile_id=$3 AND peer_profile_id=$4) OR
|
||||
(profile_id=$4 AND peer_profile_id=$3)
|
||||
)`,
|
||||
userID, typ, profileID, *peer)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpsertWithPeer soft-deletes matching then inserts.
|
||||
func (r *ReportRepo) UpsertWithPeer(ctx context.Context, userID, profileID uuid.UUID, peer *uuid.UUID, typ string, summary, detail json.RawMessage) (*model.GrowthReport, error) {
|
||||
if err := r.SoftDeleteMatching(ctx, userID, profileID, peer, typ); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.CreateWithPeer(ctx, userID, profileID, peer, typ, summary, detail)
|
||||
}
|
||||
|
||||
// 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{}
|
||||
var peerOut *uuid.UUID
|
||||
var err error
|
||||
if peer == nil {
|
||||
err = r.Pool.QueryRow(ctx, `
|
||||
SELECT id, user_id, profile_id, peer_profile_id, type, summary, detail, created_at
|
||||
FROM growth_reports
|
||||
WHERE user_id=$1 AND profile_id=$2 AND type=$3
|
||||
AND peer_profile_id IS NULL AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC LIMIT 1`,
|
||||
userID, profileID, typ,
|
||||
).Scan(&rep.ID, &rep.UserID, &rep.ProfileID, &peerOut, &rep.Type, &rep.Summary, &rep.Detail, &rep.CreatedAt)
|
||||
} else {
|
||||
err = r.Pool.QueryRow(ctx, `
|
||||
SELECT id, user_id, profile_id, peer_profile_id, type, summary, detail, created_at
|
||||
FROM growth_reports
|
||||
WHERE user_id=$1 AND type=$2 AND deleted_at IS NULL
|
||||
AND (
|
||||
(profile_id=$3 AND peer_profile_id=$4) OR
|
||||
(profile_id=$4 AND peer_profile_id=$3)
|
||||
)
|
||||
ORDER BY created_at DESC LIMIT 1`,
|
||||
userID, typ, profileID, *peer,
|
||||
).Scan(&rep.ID, &rep.UserID, &rep.ProfileID, &peerOut, &rep.Type, &rep.Summary, &rep.Detail, &rep.CreatedAt)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rep.PeerProfileID = peerOut
|
||||
return rep, nil
|
||||
}
|
||||
|
||||
// SoftDeleteForProfile marks all reports involving a profile as deleted.
|
||||
func (r *ReportRepo) SoftDeleteForProfile(ctx context.Context, userID, profileID uuid.UUID) error {
|
||||
_, err := r.Pool.Exec(ctx, `
|
||||
UPDATE growth_reports SET deleted_at=now(), updated_at=now()
|
||||
WHERE user_id=$1 AND deleted_at IS NULL
|
||||
AND (profile_id=$2 OR peer_profile_id=$2)`,
|
||||
userID, profileID)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetForUser loads a report owned by user.
|
||||
@@ -188,10 +270,52 @@ func (r *ReportRepo) PayMock(ctx context.Context, userID, orderID uuid.UUID) err
|
||||
userID, p, days); err != nil {
|
||||
return err
|
||||
}
|
||||
case "ask_pack":
|
||||
p := "pack10"
|
||||
if plan != nil && *plan != "" {
|
||||
p = *plan
|
||||
}
|
||||
delta := AskPackQuota(p)
|
||||
if delta <= 0 {
|
||||
return errString("invalid ask_pack plan")
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE users SET ask_paid_quota_left = ask_paid_quota_left + $2, updated_at=now()
|
||||
WHERE id=$1 AND deleted_at IS NULL`, userID, delta); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
// AskPackQuota returns how many ask replies a pack plan grants.
|
||||
func AskPackQuota(plan string) int {
|
||||
switch plan {
|
||||
case "pack10":
|
||||
return 10
|
||||
case "pack30":
|
||||
return 30
|
||||
case "pack100":
|
||||
return 100
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// AskPackAmountCents is mock price for an ask pack plan.
|
||||
func AskPackAmountCents(plan string) int {
|
||||
switch plan {
|
||||
case "pack10":
|
||||
return 990
|
||||
case "pack30":
|
||||
return 1980
|
||||
case "pack100":
|
||||
return 4990
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
var errMissingReport = errString("report_id required for deep_access")
|
||||
|
||||
type errString string
|
||||
|
||||
Reference in New Issue
Block a user