初始提交:姜十三论坛 Jiang13 Forum
轻量自用论坛,Go 单二进制 + React SPA 内嵌 + SQLite。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
244
handler/admin.go
Normal file
244
handler/admin.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jiang13/forum/model"
|
||||
"github.com/jiang13/forum/service"
|
||||
)
|
||||
|
||||
// --- 后台管理页面 ---
|
||||
|
||||
func (h *Handlers) adminPageData(c *gin.Context, title, activeNav string, data gin.H) gin.H {
|
||||
if data == nil {
|
||||
data = gin.H{}
|
||||
}
|
||||
data["ActiveNav"] = activeNav
|
||||
return h.pageData(c, title, data)
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminLoginPage(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "admin/login.html", h.pageData(c, "后台登录", gin.H{
|
||||
"ActiveNav": "login",
|
||||
"QueryBanned": c.Query("banned"),
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminDashboard(c *gin.Context) {
|
||||
var userCount, postCount, boardCount, commentCount int64
|
||||
model.DB.Model(&model.User{}).Count(&userCount)
|
||||
model.DB.Model(&model.Post{}).Count(&postCount)
|
||||
model.DB.Model(&model.Board{}).Count(&boardCount)
|
||||
model.DB.Model(&model.Comment{}).Count(&commentCount)
|
||||
recentPosts, _, _ := h.Post.List(service.PostListQuery{Page: 1, Size: 8})
|
||||
c.HTML(http.StatusOK, "admin/dashboard.html", h.adminPageData(c, "仪表盘", "dashboard", gin.H{
|
||||
"UserCount": userCount,
|
||||
"PostCount": postCount,
|
||||
"BoardCount": boardCount,
|
||||
"CommentCount": commentCount,
|
||||
"OnlineCount": h.Online.Count(),
|
||||
"RecentPosts": recentPosts,
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminBoardsPage(c *gin.Context) {
|
||||
boards, _ := h.Board.ListWithStats()
|
||||
c.HTML(http.StatusOK, "admin/boards.html", h.adminPageData(c, "板块管理", "boards", gin.H{
|
||||
"Boards": boards,
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminPostsPage(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
keyword := strings.TrimSpace(c.Query("keyword"))
|
||||
posts, total, _ := h.Post.List(service.PostListQuery{Page: page, Size: 20, Keyword: keyword})
|
||||
c.HTML(http.StatusOK, "admin/posts.html", h.adminPageData(c, "帖子管理", "posts", gin.H{
|
||||
"Posts": posts,
|
||||
"Total": total,
|
||||
"Page": page,
|
||||
"Keyword": keyword,
|
||||
"TotalPages": calcTotalPages(total, 20),
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminCommentsPage(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
comments, total, _ := h.Comment.ListRecent(page, 20)
|
||||
c.HTML(http.StatusOK, "admin/comments.html", h.adminPageData(c, "评论管理", "comments", gin.H{
|
||||
"Comments": comments,
|
||||
"Total": total,
|
||||
"Page": page,
|
||||
"TotalPages": calcTotalPages(total, 20),
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminUsersPage(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
users, total, _ := h.User.ListUsers(page, 20)
|
||||
c.HTML(http.StatusOK, "admin/users.html", h.adminPageData(c, "用户管理", "users", gin.H{
|
||||
"Users": users,
|
||||
"Total": total,
|
||||
"Page": page,
|
||||
"TotalPages": calcTotalPages(total, 20),
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminSettingsPage(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "admin/settings.html", h.adminPageData(c, "系统设置", "settings", gin.H{
|
||||
"FilterPath": h.Cfg.FilterWordsPath(),
|
||||
"DataDir": h.Cfg.DataDir,
|
||||
"DBPath": h.Cfg.DBPath(),
|
||||
"Port": h.Cfg.Port,
|
||||
}))
|
||||
}
|
||||
|
||||
func calcTotalPages(total int64, size int) int {
|
||||
if total == 0 {
|
||||
return 1
|
||||
}
|
||||
pages := int(total) / size
|
||||
if int(total)%size > 0 {
|
||||
pages++
|
||||
}
|
||||
if pages < 1 {
|
||||
return 1
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
// --- 后台 API ---
|
||||
|
||||
func (h *Handlers) AdminAPICreateBoard(c *gin.Context) {
|
||||
sortOrder, _ := strconv.Atoi(c.PostForm("sort_order"))
|
||||
board, err := h.Board.Create(c.PostForm("name"), c.PostForm("description"), sortOrder)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "板块已创建", "id": board.ID})
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminAPIUpdateBoard(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
sortOrder, _ := strconv.Atoi(c.PostForm("sort_order"))
|
||||
if err := h.Board.Update(uint(id), c.PostForm("name"), c.PostForm("description"), sortOrder); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "板块已更新"})
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminAPIDeleteBoard(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err := h.Board.Delete(uint(id)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "板块已删除"})
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminAPIPinPost(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
pinned := c.PostForm("pinned") == "true" || c.PostForm("pinned") == "1"
|
||||
if err := h.Post.SetPinned(uint(id), pinned); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
msg := "已取消置顶"
|
||||
if pinned {
|
||||
msg = "已置顶"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": msg, "pinned": pinned})
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminAPIDeletePost(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err := h.Post.Delete(0, uint(id), true); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "帖子已删除"})
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminAPIDeleteComment(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err := h.Comment.AdminDelete(uint(id)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "评论已删除"})
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminAPIBanUser(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
banned := c.PostForm("banned") == "true" || c.PostForm("banned") == "1"
|
||||
if err := h.User.BanUser(uint(id), banned); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
msg := "已解除禁言"
|
||||
if banned {
|
||||
msg = "已禁言"
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": msg})
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminAPIBackup(c *gin.Context) {
|
||||
path, err := h.Backup.ExportSQLite()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
filename := filepath.Base(path)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "备份成功",
|
||||
"path": path,
|
||||
"filename": filename,
|
||||
"download": "/admin/api/backup/download/" + filename,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminDownloadBackup(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
if !strings.HasPrefix(name, "jiang13_backup_") || !strings.HasSuffix(name, ".db") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的备份文件名"})
|
||||
return
|
||||
}
|
||||
path := filepath.Join(h.Cfg.DataDir, name)
|
||||
c.FileAttachment(path, name)
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminAPILogin(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" form:"username" binding:"required"`
|
||||
Password string `json:"password" form:"password" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
token, user, err := h.Auth.Login(req.Username, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if user.Role != model.RoleAdmin {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员账号才能登录后台"})
|
||||
return
|
||||
}
|
||||
h.setAuthCookie(c, token)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "登录成功",
|
||||
"user": gin.H{
|
||||
"id": user.ID, "nickname": user.Nickname, "role": user.Role,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Handlers) AdminAPILogout(c *gin.Context) {
|
||||
h.APILogout(c)
|
||||
}
|
||||
237
handler/api.go
Normal file
237
handler/api.go
Normal file
@@ -0,0 +1,237 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jiang13/forum/model"
|
||||
"github.com/jiang13/forum/service"
|
||||
)
|
||||
|
||||
// APIMe 当前登录用户
|
||||
func (h *Handlers) APIMe(c *gin.Context) {
|
||||
uid := h.currentUserID(c)
|
||||
if uid == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"user": nil})
|
||||
return
|
||||
}
|
||||
user, err := h.User.GetByID(uid)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"user": nil})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"user": user,
|
||||
})
|
||||
}
|
||||
|
||||
// APIBoards 板块列表(含帖子数)
|
||||
func (h *Handlers) APIBoards(c *gin.Context) {
|
||||
boards, err := h.Board.ListWithStats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if boards == nil {
|
||||
boards = []service.BoardWithStats{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"boards": boards})
|
||||
}
|
||||
|
||||
// APIStats 论坛概览统计
|
||||
func (h *Handlers) APIStats(c *gin.Context) {
|
||||
var userCount, postCount, boardCount int64
|
||||
model.DB.Model(&model.User{}).Count(&userCount)
|
||||
model.DB.Model(&model.Post{}).Count(&postCount)
|
||||
model.DB.Model(&model.Board{}).Count(&boardCount)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"users": userCount, "posts": postCount, "boards": boardCount,
|
||||
})
|
||||
}
|
||||
|
||||
// APIAdminCreateBoard 管理员创建板块(JSON)
|
||||
func (h *Handlers) APIAdminCreateBoard(c *gin.Context) {
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
board, err := h.Board.Create(req.Name, req.Description, req.SortOrder)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "板块已创建", "board": board})
|
||||
}
|
||||
|
||||
// APIAdminUpdateBoard 管理员更新板块
|
||||
func (h *Handlers) APIAdminUpdateBoard(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
var req struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
if err := h.Board.Update(uint(id), req.Name, req.Description, req.SortOrder); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
board, _ := h.Board.GetByID(uint(id))
|
||||
c.JSON(http.StatusOK, gin.H{"message": "板块已更新", "board": board})
|
||||
}
|
||||
|
||||
// APIAdminDeleteBoard 管理员删除板块
|
||||
func (h *Handlers) APIAdminDeleteBoard(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err := h.Board.Delete(uint(id)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "板块已删除"})
|
||||
}
|
||||
|
||||
// APIPosts 帖子列表(分页)
|
||||
func (h *Handlers) APIPosts(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "30"))
|
||||
boardID, _ := strconv.ParseUint(c.Query("board_id"), 10, 64)
|
||||
keyword := c.Query("keyword")
|
||||
|
||||
q := service.PostListQuery{
|
||||
BoardID: uint(boardID),
|
||||
Page: page,
|
||||
Size: size,
|
||||
Keyword: keyword,
|
||||
}
|
||||
items, total, err := h.Post.ListItems(q)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if items == nil {
|
||||
items = []service.PostListItem{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"posts": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"size": size,
|
||||
"has_more": int64(page*size) < total,
|
||||
})
|
||||
}
|
||||
|
||||
// APIPostDetail 帖子详情
|
||||
func (h *Handlers) APIPostDetail(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
post, err := h.Post.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "帖子不存在"})
|
||||
return
|
||||
}
|
||||
uid := h.currentUserID(c)
|
||||
if uid == 0 {
|
||||
post.Content = service.RedactMembersOnlyHTML(post.Content)
|
||||
}
|
||||
comments, _ := h.Comment.ListByPost(uint(id), uid, h.isAdmin(c), post.UserID, h.parseGuestCommentIDs(c))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"post": post,
|
||||
"comment_count": len(comments),
|
||||
"liked": h.Post.IsLiked(uid, uint(id)),
|
||||
"favorited": h.Post.IsFavorited(uid, uint(id)),
|
||||
})
|
||||
}
|
||||
|
||||
// APIPostComments 楼层列表
|
||||
func (h *Handlers) APIPostComments(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
post, err := h.Post.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "帖子不存在"})
|
||||
return
|
||||
}
|
||||
comments, err := h.Comment.ListByPost(uint(id), h.currentUserID(c), h.isAdmin(c), post.UserID, h.parseGuestCommentIDs(c))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if comments == nil {
|
||||
comments = []model.Comment{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"comments": comments, "total": len(comments)})
|
||||
}
|
||||
|
||||
// APIHotPosts 热门 TOP
|
||||
func (h *Handlers) APIHotPosts(c *gin.Context) {
|
||||
items, err := h.Post.HotPosts(10)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"posts": items})
|
||||
}
|
||||
|
||||
// APINotifications 最新动态通知
|
||||
func (h *Handlers) APINotifications(c *gin.Context) {
|
||||
posts, _, _ := h.Post.List(service.PostListQuery{Page: 1, Size: 8})
|
||||
type notice struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
list := make([]notice, 0, len(posts))
|
||||
for _, p := range posts {
|
||||
list = append(list, notice{
|
||||
ID: p.ID, Title: p.Title, Type: "post",
|
||||
CreatedAt: p.CreatedAt.Format("01-02 15:04"),
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"notifications": list})
|
||||
}
|
||||
|
||||
// APIOnline 当前浏览统计
|
||||
func (h *Handlers) APIOnline(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"count": h.Online.Count(),
|
||||
"members": h.Online.CountMembers(),
|
||||
"guests": h.Online.CountGuests(),
|
||||
"users": h.Online.List(20),
|
||||
})
|
||||
}
|
||||
|
||||
// APIPresence 上报浏览心跳(会员与游客均可)
|
||||
func (h *Handlers) APIPresence(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"count": h.Online.Count(),
|
||||
"members": h.Online.CountMembers(),
|
||||
"guests": h.Online.CountGuests(),
|
||||
})
|
||||
}
|
||||
|
||||
// APIFavorites 我的收藏
|
||||
func (h *Handlers) APIFavorites(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
size, _ := strconv.Atoi(c.DefaultQuery("size", "20"))
|
||||
favs, total, err := h.Post.ListFavorites(h.currentUserID(c), page, size)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if favs == nil {
|
||||
favs = []model.PostFavorite{}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"favorites": favs, "total": total, "page": page})
|
||||
}
|
||||
|
||||
func (h *Handlers) APIPing(c *gin.Context) {
|
||||
h.APIPresence(c)
|
||||
}
|
||||
343
handler/handlers.go
Normal file
343
handler/handlers.go
Normal file
@@ -0,0 +1,343 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/jiang13/forum/config"
|
||||
"github.com/jiang13/forum/middleware"
|
||||
"github.com/jiang13/forum/model"
|
||||
"github.com/jiang13/forum/service"
|
||||
)
|
||||
|
||||
// Handlers 聚合所有 HTTP 处理器
|
||||
type Handlers struct {
|
||||
Cfg *config.Config
|
||||
Auth *service.AuthService
|
||||
User *service.UserService
|
||||
Board *service.BoardService
|
||||
Post *service.PostService
|
||||
Comment *service.CommentService
|
||||
Backup *service.BackupService
|
||||
Filter *service.SensitiveFilter
|
||||
Limiter *service.RateLimiter
|
||||
Online *service.OnlineService
|
||||
}
|
||||
|
||||
func (h *Handlers) setAuthCookie(c *gin.Context, token string) {
|
||||
c.SetCookie(middleware.CookieName, token, int(service.TokenExpire.Seconds()), "/", "", false, true)
|
||||
}
|
||||
|
||||
func (h *Handlers) currentUserID(c *gin.Context) uint {
|
||||
if v, ok := c.Get(middleware.CtxUserID); ok {
|
||||
return v.(uint)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (h *Handlers) isAdmin(c *gin.Context) bool {
|
||||
if v, ok := c.Get(middleware.CtxRole); ok {
|
||||
return v == model.RoleAdmin
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *Handlers) parseGuestCommentIDs(c *gin.Context) []uint {
|
||||
raw := c.Query("my_ids")
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
var ids []uint
|
||||
for _, part := range strings.Split(raw, ",") {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
v, err := strconv.ParseUint(part, 10, 64)
|
||||
if err == nil && v > 0 {
|
||||
ids = append(ids, uint(v))
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
func (h *Handlers) pageData(c *gin.Context, title string, data gin.H) gin.H {
|
||||
if data == nil {
|
||||
data = gin.H{}
|
||||
}
|
||||
data["Title"] = title
|
||||
data["SiteName"] = "姜十三论坛"
|
||||
data["SiteEN"] = "Jiang13 Forum"
|
||||
if uid := h.currentUserID(c); uid > 0 {
|
||||
data["CurrentUserID"] = uid
|
||||
if u, err := h.User.GetByID(uid); err == nil {
|
||||
data["CurrentUser"] = u
|
||||
}
|
||||
}
|
||||
data["IsAdmin"] = h.isAdmin(c)
|
||||
return data
|
||||
}
|
||||
|
||||
// --- 页面路由 ---
|
||||
|
||||
func (h *Handlers) IndexPage(c *gin.Context) {
|
||||
boards, _ := h.Board.List()
|
||||
posts, _, _ := h.Post.List(service.PostListQuery{Page: 1, Size: 10})
|
||||
c.HTML(http.StatusOK, "index.html", h.pageData(c, "首页", gin.H{
|
||||
"Boards": boards, "Posts": posts,
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Handlers) LoginPage(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "login.html", h.pageData(c, "登录", nil))
|
||||
}
|
||||
|
||||
func (h *Handlers) RegisterPage(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "register.html", h.pageData(c, "注册", nil))
|
||||
}
|
||||
|
||||
func (h *Handlers) BoardPage(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
board, err := h.Board.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.HTML(http.StatusNotFound, "error.html", h.pageData(c, "板块不存在", gin.H{"Message": "板块不存在"}))
|
||||
return
|
||||
}
|
||||
posts, total, _ := h.Post.List(service.PostListQuery{BoardID: uint(id), Page: page, Size: 20})
|
||||
c.HTML(http.StatusOK, "board.html", h.pageData(c, board.Name, gin.H{
|
||||
"Board": board, "Posts": posts, "Total": total, "Page": page,
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Handlers) PostPage(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
post, err := h.Post.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.HTML(http.StatusNotFound, "error.html", h.pageData(c, "帖子不存在", gin.H{"Message": "帖子不存在"}))
|
||||
return
|
||||
}
|
||||
comments, _ := h.Comment.ListByPost(uint(id), h.currentUserID(c), h.isAdmin(c), post.UserID, nil)
|
||||
uid := h.currentUserID(c)
|
||||
c.HTML(http.StatusOK, "post.html", h.pageData(c, post.Title, gin.H{
|
||||
"Post": post, "Comments": comments,
|
||||
"Liked": h.Post.IsLiked(uid, uint(id)),
|
||||
"Favorited": h.Post.IsFavorited(uid, uint(id)),
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Handlers) PostNewPage(c *gin.Context) {
|
||||
boards, _ := h.Board.List()
|
||||
c.HTML(http.StatusOK, "post_new.html", h.pageData(c, "发帖", gin.H{"Boards": boards}))
|
||||
}
|
||||
|
||||
func (h *Handlers) PostEditPage(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
post, err := h.Post.GetByID(uint(id))
|
||||
if err != nil || (!h.isAdmin(c) && post.UserID != h.currentUserID(c)) {
|
||||
c.Redirect(http.StatusFound, "/")
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, "post_edit.html", h.pageData(c, "编辑帖子", gin.H{"Post": post}))
|
||||
}
|
||||
|
||||
func (h *Handlers) ProfilePage(c *gin.Context) {
|
||||
user, _ := h.User.GetByID(h.currentUserID(c))
|
||||
c.HTML(http.StatusOK, "profile.html", h.pageData(c, "个人主页", gin.H{"ProfileUser": user}))
|
||||
}
|
||||
|
||||
func (h *Handlers) UserProfilePage(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
user, err := h.User.GetByID(uint(id))
|
||||
if err != nil {
|
||||
c.HTML(http.StatusNotFound, "error.html", h.pageData(c, "用户不存在", gin.H{"Message": "用户不存在"}))
|
||||
return
|
||||
}
|
||||
c.HTML(http.StatusOK, "user_profile.html", h.pageData(c, user.Nickname, gin.H{"ProfileUser": user}))
|
||||
}
|
||||
|
||||
func (h *Handlers) FavoritesPage(c *gin.Context) {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
favs, total, _ := h.Post.ListFavorites(h.currentUserID(c), page, 20)
|
||||
c.HTML(http.StatusOK, "favorites.html", h.pageData(c, "我的收藏", gin.H{
|
||||
"Favorites": favs, "Total": total, "Page": page,
|
||||
}))
|
||||
}
|
||||
|
||||
// --- API ---
|
||||
|
||||
func (h *Handlers) APIRegister(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" form:"username" binding:"required"`
|
||||
Password string `json:"password" form:"password" binding:"required"`
|
||||
Nickname string `json:"nickname" form:"nickname"`
|
||||
}
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
user, err := h.Auth.Register(req.Username, req.Password, req.Nickname)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
token, _, _ := h.Auth.Login(req.Username, req.Password)
|
||||
h.setAuthCookie(c, token)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "注册成功", "user_id": user.ID})
|
||||
}
|
||||
|
||||
func (h *Handlers) APILogin(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" form:"username" binding:"required"`
|
||||
Password string `json:"password" form:"password" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBind(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
|
||||
return
|
||||
}
|
||||
token, user, err := h.Auth.Login(req.Username, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
h.setAuthCookie(c, token)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "登录成功", "user": gin.H{"id": user.ID, "nickname": user.Nickname}})
|
||||
}
|
||||
|
||||
func (h *Handlers) APILogout(c *gin.Context) {
|
||||
c.SetCookie(middleware.CookieName, "", -1, "/", "", false, true)
|
||||
c.JSON(http.StatusOK, gin.H{"message": "已退出"})
|
||||
}
|
||||
|
||||
func (h *Handlers) APIUpdateProfile(c *gin.Context) {
|
||||
nickname := c.PostForm("nickname")
|
||||
if err := h.User.UpdateNickname(h.currentUserID(c), nickname); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
user, _ := h.User.GetByID(h.currentUserID(c))
|
||||
c.JSON(http.StatusOK, gin.H{"message": "昵称已更新", "user": user})
|
||||
}
|
||||
|
||||
func (h *Handlers) APIUpdatePassword(c *gin.Context) {
|
||||
oldPass := c.PostForm("old_password")
|
||||
newPass := c.PostForm("new_password")
|
||||
if err := h.User.UpdatePassword(h.currentUserID(c), oldPass, newPass); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "密码已修改"})
|
||||
}
|
||||
|
||||
func (h *Handlers) APIUploadAvatar(c *gin.Context) {
|
||||
file, err := c.FormFile("avatar")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择头像文件"})
|
||||
return
|
||||
}
|
||||
url, err := h.User.UploadAvatar(h.currentUserID(c), file, h.Cfg.UploadDir())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "头像已更新", "avatar": url})
|
||||
}
|
||||
|
||||
func (h *Handlers) APICreatePost(c *gin.Context) {
|
||||
boardID, _ := strconv.ParseUint(c.PostForm("board_id"), 10, 64)
|
||||
title := c.PostForm("title")
|
||||
content := c.PostForm("content")
|
||||
tags := c.PostForm("tags")
|
||||
post, err := h.Post.Create(h.currentUserID(c), uint(boardID), title, content, tags)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "发帖成功", "post_id": post.ID})
|
||||
}
|
||||
|
||||
func (h *Handlers) APIUpdatePost(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
err := h.Post.Update(h.currentUserID(c), uint(id), h.isAdmin(c),
|
||||
c.PostForm("title"), c.PostForm("content"), c.PostForm("tags"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "帖子已更新"})
|
||||
}
|
||||
|
||||
func (h *Handlers) APIDeletePost(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err := h.Post.Delete(h.currentUserID(c), uint(id), h.isAdmin(c)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "帖子已删除"})
|
||||
}
|
||||
|
||||
func (h *Handlers) APIToggleLike(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
liked, err := h.Post.ToggleLike(h.currentUserID(c), uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
var post model.Post
|
||||
model.DB.First(&post, id)
|
||||
c.JSON(http.StatusOK, gin.H{"liked": liked, "like_count": post.LikeCount})
|
||||
}
|
||||
|
||||
func (h *Handlers) APIToggleFavorite(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
faved, err := h.Post.ToggleFavorite(h.currentUserID(c), uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"favorited": faved})
|
||||
}
|
||||
|
||||
func (h *Handlers) APICreateComment(c *gin.Context) {
|
||||
postID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
content := c.PostForm("content")
|
||||
var replyTo *uint
|
||||
if rt := c.PostForm("reply_to"); rt != "" {
|
||||
v, _ := strconv.ParseUint(rt, 10, 64)
|
||||
u := uint(v)
|
||||
replyTo = &u
|
||||
}
|
||||
isPrivate := c.PostForm("is_private") == "1" || c.PostForm("is_private") == "true"
|
||||
uid := h.currentUserID(c)
|
||||
|
||||
in := service.CommentCreateInput{
|
||||
UserID: uid,
|
||||
PostID: uint(postID),
|
||||
Content: content,
|
||||
ReplyTo: replyTo,
|
||||
IsPrivate: isPrivate,
|
||||
}
|
||||
if uid == 0 {
|
||||
in.GuestNick = c.PostForm("guest_nick")
|
||||
in.GuestEmail = c.PostForm("guest_email")
|
||||
in.GuestURL = c.PostForm("guest_url")
|
||||
}
|
||||
|
||||
comment, err := h.Comment.Create(in)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "评论成功", "floor": comment.Floor, "id": comment.ID})
|
||||
}
|
||||
|
||||
func (h *Handlers) APIDeleteComment(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err := h.Comment.Delete(h.currentUserID(c), uint(id), h.isAdmin(c)); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"message": "评论已删除"})
|
||||
}
|
||||
Reference in New Issue
Block a user