package service import ( "encoding/json" "strings" "gorm.io/gorm" "nl-pms-api/internal/commonservice" "nl-pms-api/internal/model" ) // ProfileService 用户公开资料。 type ProfileService struct { DB *gorm.DB } // ProfileDTO 对外资料(techTags 为数组)。 type ProfileDTO struct { Nickname string `json:"nickname"` Title string `json:"title"` Email string `json:"email"` Bio string `json:"bio"` TechTags []string `json:"techTags"` AvatarThumb string `json:"avatarThumb"` UpdatedAt string `json:"updatedAt"` } func sanitizeProfile(p *ProfileDTO) error { p.Nickname = commonservice.ClipRunes(p.Nickname, 32) p.Title = commonservice.ClipRunes(p.Title, 48) p.Email = commonservice.ClipRunes(p.Email, 128) p.Bio = commonservice.ClipRunes(p.Bio, 300) if p.Email != "" && (!strings.Contains(p.Email, "@") || strings.ContainsAny(p.Email, " \t")) { return commonservice.BadRequest("PROFILE_EMAIL_INVALID") } tags, seen := []string{}, map[string]bool{} for _, t := range p.TechTags { t = commonservice.ClipRunes(t, 24) if t == "" || seen[strings.ToLower(t)] { continue } seen[strings.ToLower(t)] = true tags = append(tags, t) if len(tags) >= 20 { break } } p.TechTags = tags return nil } // Get 读取本人资料;无记录返回空结构。 func (s *ProfileService) Get(userID int64) (*ProfileDTO, error) { var row model.UserProfile err := s.DB.Where("user_id = ?", userID).First(&row).Error if err == gorm.ErrRecordNotFound { return &ProfileDTO{TechTags: []string{}}, nil } if err != nil { return nil, commonservice.Internal("QUERY_FAILED") } return &ProfileDTO{ Nickname: row.Nickname, Title: row.Title, Email: row.Email, Bio: row.Bio, TechTags: parseTechTags(row.TechTags), AvatarThumb: row.AvatarThumb, UpdatedAt: row.UpdatedAt, }, nil } // Put LWW 写入资料:仅当传入 updatedAt 更新时覆盖。 func (s *ProfileService) Put(userID int64, p ProfileDTO) (*ProfileDTO, error) { if err := sanitizeProfile(&p); err != nil { return nil, err } if p.UpdatedAt == "" { p.UpdatedAt = commonservice.NowRFC() } tags, _ := json.Marshal(p.TechTags) var remote model.UserProfile err := s.DB.Where("user_id = ?", userID).First(&remote).Error if err == nil && remote.UpdatedAt >= p.UpdatedAt { // 远端相同或更新:返回远端,不覆盖。 return &ProfileDTO{ Nickname: remote.Nickname, Title: remote.Title, Email: remote.Email, Bio: remote.Bio, TechTags: parseTechTags(remote.TechTags), AvatarThumb: remote.AvatarThumb, UpdatedAt: remote.UpdatedAt, }, nil } q := `INSERT INTO user_profiles(user_id,nickname,title,email,bio,tech_tags,avatar_thumb,updated_at) VALUES(?,?,?,?,?,?,?,?) ON DUPLICATE KEY UPDATE nickname=VALUES(nickname),title=VALUES(title),email=VALUES(email),bio=VALUES(bio), tech_tags=VALUES(tech_tags),avatar_thumb=VALUES(avatar_thumb),updated_at=VALUES(updated_at)` if err := s.DB.Exec(q, userID, p.Nickname, p.Title, p.Email, p.Bio, string(tags), p.AvatarThumb, p.UpdatedAt).Error; err != nil { return nil, commonservice.Internal("SAVE_FAILED") } return &p, nil } func parseTechTags(s string) []string { out := []string{} if json.Unmarshal([]byte(s), &out) != nil { return []string{} } return out }