落地 synastry/star/ask API 与 H5 页面,补齐 cece-frontend-re complete-design 证据文档,并加入 Android 模拟器截图抓取脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
126 lines
3.7 KiB
Go
126 lines
3.7 KiB
Go
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
|
|
}
|