first commit
This commit is contained in:
parent
b72678fce0
commit
067eb8de56
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
9
.idea/bbs-backend.iml
Normal file
9
.idea/bbs-backend.iml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/bbs-backend.iml" filepath="$PROJECT_DIR$/.idea/bbs-backend.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
81
api/controller/bbs_controller.go
Normal file
81
api/controller/bbs_controller.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"bbs-backend/api/reply"
|
||||
"bbs-backend/api/request"
|
||||
"bbs-backend/common/errcode"
|
||||
"bbs-backend/common/response"
|
||||
"bbs-backend/logic/appservice"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetBBSInfo 获取论坛信息
|
||||
func GetBBSInfo(c *gin.Context) {
|
||||
forumInfo, err := appservice.GetBBSInfo()
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, errcode.ErrInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, reply.BBSInfoReply{
|
||||
ForumName: forumInfo["forum_name"],
|
||||
ForumLogo: forumInfo["forum_logo"],
|
||||
ForumDescription: forumInfo["forum_description"],
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateBBSInfo 更新论坛信息
|
||||
func UpdateBBSInfo(c *gin.Context) {
|
||||
// 验证管理员权限
|
||||
if !isAdmin(c) {
|
||||
response.Error(c, http.StatusForbidden, errcode.ErrForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
var req request.UpdateBBSInfoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err := appservice.UpdateBBSInfo(req)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, errcode.ErrInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "Forum info updated successfully"})
|
||||
}
|
||||
|
||||
// isAdmin 验证用户是否为管理员
|
||||
func isAdmin(c *gin.Context) bool {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte("your_secret_key"), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
return false
|
||||
}
|
||||
|
||||
userID, ok := claims["user_id"].(float64)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// 假设管理员用户的ID为1
|
||||
return uint(userID) == 1
|
||||
}
|
80
api/controller/post_controller.go
Normal file
80
api/controller/post_controller.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"bbs-backend/api/reply"
|
||||
"bbs-backend/api/request"
|
||||
"bbs-backend/common/errcode"
|
||||
"bbs-backend/common/response"
|
||||
"bbs-backend/logic/appservice"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func CreatePost(c *gin.Context) {
|
||||
var req request.CreatePostRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
post, err := appservice.CreatePost(req)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, errcode.ErrInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, reply.CreatePostReply{
|
||||
ID: post.ID,
|
||||
Title: post.Title,
|
||||
Content: post.Content,
|
||||
})
|
||||
}
|
||||
|
||||
func GetPost(c *gin.Context) {
|
||||
postID, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
post, err := appservice.GetPost(uint(postID))
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errcode.ErrPostNotFound:
|
||||
response.Error(c, http.StatusNotFound, errcode.ErrPostNotFound)
|
||||
default:
|
||||
response.Error(c, http.StatusInternalServerError, errcode.ErrInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, reply.GetPostReply{
|
||||
ID: post.ID,
|
||||
Title: post.Title,
|
||||
Content: post.Content,
|
||||
Author: post.Author.Username,
|
||||
})
|
||||
}
|
||||
|
||||
func GetPosts(c *gin.Context) {
|
||||
posts, err := appservice.GetPosts()
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, errcode.ErrInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var postReplies []reply.GetPostReply
|
||||
for _, post := range posts {
|
||||
postReplies = append(postReplies, reply.GetPostReply{
|
||||
ID: post.ID,
|
||||
Title: post.Title,
|
||||
Content: post.Content,
|
||||
Author: post.Author.Username,
|
||||
})
|
||||
}
|
||||
|
||||
response.Success(c, reply.GetPostsReply{
|
||||
Posts: postReplies,
|
||||
})
|
||||
}
|
275
api/controller/user_controller.go
Normal file
275
api/controller/user_controller.go
Normal file
|
@ -0,0 +1,275 @@
|
|||
package controller
|
||||
|
||||
import (
|
||||
"bbs-backend/api/reply"
|
||||
"bbs-backend/api/request"
|
||||
"bbs-backend/common/errcode"
|
||||
"bbs-backend/common/response"
|
||||
"bbs-backend/logic/appservice"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Register(c *gin.Context) {
|
||||
var req request.RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := appservice.RegisterUser(req)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errcode.ErrInvalidUsername:
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrInvalidUsername)
|
||||
case errcode.ErrInvalidPassword:
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrInvalidPassword)
|
||||
case errcode.ErrUserAlreadyExists:
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrUserAlreadyExists)
|
||||
case errcode.ErrEmailExists:
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrEmailExists)
|
||||
default:
|
||||
response.Error(c, http.StatusInternalServerError, errcode.ErrInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
reply := &reply.RegisterReply{}
|
||||
reply.FromModel(user)
|
||||
response.Success(c, reply)
|
||||
}
|
||||
|
||||
func Login(c *gin.Context) {
|
||||
var req request.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
token, err := appservice.LoginUser(req)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errcode.ErrInvalidUsername:
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrInvalidUsername)
|
||||
case errcode.ErrInvalidPassword:
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrInvalidPassword)
|
||||
case errcode.ErrUserNotFound:
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrUserNotFound)
|
||||
default:
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrUnauthorized)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, reply.LoginReply{Token: token})
|
||||
}
|
||||
|
||||
func GetUserInfo(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte("your_secret_key"), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
userID, ok := claims["user_id"].(float64)
|
||||
if !ok {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := appservice.GetUserInfo(uint(userID))
|
||||
if err != nil {
|
||||
switch err {
|
||||
case errcode.ErrUserNotFound:
|
||||
response.Error(c, http.StatusNotFound, errcode.ErrUserNotFound)
|
||||
default:
|
||||
response.Error(c, http.StatusInternalServerError, errcode.ErrInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
reply := &reply.UserInfoReply{}
|
||||
reply.FromModel(user)
|
||||
response.Success(c, reply)
|
||||
}
|
||||
|
||||
func UpdateUserInfo(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte("your_secret_key"), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
userID, ok := claims["user_id"].(float64)
|
||||
if !ok {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
var req request.UpdateUserInfoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = appservice.UpdateUserInfo(uint(userID), req)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, errcode.ErrInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "User info updated successfully"})
|
||||
}
|
||||
|
||||
func UploadAvatar(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte("your_secret_key"), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
userID, ok := claims["user_id"].(float64)
|
||||
if !ok {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
// 处理图片上传
|
||||
file, err := c.FormFile("avatar")
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
avatarFile, err := file.Open()
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, errcode.ErrInternalServerError)
|
||||
return
|
||||
}
|
||||
defer avatarFile.Close()
|
||||
|
||||
avatar, err := io.ReadAll(avatarFile)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, errcode.ErrInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
err = appservice.UploadAvatar(uint(userID), avatar)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, errcode.ErrInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "Avatar uploaded successfully"})
|
||||
}
|
||||
|
||||
func SendVerificationCode(c *gin.Context) {
|
||||
var req request.SendVerificationCodeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err := appservice.SendVerificationCode(req.Email)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, errcode.ErrInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "Verification code sent successfully"})
|
||||
}
|
||||
|
||||
func VerifyEmail(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := strings.Replace(authHeader, "Bearer ", "", 1)
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte("your_secret_key"), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok || !token.Valid {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
userID, ok := claims["user_id"].(float64)
|
||||
if !ok {
|
||||
response.Error(c, http.StatusUnauthorized, errcode.ErrInvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
var req request.VerifyEmailRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, errcode.ErrBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
err = appservice.VerifyEmail(uint(userID), req.Email, req.Code)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusInternalServerError, errcode.ErrInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, gin.H{"message": "Email verified successfully"})
|
||||
}
|
8
api/reply/bbs_reply.go
Normal file
8
api/reply/bbs_reply.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
// file name: bbs_reply.go
|
||||
package reply
|
||||
|
||||
type BBSInfoReply struct {
|
||||
ForumName string `json:"forum_name"`
|
||||
ForumLogo string `json:"forum_logo"`
|
||||
ForumDescription string `json:"forum_description"`
|
||||
}
|
18
api/reply/post_reply.go
Normal file
18
api/reply/post_reply.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package reply
|
||||
|
||||
type CreatePostReply struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type GetPostReply struct {
|
||||
ID uint `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Author string `json:"author"`
|
||||
}
|
||||
|
||||
type GetPostsReply struct {
|
||||
Posts []GetPostReply `json:"posts"`
|
||||
}
|
84
api/reply/user_reply.go
Normal file
84
api/reply/user_reply.go
Normal file
|
@ -0,0 +1,84 @@
|
|||
package reply
|
||||
|
||||
import (
|
||||
"bbs-backend/dal/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UserInfoReply 用户信息响应结构体
|
||||
type UserInfoReply struct {
|
||||
ID uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Gender string `json:"gender"`
|
||||
HomePage string `json:"home_page"`
|
||||
Description string `json:"description"`
|
||||
RoleIds []uint `json:"role_ids"`
|
||||
Status string `json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// FromModel 从 model.User 转换为 UserInfoReply
|
||||
func (r *UserInfoReply) FromModel(user *model.User) {
|
||||
r.ID = user.ID
|
||||
r.Type = user.Type
|
||||
r.Username = user.Username
|
||||
r.Email = user.Email
|
||||
r.Nickname = user.Nickname
|
||||
r.AvatarURL = user.AvatarURL
|
||||
r.Gender = user.Gender
|
||||
r.HomePage = user.HomePage
|
||||
r.Description = user.Description
|
||||
r.Status = user.Status
|
||||
r.CreatedAt = user.CreatedAt
|
||||
r.UpdatedAt = user.UpdatedAt
|
||||
|
||||
// 转换角色 ID 列表
|
||||
r.RoleIds = make([]uint, len(user.Roles))
|
||||
for i, role := range user.Roles {
|
||||
r.RoleIds[i] = role.ID
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterReply 注册响应结构体
|
||||
type RegisterReply struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// FromModel 从 model.User 转换为 RegisterReply
|
||||
func (r *RegisterReply) FromModel(user *model.User) {
|
||||
r.ID = user.ID
|
||||
r.Username = user.Username
|
||||
r.Email = user.Email
|
||||
}
|
||||
|
||||
// LoginReply 登录响应结构体
|
||||
type LoginReply struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
// UpdateUserInfoReply 用户信息更新响应结构体
|
||||
type UpdateUserInfoReply struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// UploadAvatarReply 头像上传响应结构体
|
||||
type UploadAvatarReply struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// SendVerificationCodeReply 发送验证码响应结构体
|
||||
type SendVerificationCodeReply struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// VerifyEmailReply 验证邮箱响应结构体
|
||||
type VerifyEmailReply struct {
|
||||
Message string `json:"message"`
|
||||
}
|
8
api/request/bbs_request.go
Normal file
8
api/request/bbs_request.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
// file name: bbs_request.go
|
||||
package request
|
||||
|
||||
type UpdateBBSInfoRequest struct {
|
||||
BBSName string `json:"forum_name"`
|
||||
BBSLogo string `json:"forum_logo"`
|
||||
BBSDescription string `json:"forum_description"`
|
||||
}
|
6
api/request/post_request.go
Normal file
6
api/request/post_request.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package request
|
||||
|
||||
type CreatePostRequest struct {
|
||||
Title string `json:"title" binding:"required"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
}
|
28
api/request/user_request.go
Normal file
28
api/request/user_request.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package request
|
||||
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type UpdateUserInfoRequest struct {
|
||||
Email string `json:"email"`
|
||||
Nickname string `json:"nickname"`
|
||||
Description string `json:"description"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type SendVerificationCodeRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
}
|
||||
|
||||
type VerifyEmailRequest struct {
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
}
|
26
api/router/router.go
Normal file
26
api/router/router.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
// file name: router.go
|
||||
package router
|
||||
|
||||
import (
|
||||
"bbs-backend/api/controller"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func SetupRouter() *gin.Engine {
|
||||
r := gin.Default()
|
||||
|
||||
r.POST("/register", controller.Register)
|
||||
r.POST("/login", controller.Login)
|
||||
r.GET("/users/:id", controller.GetUserInfo)
|
||||
r.POST("/posts", controller.CreatePost)
|
||||
r.GET("/posts", controller.GetPosts)
|
||||
r.GET("/posts/:id", controller.GetPost)
|
||||
r.PUT("/update-user-info", controller.UpdateUserInfo) // 更新用户信息
|
||||
r.POST("/upload-avatar", controller.UploadAvatar) // 上传头像
|
||||
r.POST("/send-verification-code", controller.SendVerificationCode) // 发送验证码
|
||||
r.POST("/verify-email", controller.VerifyEmail) // 验证邮箱
|
||||
r.GET("/bbs-info", controller.GetBBSInfo) // 获取论坛信息
|
||||
r.PUT("/bbs-info", controller.UpdateBBSInfo) // 更新论坛信息
|
||||
|
||||
return r
|
||||
}
|
3
common/app/app.go
Normal file
3
common/app/app.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package app
|
||||
|
||||
// This is a default file for the app package.
|
3
common/enum/enum.go
Normal file
3
common/enum/enum.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package enum
|
||||
|
||||
// This is a default file for the enum package.
|
43
common/errcode/errcode.go
Normal file
43
common/errcode/errcode.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package errcode
|
||||
|
||||
type Error struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (e *Error) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func NewError(code int, message string) *Error {
|
||||
return &Error{
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// 通用错误
|
||||
ErrBadRequest = NewError(10001, "Bad Request")
|
||||
ErrUnauthorized = NewError(10002, "Unauthorized")
|
||||
ErrForbidden = NewError(10003, "Forbidden")
|
||||
ErrNotFound = NewError(10004, "Not Found")
|
||||
ErrInternalServerError = NewError(10005, "Internal Server Error")
|
||||
|
||||
// 用户相关错误
|
||||
ErrInvalidUsername = NewError(20001, "Invalid Username")
|
||||
ErrInvalidPassword = NewError(20002, "Invalid Password")
|
||||
ErrUserAlreadyExists = NewError(20003, "User Already Exists")
|
||||
ErrUserNotFound = NewError(20004, "User Not Found")
|
||||
ErrInvalidToken = NewError(20005, "Invalid Token")
|
||||
ErrUsernameExists = NewError(20006, "Username Already Exists")
|
||||
ErrEmailExists = NewError(20007, "Email Already Exists")
|
||||
|
||||
// 帖子相关错误
|
||||
ErrPostNotFound = NewError(30001, "Post Not Found")
|
||||
ErrInvalidTitle = NewError(30002, "Invalid Title")
|
||||
ErrInvalidContent = NewError(30003, "Invalid Content")
|
||||
|
||||
// 其他错误
|
||||
ErrDatabaseError = NewError(40001, "Database Error")
|
||||
)
|
3
common/logger/logger.go
Normal file
3
common/logger/logger.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package logger
|
||||
|
||||
// This is a default file for the logger package.
|
3
common/middleware/middleware.go
Normal file
3
common/middleware/middleware.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package middleware
|
||||
|
||||
// This is a default file for the middleware package.
|
28
common/response/response.go
Normal file
28
common/response/response.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package response
|
||||
|
||||
import (
|
||||
"bbs-backend/common/errcode"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func Success(c *gin.Context, data interface{}) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Code: 0, // 自定义成功码
|
||||
Message: "success",
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
func Error(c *gin.Context, httpStatusCode int, err *errcode.Error) {
|
||||
c.JSON(httpStatusCode, Response{
|
||||
Code: err.Code,
|
||||
Message: err.Message,
|
||||
})
|
||||
}
|
3
common/util/util.go
Normal file
3
common/util/util.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package util
|
||||
|
||||
// This is a default file for the util package.
|
37
config/config.go
Normal file
37
config/config.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
// file name: config.go
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/viper"
|
||||
"log"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Database struct {
|
||||
Type string `mapstructure:"type"`
|
||||
File string `mapstructure:"file"`
|
||||
} `mapstructure:"database"`
|
||||
Server struct {
|
||||
Port int `mapstructure:"port"`
|
||||
} `mapstructure:"server"`
|
||||
}
|
||||
|
||||
var cfg *Config
|
||||
|
||||
func LoadConfig(path string) {
|
||||
viper.SetConfigFile(path)
|
||||
err := viper.ReadInConfig()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to read config file: %v", err)
|
||||
}
|
||||
|
||||
cfg = &Config{}
|
||||
err = viper.Unmarshal(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to unmarshal config file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetConfig() *Config {
|
||||
return cfg
|
||||
}
|
7
config/config.yaml
Normal file
7
config/config.yaml
Normal file
|
@ -0,0 +1,7 @@
|
|||
# Default configuration file for the project
|
||||
database:
|
||||
type: sqlite
|
||||
file: test.db
|
||||
|
||||
server:
|
||||
port: 8888
|
3
dal/cache/cache.go
vendored
Normal file
3
dal/cache/cache.go
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
package cache
|
||||
|
||||
// This is a default file for the cache package.
|
26
dal/dao/bbs_dao.go
Normal file
26
dal/dao/bbs_dao.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
// file name: bbs_dao.go
|
||||
package dao
|
||||
|
||||
import (
|
||||
"bbs-backend/dal"
|
||||
"bbs-backend/dal/model"
|
||||
)
|
||||
|
||||
func GetBBSInfo() (map[string]string, error) {
|
||||
var configs []model.BBSConfig
|
||||
err := dal.DB.Find(&configs).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forumInfo := make(map[string]string)
|
||||
for _, config := range configs {
|
||||
forumInfo[config.Key] = config.Value
|
||||
}
|
||||
|
||||
return forumInfo, nil
|
||||
}
|
||||
|
||||
func UpdateBBSInfo(key, value string) error {
|
||||
return dal.DB.Model(&model.BBSConfig{}).Where("key = ?", key).Update("value", value).Error
|
||||
}
|
28
dal/dao/post_dao.go
Normal file
28
dal/dao/post_dao.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"bbs-backend/dal"
|
||||
"bbs-backend/dal/model"
|
||||
)
|
||||
|
||||
func CreatePost(post *model.Post) error {
|
||||
return dal.DB.Create(post).Error
|
||||
}
|
||||
|
||||
func GetPostByID(postID uint) (*model.Post, error) {
|
||||
var post model.Post
|
||||
err := dal.DB.Preload("Author").First(&post, postID).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &post, nil
|
||||
}
|
||||
|
||||
func GetAllPosts() ([]*model.Post, error) {
|
||||
var posts []*model.Post
|
||||
err := dal.DB.Preload("Author").Find(&posts).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return posts, nil
|
||||
}
|
56
dal/dao/user_dao.go
Normal file
56
dal/dao/user_dao.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package dao
|
||||
|
||||
import (
|
||||
"bbs-backend/dal"
|
||||
"bbs-backend/dal/model"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUsernameExists = errors.New("username already exists")
|
||||
ErrEmailExists = errors.New("email already exists")
|
||||
)
|
||||
|
||||
func CreateUser(user *model.User) error {
|
||||
err := dal.DB.Create(user).Error
|
||||
if err != nil {
|
||||
if isDuplicateKeyError(err, "username") {
|
||||
return ErrUsernameExists
|
||||
}
|
||||
if isDuplicateKeyError(err, "email") {
|
||||
return ErrEmailExists
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetUserByUsername(username string) (*model.User, error) {
|
||||
var user model.User
|
||||
err := dal.DB.Preload("Roles").Where("username = ?", username).First(&user).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func GetUserByID(userID uint) (*model.User, error) {
|
||||
var user model.User
|
||||
err := dal.DB.Preload("Roles").First(&user, userID).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func UpdateUser(user *model.User) error {
|
||||
err := dal.DB.Save(user).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDuplicateKeyError(err error, key string) bool {
|
||||
return err != nil && err.Error() == "UNIQUE constraint failed: users."+key
|
||||
}
|
19
dal/db.go
Normal file
19
dal/db.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
// file name: db.go
|
||||
package dal
|
||||
|
||||
import (
|
||||
"bbs-backend/config"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"log"
|
||||
)
|
||||
|
||||
var DB *gorm.DB
|
||||
|
||||
func InitDB() {
|
||||
var err error
|
||||
DB, err = gorm.Open(sqlite.Open(config.GetConfig().Database.File), &gorm.Config{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
}
|
106
dal/init_db.go
Normal file
106
dal/init_db.go
Normal file
|
@ -0,0 +1,106 @@
|
|||
package dal
|
||||
|
||||
import (
|
||||
"bbs-backend/config"
|
||||
"bbs-backend/dal/model"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"golang.org/x/crypto/argon2"
|
||||
"log"
|
||||
)
|
||||
|
||||
func MigrateAndSeedDB() {
|
||||
// Load configuration
|
||||
config.LoadConfig("config/config.yaml")
|
||||
|
||||
// Initialize the database
|
||||
InitDB()
|
||||
|
||||
// Migrate the schema
|
||||
err := DB.AutoMigrate(&model.User{}, &model.Post{}, &model.Comment{}, &model.Category{}, &model.Role{}, &model.Session{}, &model.BBSConfig{})
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to migrate database schema: %v", err)
|
||||
}
|
||||
|
||||
// Seed initial roles
|
||||
roles := []model.Role{
|
||||
{Name: "admin"},
|
||||
{Name: "editor"},
|
||||
{Name: "viewer"},
|
||||
}
|
||||
for _, role := range roles {
|
||||
if err := DB.FirstOrCreate(&role, model.Role{Name: role.Name}).Error; err != nil {
|
||||
log.Fatalf("Failed to seed role: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if initial data already exists
|
||||
var userCount int64
|
||||
var categoryCount int64
|
||||
var configCount int64
|
||||
DB.Model(&model.User{}).Where("username = ?", "admin").Count(&userCount)
|
||||
DB.Model(&model.Category{}).Where("name = ?", "General").Count(&categoryCount)
|
||||
DB.Model(&model.BBSConfig{}).Count(&configCount)
|
||||
|
||||
if userCount == 0 {
|
||||
// Seed initial user data
|
||||
var adminRole model.Role
|
||||
DB.Where("name = ?", "admin").First(&adminRole)
|
||||
|
||||
// Generate salt
|
||||
salt := make([]byte, 16)
|
||||
_, err := rand.Read(salt)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate salt: %v", err)
|
||||
}
|
||||
|
||||
// Hash password using Argon2 id
|
||||
hashedPassword := argon2.IDKey([]byte("admin123"), salt, 1, 64*1024, 4, 32)
|
||||
|
||||
// Set role IDs
|
||||
roleIDs := []uint{adminRole.ID}
|
||||
roleIDsJSON, _ := json.Marshal(roleIDs)
|
||||
|
||||
user := model.User{
|
||||
Type: "admin",
|
||||
Username: "admin",
|
||||
Email: "admin@example.com",
|
||||
Nickname: "Admin User",
|
||||
AvatarURL: "",
|
||||
Gender: "unknown",
|
||||
HomePage: "",
|
||||
Description: "This is the admin user.",
|
||||
RoleIds: string(roleIDsJSON),
|
||||
Status: "active",
|
||||
Password: base64.StdEncoding.EncodeToString(hashedPassword),
|
||||
Salt: base64.StdEncoding.EncodeToString(salt),
|
||||
Roles: []model.Role{adminRole},
|
||||
}
|
||||
if err := DB.Create(&user).Error; err != nil {
|
||||
log.Fatalf("Failed to seed user: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if categoryCount == 0 {
|
||||
// Seed initial category data
|
||||
category := model.Category{Name: "General", Description: "General discussion"}
|
||||
if err := DB.Create(&category).Error; err != nil {
|
||||
log.Fatalf("Failed to seed category: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if configCount == 0 {
|
||||
// Seed initial config data
|
||||
configs := []model.BBSConfig{
|
||||
{Key: "forum_name", Value: "My BBS Forum"},
|
||||
{Key: "forum_logo", Value: "https://example.com/logo.png"},
|
||||
{Key: "forum_description", Value: "Welcome to our BBS forum!"},
|
||||
}
|
||||
for _, bbsConfig := range configs {
|
||||
if err := DB.FirstOrCreate(&bbsConfig, model.BBSConfig{Key: bbsConfig.Key}).Error; err != nil {
|
||||
log.Fatalf("Failed to seed BBS config: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
12
dal/model/bbs_model.go
Normal file
12
dal/model/bbs_model.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
// file name: bbs_model.go
|
||||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type BBSConfig struct {
|
||||
gorm.Model
|
||||
Key string `gorm:"type:varchar(100);unique;not null" json:"key"`
|
||||
Value string `gorm:"type:varchar(255)" json:"value"`
|
||||
}
|
12
dal/model/category_model.go
Normal file
12
dal/model/category_model.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Category struct {
|
||||
gorm.Model
|
||||
Name string
|
||||
Description string
|
||||
Posts []Post
|
||||
}
|
14
dal/model/comment_model.go
Normal file
14
dal/model/comment_model.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Comment struct {
|
||||
gorm.Model
|
||||
Content string
|
||||
AuthorID uint
|
||||
Author User
|
||||
PostID uint
|
||||
Post Post
|
||||
}
|
16
dal/model/post_model.go
Normal file
16
dal/model/post_model.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Post struct {
|
||||
gorm.Model
|
||||
Title string
|
||||
Content string
|
||||
AuthorID uint
|
||||
Author User
|
||||
CategoryID uint
|
||||
Category Category
|
||||
Comments []Comment
|
||||
}
|
11
dal/model/role_model.go
Normal file
11
dal/model/role_model.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Role struct {
|
||||
gorm.Model
|
||||
Name string `gorm:"type:varchar(100);not null;unique" json:"name"`
|
||||
Users []User `gorm:"many2many:user_roles;" json:"users"`
|
||||
}
|
12
dal/model/session_model.go
Normal file
12
dal/model/session_model.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
gorm.Model
|
||||
UserID uint
|
||||
User User
|
||||
Token string
|
||||
}
|
33
dal/model/user_model.go
Normal file
33
dal/model/user_model.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package model
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
gorm.Model
|
||||
Type string `gorm:"type:varchar(50);not null" json:"type"`
|
||||
Username string `gorm:"type:varchar(100);unique;not null" json:"username"`
|
||||
Email string `gorm:"type:varchar(100);unique;not null" json:"email"`
|
||||
Nickname string `gorm:"type:varchar(100)" json:"nickname"`
|
||||
Avatar []byte `gorm:"type:blob" json:"-"`
|
||||
AvatarURL string `gorm:"type:varchar(255)" json:"avatar_url"`
|
||||
Gender string `gorm:"type:varchar(10)" json:"gender"`
|
||||
HomePage string `gorm:"type:varchar(255)" json:"home_page"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
RoleIds string `gorm:"type:jsonb" json:"role_ids"`
|
||||
Status string `gorm:"type:varchar(50);not null" json:"status"`
|
||||
Password string `gorm:"type:varchar(255);not null" json:"-"`
|
||||
Salt string `gorm:"type:varchar(255);not null" json:"-"`
|
||||
Roles []Role `gorm:"many2many:user_roles;" json:"roles"`
|
||||
}
|
||||
|
||||
// BeforeSave is a GORM hook that ensures only one of Avatar or AvatarURL is set
|
||||
func (u *User) BeforeSave(tx *gorm.DB) (err error) {
|
||||
if len(u.Avatar) > 0 {
|
||||
u.AvatarURL = ""
|
||||
} else if u.AvatarURL != "" {
|
||||
u.Avatar = nil
|
||||
}
|
||||
return
|
||||
}
|
59
go.mod
Normal file
59
go.mod
Normal file
|
@ -0,0 +1,59 @@
|
|||
module bbs-backend
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
gorm.io/driver/sqlite v1.5.6
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bytedance/sonic v1.12.4 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
|
||||
github.com/gin-contrib/cors v1.7.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.23.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.19.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.9.0 // indirect
|
||||
golang.org/x/arch v0.12.0 // indirect
|
||||
golang.org/x/crypto v0.29.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.31.0 // indirect
|
||||
golang.org/x/sys v0.27.0 // indirect
|
||||
golang.org/x/text v0.20.0 // indirect
|
||||
google.golang.org/protobuf v1.35.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
161
go.sum
Normal file
161
go.sum
Normal file
|
@ -0,0 +1,161 @@
|
|||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic v1.12.4 h1:9Csb3c9ZJhfUWeMtpCDCq6BUoH5ogfDFLUgQ/jG+R0k=
|
||||
github.com/bytedance/sonic v1.12.4/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
||||
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
|
||||
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
|
||||
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o=
|
||||
github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
|
||||
github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
||||
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
|
||||
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
|
||||
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
67
handlers/post.go
Normal file
67
handlers/post.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PostHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewPostHandler(db *gorm.DB) *PostHandler {
|
||||
return &PostHandler{db: db}
|
||||
}
|
||||
|
||||
// 获取帖子列表
|
||||
func (h *PostHandler) GetPosts(c *gin.Context) {
|
||||
var posts []Post
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
|
||||
offset := (page - 1) * pageSize
|
||||
query := h.db.Model(&Post{}).Preload("Author")
|
||||
|
||||
var total int64
|
||||
query.Count(&total)
|
||||
|
||||
if err := query.Offset(offset).Limit(pageSize).Find(&posts).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"code": 500,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": gin.H{
|
||||
"total": total,
|
||||
"posts": posts,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 获取帖子详情
|
||||
func (h *PostHandler) GetPostDetail(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var post Post
|
||||
if err := h.db.Preload("Author").First(&post, id).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"code": 404,
|
||||
"message": "Post not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 200,
|
||||
"message": "success",
|
||||
"data": post,
|
||||
})
|
||||
}
|
23
logic/appservice/bbs_service.go
Normal file
23
logic/appservice/bbs_service.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package appservice
|
||||
|
||||
import (
|
||||
"bbs-backend/api/request"
|
||||
"bbs-backend/common/errcode"
|
||||
"bbs-backend/logic/domainservice"
|
||||
)
|
||||
|
||||
func GetBBSInfo() (map[string]string, error) {
|
||||
forumInfo, err := domainservice.GetBBSInfoDomainService()
|
||||
if err != nil {
|
||||
return nil, errcode.ErrInternalServerError
|
||||
}
|
||||
return forumInfo, nil
|
||||
}
|
||||
|
||||
func UpdateBBSInfo(req request.UpdateBBSInfoRequest) error {
|
||||
err := domainservice.UpdateBBSInfoDomainService(req)
|
||||
if err != nil {
|
||||
return errcode.ErrInternalServerError
|
||||
}
|
||||
return nil
|
||||
}
|
39
logic/appservice/post_service.go
Normal file
39
logic/appservice/post_service.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package appservice
|
||||
|
||||
import (
|
||||
"bbs-backend/api/request"
|
||||
"bbs-backend/common/errcode"
|
||||
"bbs-backend/dal/model"
|
||||
"bbs-backend/logic/domainservice"
|
||||
)
|
||||
|
||||
func CreatePost(req request.CreatePostRequest) (*model.Post, error) {
|
||||
post := &model.Post{
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
AuthorID: 1, // 假设当前用户ID为1
|
||||
}
|
||||
|
||||
err := domainservice.CreatePostDomainService(post)
|
||||
if err != nil {
|
||||
return nil, errcode.ErrInternalServerError
|
||||
}
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
func GetPost(postID uint) (*model.Post, error) {
|
||||
post, err := domainservice.GetPostDomainService(postID)
|
||||
if err != nil {
|
||||
return nil, errcode.ErrNotFound
|
||||
}
|
||||
return post, nil
|
||||
}
|
||||
|
||||
func GetPosts() ([]*model.Post, error) {
|
||||
posts, err := domainservice.GetPostsDomainService()
|
||||
if err != nil {
|
||||
return nil, errcode.ErrInternalServerError
|
||||
}
|
||||
return posts, nil
|
||||
}
|
120
logic/appservice/user_service.go
Normal file
120
logic/appservice/user_service.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
package appservice
|
||||
|
||||
import (
|
||||
"bbs-backend/api/request"
|
||||
"bbs-backend/common/errcode"
|
||||
"bbs-backend/dal/model"
|
||||
"bbs-backend/logic/domainservice"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"time"
|
||||
)
|
||||
|
||||
var jwtKey = []byte("your_secret_key")
|
||||
|
||||
// RegisterUser handles user registration
|
||||
func RegisterUser(req request.RegisterRequest) (*model.User, error) {
|
||||
// 转换为数据模型
|
||||
user := &model.User{
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
Password: req.Password,
|
||||
}
|
||||
|
||||
err := domainservice.RegisterUserDomainService(user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// LoginUser handles user login and generates a JWT token
|
||||
func LoginUser(req request.LoginRequest) (string, error) {
|
||||
user, err := domainservice.LoginUserDomainService(req.Username, req.Password)
|
||||
if err != nil {
|
||||
return "", errcode.ErrUnauthorized
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"user_id": user.ID,
|
||||
"exp": time.Now().Add(time.Hour * 24).Unix(),
|
||||
})
|
||||
|
||||
tokenString, err := token.SignedString(jwtKey)
|
||||
if err != nil {
|
||||
return "", errcode.ErrInternalServerError
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// GetUserInfo retrieves user information by user ID
|
||||
func GetUserInfo(userID uint) (*model.User, error) {
|
||||
user, err := domainservice.GetUserInfoDomainService(userID)
|
||||
if err != nil {
|
||||
return nil, errcode.ErrInternalServerError
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// UpdateUserInfo handles user info update
|
||||
func UpdateUserInfo(userID uint, req request.UpdateUserInfoRequest) error {
|
||||
user, err := domainservice.GetUserInfoDomainService(userID)
|
||||
if err != nil {
|
||||
return errcode.ErrInternalServerError
|
||||
}
|
||||
|
||||
fieldsToUpdate := make([]string, 0)
|
||||
|
||||
if req.Email != "" {
|
||||
user.Email = req.Email
|
||||
fieldsToUpdate = append(fieldsToUpdate, "Email")
|
||||
}
|
||||
if req.Nickname != "" {
|
||||
user.Nickname = req.Nickname
|
||||
}
|
||||
if req.Description != "" {
|
||||
user.Description = req.Description
|
||||
}
|
||||
if req.Password != "" {
|
||||
user.Password = req.Password
|
||||
fieldsToUpdate = append(fieldsToUpdate, "Password")
|
||||
}
|
||||
|
||||
err = domainservice.UpdateUserDomainService(user, fieldsToUpdate...)
|
||||
if err != nil {
|
||||
return errcode.ErrInternalServerError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadAvatar handles avatar upload
|
||||
func UploadAvatar(userID uint, avatar []byte) error {
|
||||
user, err := domainservice.GetUserInfoDomainService(userID)
|
||||
if err != nil {
|
||||
return errcode.ErrInternalServerError
|
||||
}
|
||||
|
||||
user.Avatar = avatar
|
||||
user.AvatarURL = ""
|
||||
|
||||
err = domainservice.UpdateUserDomainService(user)
|
||||
if err != nil {
|
||||
return errcode.ErrInternalServerError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendVerificationCode sends verification code to email
|
||||
func SendVerificationCode(email string) error {
|
||||
// 发送验证码逻辑
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyEmail verifies email and code
|
||||
func VerifyEmail(userID uint, email, code string) error {
|
||||
// 验证邮箱和验证码逻辑
|
||||
return nil
|
||||
}
|
8
logic/do/post_do.go
Normal file
8
logic/do/post_do.go
Normal file
|
@ -0,0 +1,8 @@
|
|||
package do
|
||||
|
||||
type Post struct {
|
||||
ID uint
|
||||
Title string
|
||||
Content string
|
||||
Author string
|
||||
}
|
45
logic/do/user_do.go
Normal file
45
logic/do/user_do.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package do
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// User 领域对象
|
||||
type User struct {
|
||||
ID uint
|
||||
Username string
|
||||
Email string
|
||||
Password string
|
||||
Salt string
|
||||
}
|
||||
|
||||
// Validate 验证用户信息
|
||||
func (u *User) Validate(requiredFields ...string) error {
|
||||
for _, field := range requiredFields {
|
||||
switch field {
|
||||
case "Username":
|
||||
if u.Username == "" {
|
||||
return errors.New("username is required")
|
||||
}
|
||||
case "Email":
|
||||
if u.Email == "" {
|
||||
return errors.New("email is required")
|
||||
}
|
||||
if !isValidEmail(u.Email) {
|
||||
return errors.New("invalid email format")
|
||||
}
|
||||
case "Password":
|
||||
if u.Password == "" {
|
||||
return errors.New("password is required")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// isValidEmail 验证邮箱格式
|
||||
func isValidEmail(email string) bool {
|
||||
re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
|
||||
return re.MatchString(email)
|
||||
}
|
38
logic/domainservice/bbs_domain_service.go
Normal file
38
logic/domainservice/bbs_domain_service.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
// file name: bbs_domain_service.go
|
||||
package domainservice
|
||||
|
||||
import (
|
||||
"bbs-backend/api/request"
|
||||
"bbs-backend/common/errcode"
|
||||
"bbs-backend/dal/dao"
|
||||
)
|
||||
|
||||
func GetBBSInfoDomainService() (map[string]string, error) {
|
||||
forumInfo, err := dao.GetBBSInfo()
|
||||
if err != nil {
|
||||
return nil, errcode.ErrInternalServerError
|
||||
}
|
||||
return forumInfo, nil
|
||||
}
|
||||
|
||||
func UpdateBBSInfoDomainService(req request.UpdateBBSInfoRequest) error {
|
||||
if req.BBSName != "" {
|
||||
err := dao.UpdateBBSInfo("forum_name", req.BBSName)
|
||||
if err != nil {
|
||||
return errcode.ErrInternalServerError
|
||||
}
|
||||
}
|
||||
if req.BBSLogo != "" {
|
||||
err := dao.UpdateBBSInfo("forum_logo", req.BBSLogo)
|
||||
if err != nil {
|
||||
return errcode.ErrInternalServerError
|
||||
}
|
||||
}
|
||||
if req.BBSDescription != "" {
|
||||
err := dao.UpdateBBSInfo("forum_description", req.BBSDescription)
|
||||
if err != nil {
|
||||
return errcode.ErrInternalServerError
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
47
logic/domainservice/post_domain_service.go
Normal file
47
logic/domainservice/post_domain_service.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package domainservice
|
||||
|
||||
import (
|
||||
"bbs-backend/common/errcode"
|
||||
"bbs-backend/dal/dao"
|
||||
"bbs-backend/dal/model"
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func CreatePostDomainService(post *model.Post) error {
|
||||
// 验证帖子内容
|
||||
if post.Title == "" {
|
||||
return errcode.ErrInvalidTitle
|
||||
}
|
||||
if post.Content == "" {
|
||||
return errcode.ErrInvalidContent
|
||||
}
|
||||
|
||||
err := dao.CreatePost(post)
|
||||
if err != nil {
|
||||
return errcode.ErrInternalServerError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetPostDomainService(postID uint) (*model.Post, error) {
|
||||
post, err := dao.GetPostByID(postID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errcode.ErrPostNotFound
|
||||
}
|
||||
return nil, errcode.ErrInternalServerError
|
||||
}
|
||||
|
||||
return post, nil
|
||||
}
|
||||
|
||||
func GetPostsDomainService() ([]*model.Post, error) {
|
||||
posts, err := dao.GetAllPosts()
|
||||
if err != nil {
|
||||
return nil, errcode.ErrInternalServerError
|
||||
}
|
||||
|
||||
return posts, nil
|
||||
}
|
124
logic/domainservice/user_domain_service.go
Normal file
124
logic/domainservice/user_domain_service.go
Normal file
|
@ -0,0 +1,124 @@
|
|||
package domainservice
|
||||
|
||||
import (
|
||||
"bbs-backend/common/errcode"
|
||||
"bbs-backend/dal/dao"
|
||||
"bbs-backend/dal/model"
|
||||
"bbs-backend/logic/do"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"golang.org/x/crypto/argon2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func RegisterUserDomainService(user *model.User) error {
|
||||
// 创建领域对象
|
||||
userDO := &do.User{
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Password: user.Password,
|
||||
}
|
||||
|
||||
// 验证领域对象
|
||||
if err := userDO.Validate("Username", "Email", "Password"); err != nil {
|
||||
return errcode.ErrInvalidUsername
|
||||
}
|
||||
|
||||
// 检查用户是否已存在
|
||||
_, err := dao.GetUserByUsername(userDO.Username)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errcode.ErrInternalServerError
|
||||
}
|
||||
|
||||
// 如果用户不存在,继续注册
|
||||
salt := make([]byte, 16)
|
||||
_, err = rand.Read(salt)
|
||||
if err != nil {
|
||||
return errcode.ErrInternalServerError
|
||||
}
|
||||
|
||||
hashedPassword := argon2.IDKey([]byte(userDO.Password), salt, 1, 64*1024, 4, 32)
|
||||
user.Password = base64.StdEncoding.EncodeToString(hashedPassword)
|
||||
user.Salt = base64.StdEncoding.EncodeToString(salt)
|
||||
|
||||
err = dao.CreateUser(user)
|
||||
if err != nil {
|
||||
if errors.Is(err, dao.ErrUsernameExists) {
|
||||
return errcode.ErrUsernameExists
|
||||
}
|
||||
if errors.Is(err, dao.ErrEmailExists) {
|
||||
return errcode.ErrEmailExists
|
||||
}
|
||||
return errcode.ErrInternalServerError
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoginUserDomainService(username, password string) (*model.User, error) {
|
||||
// 创建领域对象
|
||||
userDO := &do.User{
|
||||
Username: username,
|
||||
Password: password,
|
||||
}
|
||||
|
||||
// 验证领域对象
|
||||
if err := userDO.Validate("Username", "Password"); err != nil {
|
||||
return nil, errcode.ErrInvalidUsername
|
||||
}
|
||||
|
||||
// 检查用户是否存在
|
||||
user, err := dao.GetUserByUsername(userDO.Username)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errcode.ErrUserNotFound
|
||||
}
|
||||
return nil, errcode.ErrInternalServerError
|
||||
}
|
||||
|
||||
salt, err := base64.StdEncoding.DecodeString(user.Salt)
|
||||
if err != nil {
|
||||
return nil, errcode.ErrUnauthorized
|
||||
}
|
||||
|
||||
hashedPassword := argon2.IDKey([]byte(userDO.Password), salt, 1, 64*1024, 4, 32)
|
||||
if base64.StdEncoding.EncodeToString(hashedPassword) != user.Password {
|
||||
return nil, errcode.ErrUnauthorized
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func GetUserInfoDomainService(userID uint) (*model.User, error) {
|
||||
// 检查用户是否存在
|
||||
user, err := dao.GetUserByID(userID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errcode.ErrUserNotFound
|
||||
}
|
||||
return nil, errcode.ErrInternalServerError
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func UpdateUserDomainService(user *model.User, fieldsToUpdate ...string) error {
|
||||
// 创建领域对象
|
||||
userDO := &do.User{
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Password: user.Password,
|
||||
}
|
||||
|
||||
// 验证领域对象
|
||||
if err := userDO.Validate(fieldsToUpdate...); err != nil {
|
||||
return errcode.ErrInvalidUsername
|
||||
}
|
||||
|
||||
err := dao.UpdateUser(user)
|
||||
if err != nil {
|
||||
return errcode.ErrInternalServerError
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
35
main.go
Normal file
35
main.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bbs-backend/api/router"
|
||||
"bbs-backend/config"
|
||||
"bbs-backend/dal"
|
||||
"fmt"
|
||||
"github.com/gin-contrib/cors"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load configuration
|
||||
config.LoadConfig("config/config.yaml")
|
||||
|
||||
// Initialize the database
|
||||
dal.InitDB()
|
||||
|
||||
// Migrate and seed the database
|
||||
dal.MigrateAndSeedDB()
|
||||
|
||||
// Setup the router
|
||||
r := router.SetupRouter()
|
||||
|
||||
// 添加 CORS 中间件
|
||||
r.Use(cors.New(cors.Config{
|
||||
AllowOrigins: []string{"http://localhost:8080"}, // 允许的源
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
||||
ExposeHeaders: []string{"Content-Length"},
|
||||
AllowCredentials: true,
|
||||
}))
|
||||
|
||||
// Run the server
|
||||
r.Run(fmt.Sprintf(":%d", config.GetConfig().Server.Port))
|
||||
}
|
14
migrations/alter_posts_table.sql
Normal file
14
migrations/alter_posts_table.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
ALTER TABLE posts
|
||||
ADD COLUMN summary TEXT,
|
||||
ADD COLUMN likes INTEGER DEFAULT 0,
|
||||
ADD COLUMN views INTEGER DEFAULT 0,
|
||||
ADD COLUMN category VARCHAR(50),
|
||||
ADD COLUMN tags TEXT[];
|
||||
|
||||
CREATE TABLE post_likes (
|
||||
id SERIAL PRIMARY KEY,
|
||||
post_id INTEGER REFERENCES posts(id),
|
||||
user_id INTEGER REFERENCES users(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(post_id, user_id)
|
||||
);
|
8
migrations/rollback_posts.sql
Normal file
8
migrations/rollback_posts.sql
Normal file
|
@ -0,0 +1,8 @@
|
|||
DROP TABLE IF EXISTS post_likes;
|
||||
|
||||
ALTER TABLE posts
|
||||
DROP COLUMN IF EXISTS summary,
|
||||
DROP COLUMN IF EXISTS likes,
|
||||
DROP COLUMN IF EXISTS views,
|
||||
DROP COLUMN IF EXISTS category,
|
||||
DROP COLUMN IF EXISTS tags;
|
13
models/init.go
Normal file
13
models/init.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func InitDB() *gorm.DB {
|
||||
// ... 现有连接代码 ...
|
||||
|
||||
db.AutoMigrate(&Post{})
|
||||
|
||||
return db
|
||||
}
|
15
models/post.go
Normal file
15
models/post.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Post struct {
|
||||
ID uint `gorm:"primarykey" json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
AuthorID uint `json:"authorId"`
|
||||
Author User `gorm:"foreignKey:AuthorID" json:"author"`
|
||||
CreateTime time.Time `gorm:"column:create_time" json:"createTime"`
|
||||
UpdateTime time.Time `gorm:"column:update_time" json:"updateTime"`
|
||||
}
|
20
routes/routes.go
Normal file
20
routes/routes.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/your-project/handlers"
|
||||
)
|
||||
|
||||
func SetupRouter(db *gorm.DB) *gin.Engine {
|
||||
router := gin.Default()
|
||||
|
||||
postHandler := handlers.NewPostHandler(db)
|
||||
|
||||
// 帖子相关路由
|
||||
router.GET("/api/posts", postHandler.GetPosts)
|
||||
router.GET("/api/posts/:id", postHandler.GetPostDetail)
|
||||
|
||||
return router
|
||||
}
|
104
src/controllers/PostController.ts
Normal file
104
src/controllers/PostController.ts
Normal file
|
@ -0,0 +1,104 @@
|
|||
class PostController {
|
||||
// ... 现有代码 ...
|
||||
|
||||
async getPosts(req: Request, res: Response) {
|
||||
const page = parseInt(req.query.page as string) || 1;
|
||||
const pageSize = parseInt(req.query.pageSize as string) || 10;
|
||||
const category = req.query.category as string;
|
||||
|
||||
try {
|
||||
const result = await this.postModel.findAll({
|
||||
page,
|
||||
pageSize,
|
||||
category
|
||||
});
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: result
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getPostById(req: Request, res: Response) {
|
||||
const postId = parseInt(req.params.id);
|
||||
const userId = req.user?.id;
|
||||
|
||||
try {
|
||||
const post = await this.postModel.findById(postId, userId);
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: 'success',
|
||||
data: post
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(404).json({
|
||||
code: 404,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async likePost(req: Request, res: Response) {
|
||||
const postId = parseInt(req.params.id);
|
||||
const userId = req.user.id;
|
||||
|
||||
try {
|
||||
await db.query(
|
||||
`INSERT INTO post_likes (post_id, user_id)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT (post_id, user_id) DO NOTHING`,
|
||||
[postId, userId]
|
||||
);
|
||||
|
||||
await db.query(
|
||||
'UPDATE posts SET likes = likes + 1 WHERE id = $1',
|
||||
[postId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async unlikePost(req: Request, res: Response) {
|
||||
const postId = parseInt(req.params.id);
|
||||
const userId = req.user.id;
|
||||
|
||||
try {
|
||||
await db.query(
|
||||
'DELETE FROM post_likes WHERE post_id = $1 AND user_id = $2',
|
||||
[postId, userId]
|
||||
);
|
||||
|
||||
await db.query(
|
||||
'UPDATE posts SET likes = likes - 1 WHERE id = $1',
|
||||
[postId]
|
||||
);
|
||||
|
||||
res.json({
|
||||
code: 200,
|
||||
message: 'success'
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({
|
||||
code: 500,
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
107
src/models/Post.ts
Normal file
107
src/models/Post.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
interface Post {
|
||||
id: number;
|
||||
title: string;
|
||||
content: string;
|
||||
summary: string;
|
||||
authorId: number;
|
||||
createTime: Date;
|
||||
updateTime: Date;
|
||||
likes: number;
|
||||
comments: number;
|
||||
views: number;
|
||||
category: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
class PostModel {
|
||||
// ... 现有代码 ...
|
||||
|
||||
async findAll(params: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
category?: string;
|
||||
}): Promise<{ total: number; posts: Post[] }> {
|
||||
const offset = (params.page - 1) * params.pageSize;
|
||||
const whereClause = params.category ? 'WHERE category = $3' : '';
|
||||
|
||||
const query = `
|
||||
SELECT p.*,
|
||||
u.username, u.avatar,
|
||||
COUNT(c.id) as comments
|
||||
FROM posts p
|
||||
LEFT JOIN users u ON p.author_id = u.id
|
||||
LEFT JOIN comments c ON p.id = c.post_id
|
||||
${whereClause}
|
||||
GROUP BY p.id, u.id
|
||||
ORDER BY p.create_time DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
`;
|
||||
|
||||
const values = params.category
|
||||
? [params.pageSize, offset, params.category]
|
||||
: [params.pageSize, offset];
|
||||
|
||||
const result = await db.query(query, values);
|
||||
const countResult = await db.query(
|
||||
`SELECT COUNT(*) FROM posts ${whereClause}`,
|
||||
params.category ? [params.category] : []
|
||||
);
|
||||
|
||||
return {
|
||||
total: parseInt(countResult.rows[0].count),
|
||||
posts: result.rows.map(this.transformPostRow)
|
||||
};
|
||||
}
|
||||
|
||||
async findById(id: number, userId?: number): Promise<Post & { likedByMe: boolean }> {
|
||||
const query = `
|
||||
SELECT p.*,
|
||||
u.username, u.avatar,
|
||||
COUNT(c.id) as comments,
|
||||
EXISTS(
|
||||
SELECT 1 FROM post_likes pl
|
||||
WHERE pl.post_id = p.id AND pl.user_id = $2
|
||||
) as liked_by_me
|
||||
FROM posts p
|
||||
LEFT JOIN users u ON p.author_id = u.id
|
||||
LEFT JOIN comments c ON p.id = c.post_id
|
||||
WHERE p.id = $1
|
||||
GROUP BY p.id, u.id
|
||||
`;
|
||||
|
||||
const result = await db.query(query, [id, userId || null]);
|
||||
if (!result.rows[0]) {
|
||||
throw new Error('Post not found');
|
||||
}
|
||||
|
||||
// 增加浏览次数
|
||||
await db.query(
|
||||
'UPDATE posts SET views = views + 1 WHERE id = $1',
|
||||
[id]
|
||||
);
|
||||
|
||||
return this.transformPostRow(result.rows[0]);
|
||||
}
|
||||
|
||||
private transformPostRow(row: any): Post & { likedByMe?: boolean } {
|
||||
return {
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
content: row.content,
|
||||
summary: row.summary,
|
||||
author: {
|
||||
id: row.author_id,
|
||||
username: row.username,
|
||||
avatar: row.avatar
|
||||
},
|
||||
createTime: row.create_time.toISOString(),
|
||||
updateTime: row.update_time.toISOString(),
|
||||
likes: row.likes,
|
||||
comments: parseInt(row.comments),
|
||||
views: row.views,
|
||||
category: row.category,
|
||||
tags: row.tags,
|
||||
likedByMe: row.liked_by_me
|
||||
};
|
||||
}
|
||||
}
|
4
src/routes/posts.ts
Normal file
4
src/routes/posts.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
router.get('/posts', postController.getPosts);
|
||||
router.get('/posts/:id', postController.getPostById);
|
||||
router.post('/posts/:id/like', auth, postController.likePost);
|
||||
router.delete('/posts/:id/like', auth, postController.unlikePost);
|
Loading…
Reference in New Issue
Block a user