package service import ( "time" "github.com/sundynix/pets-be/internal/model" ) // UserThemes 主页可选主题色。存 key 不存色值——色值属于前端表现层, // 存进库以后想调色就得改数据;而且 key 还能保证前端只渲染已知的几种。 var UserThemes = map[string]bool{ "warm": true, "mint": true, "sky": true, "violet": true, "ink": true, } const defaultUserTheme = "warm" // PetCardBrief 主页宠物名片墙上的一只 type PetCardBrief struct { ID string `json:"id"` Name string `json:"name"` Emoji string `json:"emoji"` AvatarURL string `json:"avatar_url"` Type string `json:"type"` Stage string `json:"stage"` Age string `json:"age"` } // KeepStats 养宠数据。三个数都按「这个人名下所有宠物」合并算, // 不按单只——主页展示的是这个铲屎官本人,不是某一只宠物 type KeepStats struct { Days int `json:"days"` // 养宠天数:最早那只的建档日至今 Records int `json:"records"` // 累计记录条数 ActDays int `json:"act_days"` // 有记录的天数 Streak int `json:"streak"` // 连续打卡天数(截至今天或昨天) PetCount int `json:"pet_count"` // 在养几只 } // UserCard 用户公开资料。关注这件事之前只做了「点一下关注」, // 关注完既看不到对方主页、也看不到自己关注了谁——等于关注完就石沉大海。 type UserCard struct { ID string `json:"id"` Nickname string `json:"nickname"` AvatarURL string `json:"avatar_url"` IsBot bool `json:"is_bot"` IsSelf bool `json:"is_self"` Followed bool `json:"followed"` // 我有没有关注 TA Posts int64 `json:"post_count"` Following int64 `json:"following_count"` Followers int64 `json:"follower_count"` // —— 装扮 ——(列表接口不填,只有主页详情填) Bio string `json:"bio,omitempty"` BgURL string `json:"bg_url,omitempty"` Theme string `json:"theme,omitempty"` ShowPets bool `json:"show_pets"` Pets []PetCardBrief `json:"pets,omitempty"` Stats *KeepStats `json:"stats,omitempty"` } // GetUserCard 取某人的公开资料 + 三个计数 + 我是否已关注 func (s *Service) GetUserCard(viewerID, userID string) (*UserCard, error) { var u model.User if err := s.db.First(&u, "id = ?", userID).Error; err != nil { return nil, ErrNotFound } card := &UserCard{ ID: u.ID, Nickname: u.Nickname, IsBot: u.IsBot, IsSelf: u.ID == viewerID, AvatarURL: s.fileURL(u.AvatarFileID), } if card.AvatarURL == "" { card.AvatarURL = u.Avatar } s.db.Model(&model.Post{}).Where("user_id = ? AND status = ?", userID, "published").Count(&card.Posts) s.db.Model(&model.Follow{}).Where("follower_id = ?", userID).Count(&card.Following) s.db.Model(&model.Follow{}).Where("followee_id = ?", userID).Count(&card.Followers) if !card.IsSelf { var n int64 s.db.Model(&model.Follow{}). Where("follower_id = ? AND followee_id = ?", viewerID, userID).Count(&n) card.Followed = n > 0 } card.Bio = u.Bio card.BgURL = s.fileURL(u.BgFileID) card.Theme = u.Theme if !UserThemes[card.Theme] { card.Theme = defaultUserTheme } card.ShowPets = !u.HidePets stats, pets := s.keepStats(userID, card.ShowPets) card.Stats = stats card.Pets = pets return card, nil } // keepStats 养宠数据 + (可选)宠物名片墙。 // 两件事合在一个方法里是因为它们查的是同一批 pets,分开写要多查一次。 func (s *Service) keepStats(userID string, withPets bool) (*KeepStats, []PetCardBrief) { var pets []model.Pet s.db.Where("user_id = ?", userID).Order("id asc").Find(&pets) st := &KeepStats{PetCount: len(pets)} cards := []PetCardBrief{} if len(pets) == 0 { return st, cards } ids := make([]string, 0, len(pets)) for _, p := range pets { ids = append(ids, p.ID) if withPets { cards = append(cards, PetCardBrief{ ID: p.ID, Name: p.Name, Emoji: p.Emoji, Type: p.Type, Stage: p.Stage, Age: p.Age, AvatarURL: s.fileURL(p.AvatarFileID), }) } } if !withPets { cards = nil } // 养宠天数按最早那只的到家日算(没填到家日就退回建档日),和首页 // 「一起生活的第 N 天」口径一致。用建档日会因为重新建档而被重置成 1,很怪 earliest := time.Now() for _, p := range pets { start := p.CreatedAt if p.ArrivedAt != nil && !p.ArrivedAt.IsZero() { start = *p.ArrivedAt } if start.Before(earliest) { earliest = start } } st.Days = int(time.Since(earliest).Hours()/24) + 1 var total int64 s.db.Model(&model.HealthRecord{}).Where("pet_id IN ?", ids).Count(&total) st.Records = int(total) // 有记录的日期,倒序。连续打卡从今天(或昨天,当天还没记也不该断)往回数 var days []string s.db.Model(&model.HealthRecord{}). Where("pet_id IN ?", ids). Distinct("DATE(occurred_at) as d"). Order("d desc"). Pluck("d", &days) st.ActDays = len(days) has := make(map[string]bool, len(days)) for _, d := range days { if len(d) > 10 { d = d[:10] // 有的驱动把 DATE() 读回来带时间部分 } has[d] = true } cur := time.Now() if !has[cur.Format("2006-01-02")] { cur = cur.AddDate(0, 0, -1) // 今天还没记不算断签,从昨天起算 } for has[cur.Format("2006-01-02")] { st.Streak++ cur = cur.AddDate(0, 0, -1) } return st, cards } // UpdateDecoration 改主页装扮。只认这四个字段,nil 表示不动 func (s *Service) UpdateDecoration(userID string, bio, bgFileID, theme *string, showPets *bool) (*model.User, error) { fields := map[string]any{} if bio != nil { b := []rune(*bio) if len(b) > 60 { b = b[:60] } fields["bio"] = string(b) } if bgFileID != nil { fields["bg_file_id"] = *bgFileID } if theme != nil { if !UserThemes[*theme] { return nil, ErrInvalidParam } fields["theme"] = *theme } if showPets != nil { fields["hide_pets"] = !*showPets } if len(fields) == 0 { return s.GetUser(userID) } return s.UpdateUser(userID, fields) } // ListUserPosts 某人发布的帖子 func (s *Service) ListUserPosts(viewerID, userID string, offset, limit int) ([]model.Post, int64, error) { q := s.db.Model(&model.Post{}).Where("user_id = ?", userID) if viewerID == userID { // 看自己:待审核/未通过的也要能看到(带状态展示),只藏软删除的 q = q.Where("status <> ?", model.PostDeleted) } else { // 看别人:只放已过审的 q = q.Where("status = ?", model.PostPublished) } var total int64 if err := q.Count(&total).Error; err != nil { return nil, 0, err } var posts []model.Post if err := q.Order("id desc").Offset(offset).Limit(limit).Find(&posts).Error; err != nil { return nil, 0, err } s.attachPostImages(posts) s.attachCommentCounts(posts) s.markFollowed(viewerID, posts) return posts, total, nil } // ListRelations 关注列表 / 粉丝列表。 // kind = "following" 我关注的人;"followers" 关注我的人 func (s *Service) ListRelations(viewerID, userID, kind string, offset, limit int) ([]UserCard, int64, error) { q := s.db.Model(&model.Follow{}) var idCol string if kind == "followers" { q = q.Where("followee_id = ?", userID) idCol = "follower_id" } else { q = q.Where("follower_id = ?", userID) idCol = "followee_id" } var total int64 if err := q.Count(&total).Error; err != nil { return nil, 0, err } var ids []string if err := q.Order("id desc").Offset(offset).Limit(limit).Pluck(idCol, &ids).Error; err != nil { return nil, 0, err } if len(ids) == 0 { return []UserCard{}, total, nil } var users []model.User s.db.Where("id IN ?", ids).Find(&users) byID := map[string]model.User{} for _, u := range users { byID[u.ID] = u } // 一次查出我关注了这批人里的哪些,不要每行一次查询 var mine []string s.db.Model(&model.Follow{}).Where("follower_id = ? AND followee_id IN ?", viewerID, ids). Pluck("followee_id", &mine) followed := map[string]bool{} for _, id := range mine { followed[id] = true } // 按 ids 的顺序输出,保持「最近关注的在前」 out := make([]UserCard, 0, len(ids)) for _, id := range ids { u, ok := byID[id] if !ok { continue } avatar := s.fileURL(u.AvatarFileID) if avatar == "" { avatar = u.Avatar } out = append(out, UserCard{ ID: u.ID, Nickname: u.Nickname, IsBot: u.IsBot, AvatarURL: avatar, IsSelf: u.ID == viewerID, Followed: followed[u.ID], }) } return out, total, nil }