3 Incheckningar 2bb1c5830e ... 7c51b9b432

Upphovsman SHA1 Meddelande Datum
  runningwater 7c51b9b432 fix: fix typing error 4 månader sedan
  runningwater f5f8af3939 feat: 创建话题接口 4 månader sedan
  runningwater a5264466a9 feat: 话题模型和迁移 4 månader sedan

+ 40 - 0
app/http/controllers/api/v1/topics_controller.go

@@ -0,0 +1,40 @@
+package v1
+
+import (
+	"github.com/runningwater/gohub/app/models/topic"
+	"github.com/runningwater/gohub/app/requests"
+	"github.com/runningwater/gohub/pkg/auth"
+	"github.com/runningwater/gohub/pkg/response"
+
+	"github.com/gin-gonic/gin"
+)
+
+type TopicsController struct {
+	BaseApiController
+}
+
+// func (ctrl *TopicsController) Index(c *gin.Context) {
+//     topics := topic.All()
+//     response.Data(c, topics)
+// }
+
+func (ctrl *TopicsController) Store(c *gin.Context) {
+
+	request := requests.TopicRequest{}
+	if ok := requests.Validate(c, &request, requests.TopicSave); !ok {
+		return
+	}
+
+	topicModel := topic.Topic{
+		Title:      request.Title,
+		Body:       request.Body,
+		CategoryID: request.CategoryID,
+		UserID:     auth.CurrentUID(c),
+	}
+	topicModel.Create()
+	if topicModel.ID > 0 {
+		response.Created(c, topicModel)
+	} else {
+		response.Abort500(c, "创建失败,请稍后尝试~")
+	}
+}

+ 9 - 8
app/http/middlewares/logger.go

