186 lines
5.8 KiB
Go
186 lines
5.8 KiB
Go
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,
|
||
})
|
||
}
|