Files
jiang13-bbs/backend/handler/user.go
freefire d7c6b3a961 feat: 帖子收藏与用户关注
- 新增收藏功能:favorite 服务与 handler、FavoriteButton 组件
- 新增关注功能:follow 服务与 handler、FollowButton 组件
- 用户主页适配关注/粉丝与收藏展示
- flash 消息工具、广告与侧边栏组件微调
2026-09-26 03:02:40 +08:00

235 lines
6.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"net/http"
"strconv"
"github.com/freefire/jiang13-bbs/middleware"
"github.com/freefire/jiang13-bbs/model"
"github.com/freefire/jiang13-bbs/service"
"github.com/gin-gonic/gin"
)
// UpdateProfileRequest 更新资料请求(头像不在此通道,见 /api/upload/avatar)
type UpdateProfileRequest struct {
Nickname string `json:"nickname"`
Email string `json:"email"`
Signature string `json:"signature"`
}
// UpdateProfile 更新当前登录用户的资料(昵称、邮箱、签名)
func (h *Handlers) UpdateProfile(c *gin.Context) {
claims := middleware.CurrentUser(c)
var req UpdateProfileRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "请求参数格式错误"})
return
}
user, err := h.Auth.UpdateProfile(claims.ID, req.Nickname, req.Email, req.Signature)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 返回更新后的完整资料(含 email/signature,供前端刷新展示)
c.JSON(http.StatusOK, gin.H{
"user": gin.H{
"id": user.ID,
"username": user.Username,
"nickname": user.Nickname,
"avatar": user.Avatar,
"signature": user.Signature,
"email": user.Email,
"role": user.Role,
},
})
}
// UserProfile 获取用户资料:基本信息 + 统计 + 发帖列表(分页)
func (h *Handlers) UserProfile(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户 ID"})
return
}
user, err := h.Auth.GetUserByID(uint(id))
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
sort := c.DefaultQuery("sort", "new")
var viewerID uint
var actor *service.Actor
if claims := middleware.CurrentUser(c); claims != nil {
viewerID = claims.ID
if a, err := h.Auth.LoadActor(claims.ID); err == nil {
actor = a
}
}
posts, postsTotal, err := h.Post.ListByUser(user.ID, viewerID, actor, page, size, sort)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 填充点赞状态(仅登录用户)
if viewerID > 0 {
ids := make([]uint, 0, len(posts))
for _, p := range posts {
ids = append(ids, p.ID)
}
likedMap := h.Like.BatchHasLiked(ids, viewerID)
for i := range posts {
posts[i].Liked = likedMap[posts[i].ID]
}
}
postCount, _ := h.Post.CountByUser(user.ID)
commentCount, _ := h.Comment.CountByUser(user.ID)
favoriteCount := h.Favorite.CountByUser(user.ID)
followingCount := h.Follow.CountFollowing(user.ID)
isFollowing := viewerID > 0 && viewerID != user.ID && h.Follow.IsFollowing(viewerID, user.ID)
// 签到积分概览(失败不阻塞资料返回)
pointsTotal, streak := 0, 0
if cs, err := h.Checkin.Status(user.ID); err == nil && cs != nil {
pointsTotal = cs.TotalPoints
streak = cs.Streak
}
// 板块管理员公开其授权板块(角色徽章展示"板块管理员 · 板块名")
var boardIDs []uint
if user.Role == model.RoleBoardAdmin {
boardIDs, _ = h.Auth.GetUserBoardIDs(user.ID)
}
if boardIDs == nil {
boardIDs = []uint{}
}
userPayload := gin.H{
"id": user.ID,
"username": user.Username,
"nickname": user.Nickname,
"avatar": user.Avatar,
"signature": user.Signature,
"role": user.Role,
"board_ids": boardIDs,
"created_at": user.CreatedAt,
"level": user.Level,
"total_points": user.TotalPoints,
}
// 仅对具备用户管理权限的访客下发封禁态(避免信息公开)
if actor != nil && actor.HasPerm(service.PermUsers) {
userPayload["banned"] = user.Banned
}
// 访客与该用户的关注关系(关注按钮初始态)
userPayload["is_following"] = isFollowing
// 邮箱仅本人可见,打码展示(j***@163.com)
if viewerID == user.ID && user.Email != "" {
userPayload["email"] = maskEmail(user.Email)
}
// 徽章墙(失败不阻塞资料返回)
if h.Badge != nil {
if badges, err := h.Badge.ForUser(user.ID); err == nil && badges != nil {
userPayload["badges"] = badges
}
}
c.JSON(http.StatusOK, gin.H{
"user": userPayload,
"stats": gin.H{
"post_count": postCount,
"comment_count": commentCount,
"points": pointsTotal,
"streak": streak,
"favorite_count": favoriteCount,
"following_count": followingCount,
},
"posts": nonNilSlice(posts),
"posts_total": postsTotal,
"page": page,
"size": size,
})
}
// maskEmail 邮箱打码:保留首字符与 @ 后域名(j***@163.com)
func maskEmail(email string) string {
at := -1
for i, r := range email {
if r == '@' {
at = i
break
}
}
if at <= 0 {
return "***"
}
return email[:1] + "***" + email[at:]
}
// UserComments 获取用户发表的评论列表(分页,含帖子标题)
func (h *Handlers) UserComments(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的用户 ID"})
return
}
// 校验用户存在
if _, err := h.Auth.GetUserByID(uint(id)); err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
if allow, err := h.Setting.AllowComments(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "评论暂时不可用"})
return
} else if !allow {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
c.JSON(http.StatusOK, gin.H{
"comments": []any{},
"total": 0,
"page": page,
"size": size,
})
return
}
if requireLogin, err := h.Setting.CommentsRequireLogin(); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "评论暂时不可用"})
return
} else if requireLogin && middleware.CurrentUser(c) == nil {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
c.JSON(http.StatusOK, gin.H{
"comments": []any{},
"total": 0,
"page": page,
"size": size,
"require_login": true,
})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
comments, total, err := h.Comment.ListByUser(uint(id), page, size)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"comments": nonNilSlice(comments),
"total": total,
"page": page,
"size": size,
})
}