Files
digital-psychology/apps/api/internal/service/profile/service.go
T
jackyu66gitandCursor 89756f65b4 feat(ECR-012–016): 合规、题库、时辰刷新、头像、MBTI OEJTS 与埋点
落地输入合规、探索题库、报告日/时辰刷新、账号头像、OEJTS 量表,并补齐 H5 埋点与 Admin 漏斗;同步 ESS 工件、切至自建 Git、清理 GitHub Actions。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 01:27:58 +08:00

162 lines
4.0 KiB
Go

package profile
import (
"context"
"errors"
"strings"
"time"
"github.com/google/uuid"
"github.com/yuxingu/digital-psychology/apps/api/internal/model"
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
"github.com/yuxingu/digital-psychology/apps/api/internal/service/bootstrap"
"github.com/yuxingu/digital-psychology/apps/api/internal/textsafe"
)
// Service manages personal archives.
type Service struct {
Repo *repository.ProfileRepo
Reports *repository.ReportRepo
Bootstrap *bootstrap.Service
}
// CreateInput is validated create payload.
type CreateInput struct {
Relation string
DisplayName string
BirthDate time.Time
RelationType *string
BirthTime *string
BirthPlace *string
}
// ErrSelfExists is returned when creating a second self profile.
var ErrSelfExists = errors.New("self profile already exists")
// Create stores a profile for the user.
func (s *Service) Create(ctx context.Context, userID uuid.UUID, in CreateInput) (*model.Profile, error) {
if in.Relation != "self" && in.Relation != "other" {
return nil, errors.New("relation must be self or other")
}
if in.BirthDate.IsZero() {
return nil, errors.New("birth_date required")
}
if in.Relation == "self" {
n, err := s.Repo.CountActiveSelf(ctx, userID)
if err != nil {
return nil, err
}
if n > 0 {
return nil, ErrSelfExists
}
}
name := in.DisplayName
if name == "" {
if in.Relation == "self" {
name = "我"
} else {
name = "TA"
}
}
name, err := textsafe.Check(textsafe.DisplayName, name)
if err != nil {
return nil, err
}
p, err := s.Repo.Create(ctx, userID, in.Relation, name, in.BirthDate, in.RelationType, in.BirthTime, in.BirthPlace)
if err != nil {
return nil, err
}
if s.Bootstrap != nil {
s.Bootstrap.GenerateForProfile(ctx, userID, p)
}
return p, nil
}
// List returns user's profiles.
func (s *Service) List(ctx context.Context, userID uuid.UUID) ([]model.Profile, error) {
return s.Repo.ListByUser(ctx, userID)
}
// UpdateInput for PATCH /profiles/:id.
type UpdateInput struct {
DisplayName string
BirthDate time.Time
RelationType *string
BirthTime *string
BirthPlace *string
GeoLat *float64
GeoLng *float64
GeoVisible *bool
}
// Update patches an owned profile.
func (s *Service) Update(ctx context.Context, userID, profileID uuid.UUID, in UpdateInput) (*model.Profile, error) {
cur, err := s.Repo.GetForUser(ctx, userID, profileID)
if err != nil {
return nil, errors.New("profile not found")
}
name := strings.TrimSpace(in.DisplayName)
if name == "" {
name = cur.DisplayName
} else {
var err error
name, err = textsafe.Check(textsafe.DisplayName, name)
if err != nil {
return nil, err
}
}
birth := in.BirthDate
if birth.IsZero() {
birth = cur.BirthDate
}
rt := in.RelationType
if rt == nil {
rt = cur.RelationType
}
bt := in.BirthTime
if bt == nil {
bt = cur.BirthTime
}
bp := in.BirthPlace
if bp == nil {
bp = cur.BirthPlace
}
birthChanged := !in.BirthDate.IsZero() && !sameDay(in.BirthDate, cur.BirthDate)
timeChanged := in.BirthTime != nil && strPtr(in.BirthTime) != strPtr(cur.BirthTime)
placeChanged := in.BirthPlace != nil && strPtr(in.BirthPlace) != strPtr(cur.BirthPlace)
p, err := s.Repo.UpdateForUser(ctx, userID, profileID, name, birth, rt, bt, bp, in.GeoLat, in.GeoLng, in.GeoVisible)
if err != nil {
return nil, err
}
if s.Bootstrap != nil && (birthChanged || timeChanged || placeChanged) {
s.Bootstrap.GenerateForProfile(ctx, userID, p)
}
return p, nil
}
func sameDay(a, b time.Time) bool {
ay, am, ad := a.Date()
by, bm, bd := b.Date()
return ay == by && am == bm && ad == bd
}
func strPtr(p *string) string {
if p == nil {
return ""
}
return *p
}
// Delete soft-deletes an owned profile.
func (s *Service) Delete(ctx context.Context, userID, profileID uuid.UUID) error {
if _, err := s.Repo.GetForUser(ctx, userID, profileID); err != nil {
return errors.New("profile not found")
}
if s.Reports != nil {
_ = s.Reports.SoftDeleteForProfile(ctx, userID, profileID)
}
return s.Repo.SoftDeleteForUser(ctx, userID, profileID)
}