Files
SproutGate/sproutgate-backend/internal/handlers/public_profile.go
2026-05-13 12:19:36 +08:00

186 lines
5.8 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 handlers
import (
"errors"
"net/http"
"sort"
"strings"
"github.com/gin-gonic/gin"
"sproutgate-backend/internal/auth"
"sproutgate-backend/internal/models"
"sproutgate-backend/internal/storage"
)
// ListPublicUsers
// @Summary 公开用户目录
// @Tags public
// @Produce json
// @Success 200 {object} map[string]interface{} "total、users"
// @Failure 500 {object} map[string]interface{}
// @Router /api/public/users [get]
func (h *Handler) ListPublicUsers(c *gin.Context) {
users, err := h.store.ListUsers()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load users"})
return
}
out := make([]models.PublicUserListEntry, 0, len(users))
for _, u := range users {
if u.Banned {
continue
}
out = append(out, u.PublicListEntry())
}
sort.Slice(out, func(i, j int) bool {
return strings.TrimSpace(out[i].CreatedAt) < strings.TrimSpace(out[j].CreatedAt)
})
c.JSON(http.StatusOK, gin.H{"total": len(out), "users": out})
}
// GetPublicUser
// @Summary 公开用户主页
// @Description 可选 Authorization登录用户可看到点赞状态等扩展字段。
// @Tags public
// @Produce json
// @Param account path string true "账号"
// @Param Authorization header string false "Bearer可选"
// @Success 200 {object} map[string]interface{} "user、profileLikeCount 等"
// @Failure 400 {object} map[string]interface{}
// @Failure 404 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /api/public/users/{account} [get]
func (h *Handler) GetPublicUser(c *gin.Context) {
account := strings.TrimSpace(c.Param("account"))
if account == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "account is required"})
return
}
users, err := h.store.ListUsers()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load users"})
return
}
for _, user := range users {
if strings.EqualFold(strings.TrimSpace(user.Account), account) {
if user.Banned {
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
return
}
likeCount, err := h.store.CountProfileLikes(user.Account)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load likes"})
return
}
payload := gin.H{
"user": user.PublicProfile(),
"profileLikeCount": likeCount,
}
if token := bearerToken(c.GetHeader("Authorization")); token != "" {
claims, err := auth.ParseToken(h.store.JWTSecret(), h.store.JWTIssuer(), token)
if err == nil {
viewer, vFound, vErr := h.store.GetUser(claims.Account)
if vErr == nil && vFound && !viewer.Banned && claims.TokenEpoch == viewer.TokenEpoch {
if strings.EqualFold(viewer.Account, user.Account) {
payload["viewerIsOwner"] = true
} else {
has, _ := h.store.ViewerHasLikedProfileToday(viewer.Account, user.Account)
payload["viewerHasLikedToday"] = has
rem, _ := h.store.ProfileLikeRemainingToday(viewer.Account)
payload["viewerLikesRemainingToday"] = rem
payload["profileLikeDailyMax"] = storage.MaxProfileLikesPerDay
}
}
}
}
c.JSON(http.StatusOK, payload)
return
}
}
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
}
// PostPublicProfileLike
// @Summary 主页点赞
// @Tags public
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param account path string true "被点赞用户账号"
// @Success 200 {object} map[string]interface{}
// @Failure 400 {object} map[string]interface{}
// @Failure 401 {object} map[string]interface{}
// @Failure 404 {object} map[string]interface{}
// @Failure 500 {object} map[string]interface{}
// @Router /api/public/users/{account}/like [post]
func (h *Handler) PostPublicProfileLike(c *gin.Context) {
likedParam := strings.TrimSpace(c.Param("account"))
if likedParam == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "account is required"})
return
}
token := bearerToken(c.GetHeader("Authorization"))
if token == "" {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
return
}
claims, err := auth.ParseToken(h.store.JWTSecret(), h.store.JWTIssuer(), token)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
likerUser, found, err := h.store.GetUser(claims.Account)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load user"})
return
}
if !found {
c.JSON(http.StatusUnauthorized, gin.H{"error": "user not found"})
return
}
if abortIfUserBanned(c, likerUser) {
return
}
if abortIfTokenEpochStale(c, claims, likerUser) {
return
}
newCount, err := h.store.AddProfileLike(likerUser.Account, likedParam)
if err != nil {
switch {
case errors.Is(err, storage.ErrCannotLikeOwnProfile):
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
case errors.Is(err, storage.ErrProfileLikeTarget):
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
return
case errors.Is(err, storage.ErrAlreadyLikedToday):
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
case errors.Is(err, storage.ErrDailyLikeLimitReached):
rem, _ := h.store.ProfileLikeRemainingToday(likerUser.Account)
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
"viewerLikesRemainingToday": rem,
"profileLikeDailyMax": storage.MaxProfileLikesPerDay,
})
return
case errors.Is(err, storage.ErrProfileLikeLiker):
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
return
default:
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
}
rem, _ := h.store.ProfileLikeRemainingToday(likerUser.Account)
c.JSON(http.StatusOK, gin.H{
"profileLikeCount": newCount,
"viewerHasLikedToday": true,
"viewerLikesRemainingToday": rem,
"profileLikeDailyMax": storage.MaxProfileLikesPerDay,
})
}