Files
jiang13-bbs/backend/handler/user.go
freefire 4ae57d9392 feat: 用户徽章与等级体系,积分经济管理重构
- 新增徽章系统:badge 服务与 handler、管理端配置页、BadgeChip/LevelBadge 展示组件
- 新增等级模型:model/level 及前端 levels 元数据
- 管理端经济模块拆分为 points/badges/levels 独立页面,EconomySettings 迁入设置
- 积分、签到、评论等服务适配等级与徽章逻辑
2026-09-25 16:38:29 +08:00

208 lines
5.7 KiB
Go
Raw 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"))
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)
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)
// 签到积分概览(失败不阻塞资料返回)
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
}
// 徽章墙(失败不阻塞资料返回)
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,
},
"posts": nonNilSlice(posts),
"posts_total": postsTotal,
"page": page,
"size": size,
})
}
// 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,
})
}