114 lines
3.3 KiB
Go
114 lines
3.3 KiB
Go
|
|
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
|
|||
|
|
}
|