diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/bbs-backend.iml b/.idea/bbs-backend.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/bbs-backend.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..27d4b21 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/api/controller/bbs_controller.go b/api/controller/bbs_controller.go new file mode 100644 index 0000000..39d8a52 --- /dev/null +++ b/api/controller/bbs_controller.go @@ -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 +} diff --git a/api/controller/post_controller.go b/api/controller/post_controller.go new file mode 100644 index 0000000..de0956c --- /dev/null +++ b/api/controller/post_controller.go @@ -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, + }) +} diff --git a/api/controller/user_controller.go b/api/controller/user_controller.go new file mode 100644 index 0000000..94b752c --- /dev/null +++ b/api/controller/user_controller.go @@ -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"}) +} diff --git a/api/reply/bbs_reply.go b/api/reply/bbs_reply.go new file mode 100644 index 0000000..0c4d762 --- /dev/null +++ b/api/reply/bbs_reply.go @@ -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"` +} diff --git a/api/reply/post_reply.go b/api/reply/post_reply.go new file mode 100644 index 0000000..1c1fc60 --- /dev/null +++ b/api/reply/post_reply.go @@ -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"` +} diff --git a/api/reply/user_reply.go b/api/reply/user_reply.go new file mode 100644 index 0000000..5662956 --- /dev/null +++ b/api/reply/user_reply.go @@ -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"` +} diff --git a/api/request/bbs_request.go b/api/request/bbs_request.go new file mode 100644 index 0000000..b83d12d --- /dev/null +++ b/api/request/bbs_request.go @@ -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"` +} diff --git a/api/request/post_request.go b/api/request/post_request.go new file mode 100644 index 0000000..f1554fe --- /dev/null +++ b/api/request/post_request.go @@ -0,0 +1,6 @@ +package request + +type CreatePostRequest struct { + Title string `json:"title" binding:"required"` + Content string `json:"content" binding:"required"` +} diff --git a/api/request/user_request.go b/api/request/user_request.go new file mode 100644 index 0000000..f97ebb6 --- /dev/null +++ b/api/request/user_request.go @@ -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"` +} diff --git a/api/router/router.go b/api/router/router.go new file mode 100644 index 0000000..e1e501b --- /dev/null +++ b/api/router/router.go @@ -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 +} diff --git a/common/app/app.go b/common/app/app.go new file mode 100644 index 0000000..b175516 --- /dev/null +++ b/common/app/app.go @@ -0,0 +1,3 @@ +package app + +// This is a default file for the app package. diff --git a/common/enum/enum.go b/common/enum/enum.go new file mode 100644 index 0000000..c5a447b --- /dev/null +++ b/common/enum/enum.go @@ -0,0 +1,3 @@ +package enum + +// This is a default file for the enum package. diff --git a/common/errcode/errcode.go b/common/errcode/errcode.go new file mode 100644 index 0000000..49097e8 --- /dev/null +++ b/common/errcode/errcode.go @@ -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") +) diff --git a/common/logger/logger.go b/common/logger/logger.go new file mode 100644 index 0000000..275ab8e --- /dev/null +++ b/common/logger/logger.go @@ -0,0 +1,3 @@ +package logger + +// This is a default file for the logger package. diff --git a/common/middleware/middleware.go b/common/middleware/middleware.go new file mode 100644 index 0000000..b73961b --- /dev/null +++ b/common/middleware/middleware.go @@ -0,0 +1,3 @@ +package middleware + +// This is a default file for the middleware package. diff --git a/common/response/response.go b/common/response/response.go new file mode 100644 index 0000000..f7a99a2 --- /dev/null +++ b/common/response/response.go @@ -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, + }) +} diff --git a/common/util/util.go b/common/util/util.go new file mode 100644 index 0000000..c6be234 --- /dev/null +++ b/common/util/util.go @@ -0,0 +1,3 @@ +package util + +// This is a default file for the util package. diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..6c130c3 --- /dev/null +++ b/config/config.go @@ -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 +} diff --git a/config/config.yaml b/config/config.yaml new file mode 100644 index 0000000..586e6af --- /dev/null +++ b/config/config.yaml @@ -0,0 +1,7 @@ +# Default configuration file for the project +database: + type: sqlite + file: test.db + +server: + port: 8888 diff --git a/dal/cache/cache.go b/dal/cache/cache.go new file mode 100644 index 0000000..3acfd23 --- /dev/null +++ b/dal/cache/cache.go @@ -0,0 +1,3 @@ +package cache + +// This is a default file for the cache package. diff --git a/dal/dao/bbs_dao.go b/dal/dao/bbs_dao.go new file mode 100644 index 0000000..38c76b8 --- /dev/null +++ b/dal/dao/bbs_dao.go @@ -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 +} diff --git a/dal/dao/post_dao.go b/dal/dao/post_dao.go new file mode 100644 index 0000000..629c170 --- /dev/null +++ b/dal/dao/post_dao.go @@ -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 +} diff --git a/dal/dao/user_dao.go b/dal/dao/user_dao.go new file mode 100644 index 0000000..df4eb87 --- /dev/null +++ b/dal/dao/user_dao.go @@ -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 +} diff --git a/dal/db.go b/dal/db.go new file mode 100644 index 0000000..1e4cc6e --- /dev/null +++ b/dal/db.go @@ -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) + } +} diff --git a/dal/init_db.go b/dal/init_db.go new file mode 100644 index 0000000..f07c186 --- /dev/null +++ b/dal/init_db.go @@ -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) + } + } + } +} diff --git a/dal/model/bbs_model.go b/dal/model/bbs_model.go new file mode 100644 index 0000000..672f1cf --- /dev/null +++ b/dal/model/bbs_model.go @@ -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"` +} diff --git a/dal/model/category_model.go b/dal/model/category_model.go new file mode 100644 index 0000000..c1bbc55 --- /dev/null +++ b/dal/model/category_model.go @@ -0,0 +1,12 @@ +package model + +import ( + "gorm.io/gorm" +) + +type Category struct { + gorm.Model + Name string + Description string + Posts []Post +} diff --git a/dal/model/comment_model.go b/dal/model/comment_model.go new file mode 100644 index 0000000..882709e --- /dev/null +++ b/dal/model/comment_model.go @@ -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 +} diff --git a/dal/model/post_model.go b/dal/model/post_model.go new file mode 100644 index 0000000..f27e2d5 --- /dev/null +++ b/dal/model/post_model.go @@ -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 +} diff --git a/dal/model/role_model.go b/dal/model/role_model.go new file mode 100644 index 0000000..2dce417 --- /dev/null +++ b/dal/model/role_model.go @@ -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"` +} diff --git a/dal/model/session_model.go b/dal/model/session_model.go new file mode 100644 index 0000000..526728c --- /dev/null +++ b/dal/model/session_model.go @@ -0,0 +1,12 @@ +package model + +import ( + "gorm.io/gorm" +) + +type Session struct { + gorm.Model + UserID uint + User User + Token string +} diff --git a/dal/model/user_model.go b/dal/model/user_model.go new file mode 100644 index 0000000..736a7ee --- /dev/null +++ b/dal/model/user_model.go @@ -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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..eec2b1b --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..08439ce --- /dev/null +++ b/go.sum @@ -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= diff --git a/handlers/post.go b/handlers/post.go new file mode 100644 index 0000000..b1a7728 --- /dev/null +++ b/handlers/post.go @@ -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, + }) +} diff --git a/logic/appservice/bbs_service.go b/logic/appservice/bbs_service.go new file mode 100644 index 0000000..950824e --- /dev/null +++ b/logic/appservice/bbs_service.go @@ -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 +} diff --git a/logic/appservice/post_service.go b/logic/appservice/post_service.go new file mode 100644 index 0000000..6f11fdc --- /dev/null +++ b/logic/appservice/post_service.go @@ -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 +} diff --git a/logic/appservice/user_service.go b/logic/appservice/user_service.go new file mode 100644 index 0000000..243175f --- /dev/null +++ b/logic/appservice/user_service.go @@ -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 +} diff --git a/logic/do/post_do.go b/logic/do/post_do.go new file mode 100644 index 0000000..fdcb086 --- /dev/null +++ b/logic/do/post_do.go @@ -0,0 +1,8 @@ +package do + +type Post struct { + ID uint + Title string + Content string + Author string +} diff --git a/logic/do/user_do.go b/logic/do/user_do.go new file mode 100644 index 0000000..48e5343 --- /dev/null +++ b/logic/do/user_do.go @@ -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) +} diff --git a/logic/domainservice/bbs_domain_service.go b/logic/domainservice/bbs_domain_service.go new file mode 100644 index 0000000..5a837f7 --- /dev/null +++ b/logic/domainservice/bbs_domain_service.go @@ -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 +} diff --git a/logic/domainservice/post_domain_service.go b/logic/domainservice/post_domain_service.go new file mode 100644 index 0000000..56fc236 --- /dev/null +++ b/logic/domainservice/post_domain_service.go @@ -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 +} diff --git a/logic/domainservice/user_domain_service.go b/logic/domainservice/user_domain_service.go new file mode 100644 index 0000000..6387d5a --- /dev/null +++ b/logic/domainservice/user_domain_service.go @@ -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 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..a40ff8e --- /dev/null +++ b/main.go @@ -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)) +} diff --git a/migrations/alter_posts_table.sql b/migrations/alter_posts_table.sql new file mode 100644 index 0000000..964e60b --- /dev/null +++ b/migrations/alter_posts_table.sql @@ -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) +); \ No newline at end of file diff --git a/migrations/rollback_posts.sql b/migrations/rollback_posts.sql new file mode 100644 index 0000000..158ef36 --- /dev/null +++ b/migrations/rollback_posts.sql @@ -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; \ No newline at end of file diff --git a/models/init.go b/models/init.go new file mode 100644 index 0000000..8729e8b --- /dev/null +++ b/models/init.go @@ -0,0 +1,13 @@ +package models + +import ( + "gorm.io/gorm" +) + +func InitDB() *gorm.DB { + // ... 现有连接代码 ... + + db.AutoMigrate(&Post{}) + + return db +} diff --git a/models/post.go b/models/post.go new file mode 100644 index 0000000..0913c09 --- /dev/null +++ b/models/post.go @@ -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"` +} diff --git a/routes/routes.go b/routes/routes.go new file mode 100644 index 0000000..12655fe --- /dev/null +++ b/routes/routes.go @@ -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 +} diff --git a/src/controllers/PostController.ts b/src/controllers/PostController.ts new file mode 100644 index 0000000..8522579 --- /dev/null +++ b/src/controllers/PostController.ts @@ -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 + }); + } + } +} \ No newline at end of file diff --git a/src/models/Post.ts b/src/models/Post.ts new file mode 100644 index 0000000..27cb29b --- /dev/null +++ b/src/models/Post.ts @@ -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 { + 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 + }; + } +} \ No newline at end of file diff --git a/src/routes/posts.ts b/src/routes/posts.ts new file mode 100644 index 0000000..f8fb175 --- /dev/null +++ b/src/routes/posts.ts @@ -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); \ No newline at end of file