package logic import ( "context" "strings" "time" "spark-server/internal/module/user/entity" "spark-server/internal/pkg/errs" "spark-server/internal/pkg/i18n" "spark-server/internal/pkg/reqctx" "spark-server/internal/pkg/validate" ) // Profile is the public account view. type Profile struct { UID int64 `json:"uid"` SparkID string `json:"spark_id"` Nickname string `json:"nickname"` Avatar string `json:"avatar"` QRCode string `json:"qrcode"` Birthday string `json:"birthday"` Gender int `json:"gender"` Status int `json:"status"` LastLoginTime int64 `json:"last_login_time"` LastActiveTime int64 `json:"last_active_time"` CreateTime int64 `json:"create_time"` UpdateTime int64 `json:"update_time"` } func PublicProfile(u *entity.User) *Profile { if u == nil { return nil } birthday := "" if !u.Birthday.IsZero() { birthday = u.Birthday.Format("2006-01-02") } return &Profile{ UID: u.ID, SparkID: u.SparkID, Nickname: u.Nickname, Avatar: u.Avatar, QRCode: u.QRCode, Birthday: birthday, Gender: u.Gender, Status: u.Status, LastLoginTime: unixMS(u.LastLoginAt), LastActiveTime: unixMS(u.LastActiveAt), CreateTime: unixMS(u.CreatedAt), UpdateTime: unixMS(u.UpdatedAt), } } func unixMS(t time.Time) int64 { if t.IsZero() { return 0 } return t.UnixMilli() } func (s *Service) Get(ctx context.Context, id int64) (*entity.User, error) { if s == nil || s.repo == nil || id <= 0 { return nil, errs.ErrNotFound } return s.repo.GetByID(ctx, id) } func (s *Service) GetByQRCode(ctx context.Context, qrcode string) (*entity.User, error) { if s == nil || s.repo == nil || !entity.ValidQRCode(qrcode) { return nil, errs.ErrNotFound } return s.repo.GetByQRCode(ctx, qrcode) } type UpdateSparkIDInput struct { SparkID string `json:"spark_id" validate:"required,min=6,max=32" label:"field.spark_id" errmsg:"required:validate.user.spark_id_required;min:validate.user.spark_id_invalid;max:validate.user.spark_id_invalid"` } // UpdateProfileInput is a partial profile PATCH. Nil fields are left unchanged. type UpdateProfileInput struct { SparkID *string `json:"spark_id" validate:"omitempty,min=6,max=32" label:"field.spark_id" errmsg:"min:validate.user.spark_id_invalid;max:validate.user.spark_id_invalid"` Nickname *string `json:"nickname" validate:"omitempty,min=1,max=64" label:"field.nickname" errmsg:"min:validate.user.nickname_invalid;max:validate.user.nickname_invalid"` Avatar *string `json:"avatar" validate:"omitempty,max=512" label:"field.avatar" errmsg:"max:validate.user.avatar_invalid"` Gender *int `json:"gender" validate:"omitempty,oneof=0 1 2" label:"field.gender" errmsg:"oneof:validate.user.gender_invalid"` Birthday *string `json:"birthday" validate:"omitempty" label:"field.birthday"` } func (s *Service) UpdateSparkID(ctx context.Context, userID int64, sparkID string) (*entity.User, error) { id := sparkID return s.UpdateProfile(ctx, userID, UpdateProfileInput{SparkID: &id}) } func (s *Service) UpdateProfile(ctx context.Context, userID int64, in UpdateProfileInput) (*entity.User, error) { if s == nil || s.repo == nil || userID <= 0 { return nil, errs.ErrUnauthorized } if err := validate.StructLang(reqctx.Lang(ctx), in); err != nil { return nil, err } u, err := s.repo.GetByID(ctx, userID) if err != nil { return nil, err } if !profilePatchSet(&in) { return u, nil } if err := applyProfilePatch(ctx, u, &in); err != nil { return nil, err } u.UpdatedAt = time.Now() if err := s.repo.UpdateProfile(ctx, u); err != nil { return nil, err } return u, nil } func profilePatchSet(in *UpdateProfileInput) bool { if in == nil { return false } return in.SparkID != nil || in.Nickname != nil || in.Avatar != nil || in.Gender != nil || in.Birthday != nil } func applyProfilePatch(ctx context.Context, u *entity.User, in *UpdateProfileInput) error { if in == nil { return nil } lang := reqctx.Lang(ctx) if in.SparkID != nil { sparkID := strings.TrimSpace(*in.SparkID) if !entity.ValidSparkID(sparkID) { return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeyUserSparkIDInvalid, nil)) } u.SparkID = sparkID } if in.Nickname != nil { nick := strings.TrimSpace(*in.Nickname) if nick == "" || len(nick) > 64 { return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeyUserNicknameInvalid, nil)) } u.Nickname = nick } if in.Avatar != nil { avatar := strings.TrimSpace(*in.Avatar) if len(avatar) > 512 { return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeyUserAvatarInvalid, nil)) } u.Avatar = avatar } if in.Gender != nil { if !entity.ValidGender(*in.Gender) { return errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeyUserGenderInvalid, nil)) } u.Gender = *in.Gender } if in.Birthday != nil { day, err := parseBirthday(lang, *in.Birthday) if err != nil { return err } u.Birthday = day } return nil } func parseBirthday(lang i18n.Lang, raw string) (time.Time, error) { raw = strings.TrimSpace(raw) if raw == "" { return time.Time{}, nil } day, err := time.ParseInLocation("2006-01-02", raw, time.Local) if err != nil { return time.Time{}, errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeyUserBirthdayInvalid, nil)) } today := time.Now().In(time.Local) if day.After(today) || day.Year() < 1900 { return time.Time{}, errs.ErrInvalidArgument.WithMsg(i18n.T(lang, i18n.KeyUserBirthdayInvalid, nil)) } return day, nil }