feat: P1 合盘/星座/问答与测测完整设计包及模拟器取证工具
落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"github.com/yuxingu/digital-psychology/apps/api/internal/model"
|
||||
)
|
||||
|
||||
// SynastryInviteRepo persists synastry invite tokens.
|
||||
type SynastryInviteRepo struct {
|
||||
Pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
// Create inserts an invite (7-day expiry).
|
||||
func (r *SynastryInviteRepo) Create(ctx context.Context, hostUser, hostProfile uuid.UUID) (*model.SynastryInvite, error) {
|
||||
token, err := randomToken(16)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
exp := time.Now().UTC().Add(7 * 24 * time.Hour)
|
||||
inv := &model.SynastryInvite{}
|
||||
err = r.Pool.QueryRow(ctx, `
|
||||
INSERT INTO synastry_invites(token, host_user_id, host_profile_id, expires_at)
|
||||
VALUES ($1,$2,$3,$4)
|
||||
RETURNING id, token, host_user_id, host_profile_id, expires_at,
|
||||
guest_user_id, guest_profile_id, report_id, created_at`,
|
||||
token, hostUser, hostProfile, exp,
|
||||
).Scan(&inv.ID, &inv.Token, &inv.HostUserID, &inv.HostProfileID, &inv.ExpiresAt,
|
||||
&inv.GuestUserID, &inv.GuestProfileID, &inv.ReportID, &inv.CreatedAt)
|
||||
return inv, err
|
||||
}
|
||||
|
||||
// GetByToken loads a non-deleted invite.
|
||||
func (r *SynastryInviteRepo) GetByToken(ctx context.Context, token string) (*model.SynastryInvite, error) {
|
||||
inv := &model.SynastryInvite{}
|
||||
err := r.Pool.QueryRow(ctx, `
|
||||
SELECT id, token, host_user_id, host_profile_id, expires_at,
|
||||
guest_user_id, guest_profile_id, report_id, created_at
|
||||
FROM synastry_invites WHERE token=$1 AND deleted_at IS NULL`, token,
|
||||
).Scan(&inv.ID, &inv.Token, &inv.HostUserID, &inv.HostProfileID, &inv.ExpiresAt,
|
||||
&inv.GuestUserID, &inv.GuestProfileID, &inv.ReportID, &inv.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return inv, nil
|
||||
}
|
||||
|
||||
// AcceptAtomic creates guest profile + synastry report + marks invite in one transaction.
|
||||
func (r *SynastryInviteRepo) AcceptAtomic(
|
||||
ctx context.Context,
|
||||
inviteID, guestUser uuid.UUID,
|
||||
name string,
|
||||
birth time.Time,
|
||||
birthTime, birthPlace *string,
|
||||
summary, detail json.RawMessage,
|
||||
) (*model.GrowthReport, error) {
|
||||
tx, err := r.Pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
var guestID uuid.UUID
|
||||
var bt *string
|
||||
var bp *string
|
||||
var createdAt time.Time
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO profiles(user_id, relation, display_name, birth_date, birth_time, birth_place)
|
||||
VALUES ($1,'other',$2,$3,
|
||||
CASE WHEN $4::text IS NULL OR $4::text = '' THEN NULL ELSE $4::time END,
|
||||
NULLIF(TRIM($5::text), ''))
|
||||
RETURNING id,
|
||||
CASE WHEN birth_time IS NULL THEN NULL ELSE to_char(birth_time, 'HH24:MI') END,
|
||||
birth_place, created_at`,
|
||||
guestUser, name, birth, birthTime, birthPlace,
|
||||
).Scan(&guestID, &bt, &bp, &createdAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rep := &model.GrowthReport{}
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO growth_reports(user_id, profile_id, type, summary, detail)
|
||||
VALUES ($1,$2,'synastry',$3,$4)
|
||||
RETURNING id, user_id, profile_id, type, summary, detail, created_at`,
|
||||
guestUser, guestID, summary, detail,
|
||||
).Scan(&rep.ID, &rep.UserID, &rep.ProfileID, &rep.Type, &rep.Summary, &rep.Detail, &rep.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tag, err := tx.Exec(ctx, `
|
||||
UPDATE synastry_invites
|
||||
SET guest_user_id=$2, guest_profile_id=$3, report_id=$4, updated_at=now()
|
||||
WHERE id=$1 AND deleted_at IS NULL AND report_id IS NULL AND expires_at > now()`,
|
||||
inviteID, guestUser, guestID, rep.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return nil, errors.New("invite already used or expired")
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rep, nil
|
||||
}
|
||||
|
||||
func randomToken(n int) (string, error) {
|
||||
b := make([]byte, n)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
Reference in New Issue
Block a user