feat: 补齐关注的入口——用户主页 + 关注/粉丝列表
关注之前只做了一半:能从帖子上点「+关注」,也有「关注」信息流,但 关注完就石沉大海——没有用户主页、看不到自己关注了谁、也不知道谁关注 了自己。功能是通的,路是断的。 后端新增: - GET /api/users/:id/profile 公开资料 + 帖子/关注/粉丝三个计数 + 我是否已关注 - GET /api/users/:id/posts TA 发布的帖子 - GET /api/users/:id/relations?kind=following|followers - /api/user/summary 补 following / followers,否则「我的」页那两行永远是 0 关系列表里「我是否关注了这批人」用一次 IN 查询查完,不是每行一次。 小程序端新增两个页面: - pages/user/user 用户主页:头像/昵称/三项计数/关注按钮 + TA 的帖子 - pages/relations 关注列表与粉丝列表(同一页,kind 区分) 入口: - 社区点头像或昵称 → TA 的主页 - 主页点「关注/粉丝」数字 → 对应列表 - 「我的」页新增「我的关注」「我的粉丝」两行 关注按钮先本地翻转再发请求,失败回滚——等接口返回按钮才变会显得很迟钝。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -182,3 +182,40 @@ func (h *Handler) ListReplies(c *gin.Context) {
|
||||
}
|
||||
response.OK(c, list)
|
||||
}
|
||||
|
||||
// UserCard GET /api/users/:id/profile 某人的公开资料
|
||||
func (h *Handler) UserCard(c *gin.Context) {
|
||||
card, err := h.svc.GetUserCard(middleware.UserID(c), idParam(c, "id"))
|
||||
if err != nil {
|
||||
respondErr(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, card)
|
||||
}
|
||||
|
||||
// UserPosts GET /api/users/:id/posts 某人发布的帖子
|
||||
func (h *Handler) UserPosts(c *gin.Context) {
|
||||
var req adminListReq
|
||||
_ = c.ShouldBindQuery(&req)
|
||||
req.Normalize()
|
||||
posts, total, err := h.svc.ListUserPosts(middleware.UserID(c), idParam(c, "id"), req.Offset(), req.Limit())
|
||||
if err != nil {
|
||||
response.FailErr(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, response.NewPage(posts, total, req.PageQuery))
|
||||
}
|
||||
|
||||
// Relations GET /api/users/:id/relations?kind=following|followers
|
||||
func (h *Handler) Relations(c *gin.Context) {
|
||||
var req adminListReq
|
||||
_ = c.ShouldBindQuery(&req)
|
||||
req.Normalize()
|
||||
kind := c.Query("kind")
|
||||
list, total, err := h.svc.ListRelations(middleware.UserID(c), idParam(c, "id"), kind, req.Offset(), req.Limit())
|
||||
if err != nil {
|
||||
response.FailErr(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, response.NewPage(list, total, req.PageQuery))
|
||||
}
|
||||
|
||||
@@ -88,6 +88,9 @@ func registerUserAPI(api *gin.RouterGroup, h *handler.Handler, jm *appjwt.Manage
|
||||
g.GET("/pets/:id/health-summary", h.HealthSummary)
|
||||
g.GET("/pets/:id/poster", h.Poster)
|
||||
|
||||
g.GET("/users/:id/profile", h.UserCard)
|
||||
g.GET("/users/:id/posts", h.UserPosts)
|
||||
g.GET("/users/:id/relations", h.Relations)
|
||||
g.POST("/users/:id/follow", h.FollowUser)
|
||||
g.DELETE("/users/:id/follow", h.UnfollowUser)
|
||||
|
||||
|
||||
@@ -216,6 +216,8 @@ type UserSummary struct {
|
||||
Pets int64 `json:"pets"`
|
||||
Records int64 `json:"records"`
|
||||
Reminders int64 `json:"reminders"`
|
||||
Following int64 `json:"following"` // 我关注了多少人
|
||||
Followers int64 `json:"followers"` // 多少人关注我
|
||||
ProStatus string `json:"pro_status"`
|
||||
}
|
||||
|
||||
@@ -225,6 +227,8 @@ func (s *Service) GetUserSummary(userID string) (*UserSummary, error) {
|
||||
s.db.Model(&model.Pet{}).Where("user_id = ?", userID).Count(&sum.Pets)
|
||||
s.db.Model(&model.HealthRecord{}).Where("user_id = ?", userID).Count(&sum.Records)
|
||||
s.db.Model(&model.Reminder{}).Where("user_id = ?", userID).Count(&sum.Reminders)
|
||||
s.db.Model(&model.Follow{}).Where("follower_id = ?", userID).Count(&sum.Following)
|
||||
s.db.Model(&model.Follow{}).Where("followee_id = ?", userID).Count(&sum.Followers)
|
||||
|
||||
sum.ProStatus = model.ProNone
|
||||
var m model.ProMembership
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"github.com/sundynix/pets-be/internal/model"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
return card, nil
|
||||
}
|
||||
|
||||
// ListUserPosts 某人发布的帖子
|
||||
func (s *Service) ListUserPosts(viewerID, userID string, offset, limit int) ([]model.Post, int64, error) {
|
||||
q := s.db.Model(&model.Post{}).Where("user_id = ? AND status = ?", userID, "published")
|
||||
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.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
|
||||
}
|
||||
Reference in New Issue
Block a user