@@ -6,10 +6,11 @@ import (
 	"time"
 
 	"github.com/gin-gonic/gin"
-	"github.com/runningwater/gohub/pkg/helpers"
-	"github.com/runningwater/gohub/pkg/logger"
 	"github.com/spf13/cast"
 	"go.uber.org/zap"
+
+	"github.com/runningwater/gohub/pkg/helpers"
+	"github.com/runningwater/gohub/pkg/logger"
 )
 
 type responseBodyWriter struct {
@@ -45,10 +46,10 @@ func Logger() gin.HandlerFunc {
 
 		// 开始记录日志的逻辑
 		cost := time.Since(start)
-		responStatus := c.Writer.Status()
+		responseStatus := c.Writer.Status()
 
 		logFields := []zap.Field{
-			zap.Int("status", responStatus),
+			zap.Int("status", responseStatus),
 			zap.String("request", c.Request.Method+c.Request.URL.String()),
 			zap.String("query", c.Request.URL.RawQuery),
 			zap.String("ip", c.ClientIP()),
@@ -63,11 +64,11 @@ func Logger() gin.HandlerFunc {
 			logFields = append(logFields, zap.String("ResponseBody", w.body.String()))
 		}
 
-		if responStatus > 400 && responStatus < 500 {
+		if responseStatus > 400 && responseStatus < 500 {
 			// 403 404 等客户端错误,使用 Warn 级别
-			logger.Warn("HTTP Warning "+cast.ToString(responStatus), logFields...)
-		} else if responStatus >= 500 && responStatus < 600 {
-			logger.Error("HTTP Error "+cast.ToString(responStatus), logFields...)
+			logger.Warn("HTTP Warning "+cast.ToString(responseStatus), logFields...)
+		} else if responseStatus >= 500 && responseStatus < 600 {
+			logger.Error("HTTP Error "+cast.ToString(responseStatus), logFields...)
 		} else {
 			logger.Debug("HTTP Access Log", logFields...)
 		}

+ 1 - 1
app/models/model.go

@@ -7,7 +7,7 @@ import (
 
 // BaseModel 基础模型
 type BaseModel struct {
-	ID uint `gorm:"column:id;primaryKey;autoIncrement;" json:"id,omitempty"` // 主键ID
+	ID uint `gorm:"column:id;primaryKey;autoIncrement;comment:主键ID" json:"id,omitempty"` // 主键ID
 }
 
 type CommonTimestampsField struct {

+ 11 - 0
app/models/topic/topic_hooks.go

@@ -0,0 +1,11 @@
+package topic
+
+// func (topic *Topic) BeforeSave(tx *gorm.DB) (err error) {}
+// func (topic *Topic) BeforeCreate(tx *gorm.DB) (err error) {}
+// func (topic *Topic) AfterCreate(tx *gorm.DB) (err error) {}
+// func (topic *Topic) BeforeUpdate(tx *gorm.DB) (err error) {}
+// func (topic *Topic) AfterUpdate(tx *gorm.DB) (err error) {}
+// func (topic *Topic) AfterSave(tx *gorm.DB) (err error) {}
+// func (topic *Topic) BeforeDelete(tx *gorm.DB) (err error) {}
+// func (topic *Topic) AfterDelete(tx *gorm.DB) (err error) {}
+// func (topic *Topic) AfterFind(tx *gorm.DB) (err error) {}

+ 39 - 0
app/models/topic/topic_model.go

@@ -0,0 +1,39 @@
+// Package topic 模型
+package topic
+
+import (
+	"github.com/runningwater/gohub/app/models"
+	"github.com/runningwater/gohub/app/models/category"
+	"github.com/runningwater/gohub/app/models/user"
+	"github.com/runningwater/gohub/pkg/database"
+)
+
+type Topic struct {
+	models.BaseModel
+
+	Title      string `json:"title,omitempty"`
+	Body       string `json:"body,omitempty"`
+	UserID     string `json:"user_id,omitempty"`
+	CategoryID string `json:"category_id,omitempty"`
+
+	// 通过 user_id 关联用户
+	User user.User `json:"user,omitempty"`
+	// 通过 category_id 关联分类
+	Category category.Category `json:"category,omitempty"`
+
+	models.CommonTimestampsField
+}
+
+func (topic *Topic) Create() {
+	database.DB.Create(&topic)
+}
+
+func (topic *Topic) Save() (rowsAffected int64) {
+	result := database.DB.Save(&topic)
+	return result.RowsAffected
+}
+
+func (topic *Topic) Delete() (rowsAffected int64) {
+	result := database.DB.Delete(&topic)
+	return result.RowsAffected
+}

+ 42 - 0
app/models/topic/topic_util.go

@@ -0,0 +1,42 @@
+package topic
+
+import (
+	"github.com/gin-gonic/gin"
+
+	"github.com/runningwater/gohub/pkg/app"
+	"github.com/runningwater/gohub/pkg/database"
+	"github.com/runningwater/gohub/pkg/paginator"
+)
+
+func Get(idStr string) (topic Topic) {
+	database.DB.Where("id", idStr).First(&topic)
+	return
+}
+
+func GetBy(field, value string) (topic Topic) {
+	database.DB.Where("? = ?", field, value).First(&topic)
+	return
+}
+
+func All() (topics []Topic) {
+	database.DB.Find(&topics)
+	return
+}
+
+func IsExist(field, value string) bool {
+	var count int64
+	database.DB.Model(Topic{}).Where("? = ?", field, value).Count(&count)
+	return count > 0
+}
+
+// Paginate 分页内容
+func Paginate(c *gin.Context, pageSize int) (topics []Topic, paging paginator.Paging) {
+	paging = paginator.Paginate(
+		c,
+		database.DB.Model(Topic{}),
+		&topics,
+		app.V1URL(database.TableName(&Topic{})),
+		pageSize,
+	)
+	return
+}

+ 27 - 0
app/requests/topic_request.go

@@ -0,0 +1,27 @@
+package requests
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/thedevsaddam/govalidator"
+)
+
+type TopicRequest struct {
+	Title      string `json:"title,omitempty" valid:"title"`
+	Body       string `json:"body,omitempty" valid:"body"`
+	CategoryID string `json:"category_id,omitempty" valid:"category_id"`
+}
+
+func TopicSave(data any, c *gin.Context) map[string][]string {
+
+	rules := govalidator.MapData{
+		"title":       []string{"required", "min_cn:3", "max_cn:40"},
+		"body":        []string{"required", "min_cn:10", "max_cn:50000"},
+		"category_id": []string{"required", "exists:categories,id"},
+	}
+	messages := govalidator.MapData{
+		"title":       []string{"required:标题为必填项", "min_cn:标题长度需至少 3 个字符", "max_cn:标题长度不能超过 40 个字符"},
+		"body":        []string{"required:内容为必填项", "min_cn:内容长度需至少 10 个字符", "max_cn:内容长度不能超过 50000 个字符"},
+		"category_id": []string{"required:分类为必填项", "exists:分类不存在"},
+	}
+	return validate(data, rules, messages)
+}

+ 29 - 0
app/requests/validators/custom_rules.go

@@ -87,4 +87,33 @@ func init() {
 		}
 		return nil
 	})
+
+	// 自定义规则 exists, 确保数据库存在某条数据
+	// exists:categories,id
+	govalidator.AddCustomRule("exists", func(field string, rule string, message string, value any) error {
+		rng := strings.Split(strings.TrimPrefix(rule, "exists:"), ",")
+
+		// 第一个参数,表名称,如 categories
+		tableName := rng[0]
+		// 第二个参数,字段名称,如 id
+		fieldName := rng[1]
+
+		// 用户请求的值
+		requestValue := value.(string)
+
+		// 调用数据库查询方法,检查数据是否存在
+		var count int64
+		database.DB.Table(tableName).Where(fieldName+" = ?", requestValue).Count(&count)
+
+		// 如果 count 小于等于 0,表示数据不存在,返回错误信息
+		if count <= 0 {
+			// 如果有自定义错误消息的话,使用自定义消息
+			if message != "" {
+				return errors.New(message)
+			}
+			return fmt.Errorf("%v 不存在", requestValue)
+		}
+
+		return nil
+	})
 }

+ 44 - 0
database/migrations/2025_07_16_155731_add_topics_table.go

@@ -0,0 +1,44 @@
+package migrations
+
+import (
+	"gorm.io/gorm"
+
+	"github.com/runningwater/gohub/app/models"
+	"github.com/runningwater/gohub/pkg/migrate"
+)
+
+func init() {
+
+	type User struct {
+		models.BaseModel
+	}
+
+	type Category struct {
+		models.BaseModel
+	}
+
+	type Topic struct {
+		models.BaseModel
+
+		Title      string `gorm:"type:varchar(255);not null;index;comment:标题"`
+		Body       string `gorm:"type:longtext;not null;comment:内容"`
+		UserID     uint   `gorm:"type:bigint;not null; index;comment:用户ID"`
+		CategoryID uint   `gorm:"type:bigint;not null; index;comment:分类ID"`
+
+		// 创建 user_id 和 category_id 外键关联
+		User     User
+		Category Category
+
+		models.CommonTimestampsField
+	}
+
+	up := func(migrator gorm.Migrator, DB *gorm.DB) {
+		_ = DB.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci").AutoMigrate(&Topic{})
+	}
+
+	down := func(migrator gorm.Migrator, DB *gorm.DB) {
+		_ = migrator.DropTable(&Topic{})
+	}
+
+	migrate.Add(up, down, "2025_07_16_155731_add_topics_table")
+}

+ 12 - 1
gohub.http

@@ -123,4 +123,15 @@ Content-Type: application/json
 ### 删除分类
 DELETE {{base_url}}/v1/categories/1 HTTP/1.1
 Authorization: Bearer {{access_token}}
-Content-Type: application/json
+Content-Type: application/json
+
+### 创建话题
+POST {{base_url}}/v1/topics HTTP/1.1
+Authorization: Bearer {{access_token}}
+Content-Type: application/json
+
+{
+  "title": "我的的帖子2",
+  "body": "话题1内容, 这里是帖子描述内容帖子描述内容",
+  "category_id": "3"
+}

+ 7 - 6
pkg/logger/logger.go

@@ -8,10 +8,11 @@ import (
 	"strings"
 	"time"
 
-	"github.com/runningwater/gohub/pkg/app"
 	"go.uber.org/zap"
 	"go.uber.org/zap/zapcore"
 	"gopkg.in/natefinch/lumberjack.v2"
+
+	"github.com/runningwater/gohub/pkg/app"
 )
 
 // Logger 全局日志对象
@@ -33,7 +34,7 @@ func Init(filename string, maxSize, maxBackup, maxAge int, compress bool, logTyp
 	Logger = zap.New(core,
 		zap.AddCaller(),      // 调用文件和行号,内部使用 runtime.Caller
 		zap.AddCallerSkip(1), // 调用文件和行号,内部使用 runtime.Caller
-		//zap.Development(), // 开发模式,堆栈跟踪
+		// zap.Development(), // 开发模式,堆栈跟踪
 		zap.AddStacktrace(zapcore.ErrorLevel), // 记录错误级别以上的堆栈信息
 	)
 
@@ -80,8 +81,8 @@ func customTimeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
 func getLogWriter(filename string, maxSize, maxBackup, maxAge int, compress bool, logType string) zapcore.WriteSyncer {
 	// 如果配置了按日期记录日志,则使用按日期记录日志
 	if logType == "daily" {
-		logname := time.Now().Format("2006-01-02") + ".log"
-		filename = strings.Replace(filename, "logs.log", logname, 1)
+		logName := time.Now().Format("2006-01-02") + ".log"
+		filename = strings.Replace(filename, "logs.log", logName, 1)
 	}
 
 	// 滚动日志,详见 config/log.go
@@ -169,8 +170,8 @@ func Fatal(msg string, fields ...zap.Field) {
 // DebugString 记录一条字符串类型的 debug 日志
 //
 //	logger.DebugString("User", "name", "John")
-func DebugString(modeleName, name, msg string) {
-	Logger.Debug(modeleName, zap.String(name, msg))
+func DebugString(moduleName, name, msg string) {
+	Logger.Debug(moduleName, zap.String(name, msg))
 }
 
 func InfoString(moduleName, name, msg string) {

+ 4 - 2
pkg/response/response.go

@@ -2,11 +2,13 @@
 package response
 
 import (
+	"errors"
 	"net/http"
 
 	"github.com/gin-gonic/gin"
-	"github.com/runningwater/gohub/pkg/logger"
 	"gorm.io/gorm"
+
+	"github.com/runningwater/gohub/pkg/logger"
 )
 
 // JSON 响应 200 和 JSON 数据
@@ -83,7 +85,7 @@ func Error(c *gin.Context, err error, msg ...string) {
 	logger.LogIf(err)
 
 	// error 类型为『数据库未找到内容』
-	if err == gorm.ErrRecordNotFound {
+	if errors.Is(err, gorm.ErrRecordNotFound) {
 		Abort404(c)
 		return
 	}

+ 5 - 1
routes/api.go

@@ -58,7 +58,6 @@ func RegisterAPIRoutes(router *gin.Engine) {
 		{
 			userGroup.GET("", uc.Index)
 		}
-
 		cgc := new(controllers.CategoriesController)
 		categoryGroup := v1.Group("/categories")
 		{
@@ -67,5 +66,10 @@ func RegisterAPIRoutes(router *gin.Engine) {
 			categoryGroup.PUT("/:id", middlewares.AuthJWT(), cgc.Update)
 			categoryGroup.DELETE("/:id", middlewares.AuthJWT(), cgc.Delete)
 		}
+		tpc := new(controllers.TopicsController)
+		tpcGroup := v1.Group("/topics")
+		{
+			tpcGroup.POST("", middlewares.AuthJWT(), tpc.Store)
+		}
 	}
 }