5 Коміти c3ffd20d2b ... 2bb1c5830e

Автор SHA1 Опис Дата
  runningwater 2bb1c5830e feat: 删除分类数据 4 місяців тому
  runningwater 72bc8f0671 feat: 分类列表 4 місяців тому
  runningwater ffb822ae91 feat(Command): seed 命令 4 місяців тому
  runningwater e827194a7e feat: 更新分类 4 місяців тому
  runningwater 79abe57834 feat: 创建分类 4 місяців тому

+ 37 - 0
app/cmd/seed.go

@@ -0,0 +1,37 @@
+// Author: simon (ynwdlxm@163.com)
+// Date: 2025/7/16 11:24
+// Desc: seed 命令
+
+package cmd
+
+import (
+	"github.com/spf13/cobra"
+
+	"github.com/runningwater/gohub/database/seeders"
+	"github.com/runningwater/gohub/pkg/console"
+	"github.com/runningwater/gohub/pkg/seed"
+)
+
+var CmdDBSeed = &cobra.Command{
+	Use:   "seed",
+	Short: "Insert fake data to the database",
+	Run:   runSeeders,
+	Args:  cobra.MaximumNArgs(1), // 只允许一个参数
+}
+
+func runSeeders(cmd *cobra.Command, args []string) {
+	seeders.Initialize()
+	if len(args) > 0 {
+		name := args[0]
+		seeder := seed.GetSeeder(name)
+		if len(seeder.Name) > 0 {
+			seed.RunSeeder(name)
+		} else {
+			console.Error("Seeder not found: " + name)
+		}
+	} else {
+		// 默认运行全部迁移
+		seed.RunAll()
+		console.Success("Done seeding.")
+	}
+}

+ 89 - 0
app/http/controllers/api/v1/categories_controller.go

@@ -0,0 +1,89 @@
+package v1
+
+import (
+	"github.com/runningwater/gohub/app/models/category"
+	"github.com/runningwater/gohub/app/requests"
+	"github.com/runningwater/gohub/pkg/response"
+
+	"github.com/gin-gonic/gin"
+)
+
+type CategoriesController struct {
+	BaseApiController
+}
+
+func (ctrl *CategoriesController) Index(c *gin.Context) {
+	request := requests.PaginationRequest{}
+	if ok := requests.Validate(c, &request, requests.Pagination); !ok {
+		return
+	}
+
+	data, pager := category.Paginate(c, 10)
+
+	response.JSON(c, gin.H{
+		"data":  data,
+		"pager": pager,
+	})
+}
+
+func (ctrl *CategoriesController) Save(c *gin.Context) {
+
+	request := requests.CategoryRequest{}
+	if ok := requests.Validate(c, &request, requests.CategorySave); !ok {
+		return
+	}
+
+	categoryModel := category.Category{
+		Name:        request.Name,
+		Description: request.Description,
+	}
+	categoryModel.Create()
+	if categoryModel.ID > 0 {
+		response.Created(c, categoryModel)
+	} else {
+		response.Abort500(c, "创建失败,请稍后尝试~")
+	}
+}
+
+func (ctrl *CategoriesController) Update(c *gin.Context) {
+
+	// 1. 验证 url 参数 id 是否正确
+	categoryModel := category.Get(c.Param("id"))
+	if categoryModel.ID == 0 {
+		response.Abort404(c)
+		return
+	}
+
+	// 2. 验证请求参数
+	request := requests.CategoryRequest{}
+	if ok := requests.Validate(c, &request, requests.CategorySave); !ok {
+		return
+	}
+
+	// 3. 更新数据
+	categoryModel.Name = request.Name
+	categoryModel.Description = request.Description
+	rowsAffected := categoryModel.Save()
+	if rowsAffected > 0 {
+		response.Data(c, categoryModel)
+	} else {
+		response.Abort500(c, "更新失败,请稍后尝试~")
+	}
+}
+
+func (ctrl *CategoriesController) Delete(c *gin.Context) {
+
+	categoryModel := category.Get(c.Param("id"))
+	if categoryModel.ID == 0 {
+		response.Abort404(c)
+		return
+	}
+
+	rowsAffected := categoryModel.Delete()
+	if rowsAffected > 0 {
+		response.Success(c)
+		return
+	}
+
+	response.Abort500(c, "删除失败,请稍后尝试~")
+}

+ 1 - 1
app/http/controllers/api/v1/users_controller.go

@@ -28,7 +28,7 @@ func (ctrl *UsersController) Index(c *gin.Context) {
 		return
 	}
 	data, pager := user.Paginate(c, 2)
-	response.Data(c, gin.H{
+	response.JSON(c, gin.H{
 		"data":  data,
 		"pager": pager,
 	})

+ 32 - 0
app/requests/category_request.go

@@ -0,0 +1,32 @@
+package requests
+
+import (
+	"github.com/gin-gonic/gin"
+	"github.com/thedevsaddam/govalidator"
+)
+
+type CategoryRequest struct {
+	Name        string `valid:"name" json:"name"`
+	Description string `valid:"description" json:"description,omitempty"`
+}
+
+func CategorySave(data any, c *gin.Context) map[string][]string {
+
+	rules := govalidator.MapData{
+		"name":        []string{"required", "min_cn:2", "max_cn:8", "not_exists:categories,name"},
+		"description": []string{"min_cn:3", "max_cn:255"},
+	}
+	messages := govalidator.MapData{
+		"name": []string{
+			"required:名称为必填项",
+			"min_cn:名称长度需至少 2 个字",
+			"max_cn:名称长度不能超过 8 个字",
+			"not_exists:名称已存在",
+		},
+		"description": []string{
+			"min_cn:描述长度需至少 3 个字",
+			"max_cn:描述长度不能超过 255 个字",
+		},
+	}
+	return validate(data, rules, messages)
+}

+ 32 - 1
app/requests/validators/custom_rules.go

@@ -3,10 +3,13 @@ package validators
 import (
 	"errors"
 	"fmt"
+	"strconv"
 	"strings"
+	"unicode/utf8"
 
-	"github.com/runningwater/gohub/pkg/database"
 	"github.com/thedevsaddam/govalidator"
+
+	"github.com/runningwater/gohub/pkg/database"
 )
 
 func init() {
@@ -56,4 +59,32 @@ func init() {
 		return nil
 
 	})
+
+	// max_cn:8 中文最多 8 个字
+	govalidator.AddCustomRule("max_cn", func(field string, rule string, message string, value any) error {
+		valueLen := utf8.RuneCountInString(value.(string))
+		maxLen, _ := strconv.Atoi(strings.TrimPrefix(rule, "max_cn:"))
+		if valueLen > maxLen {
+			// 如果有自定义错误消息的话,使用自定义消息
+			if message != "" {
+				return errors.New(message)
+			}
+			return fmt.Errorf("%v 长度不能超过 %v 个字", field, maxLen)
+		}
+		return nil
+	})
+
+	// min_cn:8 中文长度最少 8 个字
+	govalidator.AddCustomRule("min_cn", func(field string, rule string, message string, value any) error {
+		valueLen := utf8.RuneCountInString(value.(string))
+		minLen, _ := strconv.Atoi(strings.TrimPrefix(rule, "min_cn:"))
+		if valueLen < minLen {
+			// 如果有自定义错误消息的话,使用自定义消息
+			if message != "" {
+				return errors.New(message)
+			}
+			return fmt.Errorf("%v 长度不能小于 %v 个字", field, minLen)
+		}
+		return nil
+	})
 }

+ 24 - 0
database/factories/category_factory.go

@@ -0,0 +1,24 @@
+// Package factories 存放 category 工厂方法
+package factories
+
+import (
+	"github.com/bxcodec/faker/v4"
+
+	"github.com/runningwater/gohub/app/models/category"
+)
+
+func MakeCategories(times int) []category.Category {
+	var objs []category.Category
+
+	// 设置唯一值, 如 Category 模型中的某个字段需要唯一
+	faker.SetGenerateUniqueValues(true)
+
+	for range times {
+		model := category.Category{
+			Name:        faker.Username(),
+			Description: faker.Sentence(),
+		}
+		objs = append(objs, model)
+	}
+	return objs
+}

+ 31 - 0
database/seeders/category_seeder.go

@@ -0,0 +1,31 @@
+package seeders
+
+import (
+	"fmt"
+
+	"gorm.io/gorm"
+
+	"github.com/runningwater/gohub/database/factories"
+	"github.com/runningwater/gohub/pkg/console"
+	"github.com/runningwater/gohub/pkg/logger"
+	"github.com/runningwater/gohub/pkg/seed"
+)
+
+func init() {
+	// 添加 Seeder
+	seed.Add("CategoriesTableSeeder", func(db *gorm.DB) {
+		// 创建 100 个用户对象
+		categories := factories.MakeCategories(100)
+
+		// 批量插入到数据库
+		result := db.Table("categories").Create(&categories)
+
+		if err := result.Error; err != nil {
+			logger.LogIf(err)
+			return
+		}
+
+		// 打印成功信息
+		console.Success(fmt.Sprintf("Table [%v] %v rows seeded", result.Statement.Table, result.RowsAffected))
+	})
+}

+ 1 - 0
database/seeders/init.go

@@ -9,5 +9,6 @@ func Initialize() {
 	// 设置按顺序执行的 Seeder
 	seed.SetRunOrder([]string{
 		"UsersTableSeeder",
+		"CategoriesTableSeeder",
 	})
 }

+ 2 - 1
database/seeders/users_seeder.go

@@ -3,11 +3,12 @@ package seeders
 import (
 	"fmt"
 
+	"gorm.io/gorm"
+
 	"github.com/runningwater/gohub/database/factories"
 	"github.com/runningwater/gohub/pkg/console"
 	"github.com/runningwater/gohub/pkg/logger"
 	"github.com/runningwater/gohub/pkg/seed"
-	"gorm.io/gorm"
 )
 
 func init() {

+ 45 - 16
gohub.http

@@ -43,11 +43,11 @@ POST {{base_url}}/v1/auth/signup/using-phone HTTP/1.1
 Content-Type: application/json
 
 {
-    "name":"summer",
-    "password":"secret",
-    "password_confirm":"secret",
-    "verify_code": "439665",
-    "phone": "00011059149"
+  "name": "summer",
+  "password": "secret",
+  "password_confirm": "secret",
+  "verify_code": "439665",
+  "phone": "00011059149"
 }
 
 ### 注册用户
@@ -55,11 +55,11 @@ POST {{base_url}}/v1/auth/signup/using-email HTTP/1.1
 Content-Type: application/json
 
 {
-    "name":"summer3",
-    "password":"secret",
-    "password_confirm":"secret",
-    "verify_code": "123123",
-    "email": "summer3@testing.com"
+  "name": "summer3",
+  "password": "secret",
+  "password_confirm": "secret",
+  "verify_code": "123123",
+  "email": "summer3@testing.com"
 }
 
 ### 登录用户
@@ -67,10 +67,10 @@ POST {{base_url}}/v1/auth/login/using-password HTTP/1.1
 Content-Type: application/json
 
 {
-    "captcha_id" :"xTS6AtcgjUVABJj2M9NE",
-    "captcha_answer": "338750",
-    "login_id": "summer",
-    "password": "secret"
+  "captcha_id": "xTS6AtcgjUVABJj2M9NE",
+  "captcha_answer": "338750",
+  "login_id": "summer",
+  "password": "secret"
 }
 
 ### 刷新token
@@ -93,5 +93,34 @@ GET {{base_url}}/v1/auth/user HTTP/1.1
 Authorization: Bearer {{access_token}}
 
 
-### users
-GET {{base_url}}/v1/users HTTP/1.1
+### 用户列表
+GET {{base_url}}/v1/users HTTP/1.1
+
+### 创建分类
+POST {{base_url}}/v1/categories HTTP/1.1
+Authorization: Bearer {{access_token}}
+Content-Type: application/json
+
+{
+  "name": "分类1",
+  "description": "分类1描述"
+}
+
+### 更新分类
+PUT {{base_url}}/v1/categories/1 HTTP/1.1
+Authorization: Bearer {{access_token}}
+Content-Type: application/json
+
+{
+  "name": "新分类名称",
+  "description": "分类1描述"
+}
+
+### 分类列表
+GET {{base_url}}/v1/categories?page=2&sort=id&order=asc&per_page=10 HTTP/1.1
+Content-Type: application/json
+
+### 删除分类
+DELETE {{base_url}}/v1/categories/1 HTTP/1.1
+Authorization: Bearer {{access_token}}
+Content-Type: application/json

+ 1 - 1
http-client.env.json

@@ -1,6 +1,6 @@
 {
   "dev": {
     "base_url": "http://127.0.0.1:3000",
-    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsInVzZXJfbmFtZSI6InN1bW1lciIsImV4cGlyZV90aW1lIjoxNzU3NzMyNTk3LCJpc3MiOiJHb2h1YiIsInN1YiI6IjEiLCJhdWQiOlsic3VtbWVyIl0sImV4cCI6MTc1NzczMjU5NywibmJmIjoxNzUyNTQ4NTk3LCJpYXQiOjE3NTI1NDg1OTd9.4bE5RE1saBymOUuMewdAUKmy5U6AKM_tc4hQyH9lrk4"
+    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMSIsInVzZXJfbmFtZSI6InN1bW1lciIsImV4cGlyZV90aW1lIjoxNzU3ODE2NDI3LCJpc3MiOiJHb2h1YiIsInN1YiI6IjEiLCJhdWQiOlsic3VtbWVyIl0sImV4cCI6MTc1NzgxNjQyNywibmJmIjoxNzUyNjMyNDI3LCJpYXQiOjE3NTI2MzI0Mjd9.9tYiBN5J4PVblgM1vk09s8sPq2Hqsvm5YimJcV19iaw"
   }
 }

+ 1 - 0
main.go

@@ -50,6 +50,7 @@ func main() {
 		cmd.CmdKey,
 		cmd.CmdPlay,
 		cmd.CmdMigrate,
+		cmd.CmdDBSeed,
 		// cmd.CmdTestCommand,
 
 		make.CmdMake,

+ 4 - 4
pkg/captcha/store_redis.go

@@ -10,13 +10,13 @@ import (
 )
 
 // RedisStore is a store for captcha using Redis.
-// It implements the Store interface. base64Captcha.Store interface.
+// It implements the Save interface. base64Captcha.Store interface.
 type RedisStore struct {
 	RedisClient *redis.RedisClient
 	KeyPrefix   string
 }
 
-// Set 实现 base64Captcha.Store.Set 方法
+// Set 实现 base64Captcha.Save.Set 方法
 func (s *RedisStore) Set(key string, value string) error {
 	expiretime := time.Minute * time.Duration(config.GetInt64("captcha.expire_time"))
 	// 方便本地开发调试
@@ -30,7 +30,7 @@ func (s *RedisStore) Set(key string, value string) error {
 	return nil
 }
 
-// Get 实现 base64Captcha.Store.Get 方法
+// Get 实现 base64Captcha.Save.Get 方法
 func (s *RedisStore) Get(key string, clear bool) string {
 	key = s.KeyPrefix + key
 	val := s.RedisClient.Get(key)
@@ -40,7 +40,7 @@ func (s *RedisStore) Get(key string, clear bool) string {
 	return val
 }
 
-// Verify 实现 base64Captcha.Store.Verify 方法
+// Verify 实现 base64Captcha.Save.Verify 方法
 func (s *RedisStore) Verify(key, answer string, clear bool) bool {
 	v := s.Get(key, clear)
 	return v == answer

+ 1 - 1
pkg/console/console.go

@@ -43,5 +43,5 @@ func ExitIf(err error) {
 }
 
 func colorOut(msg, color string) {
-	fmt.Fprintln(os.Stdout, ansi.Color(msg, color))
+	_, _ = fmt.Fprintln(os.Stdout, ansi.Color(msg, color))
 }

+ 1 - 1
pkg/migrate/migrator.go

@@ -24,7 +24,7 @@ func (m *Migrator) createMigrationsTable() {
 
 	if !m.Migrator.HasTable(&migration) {
 		// 如果表不存在,则创建表
-		if err := m.DB.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci").AutoMigrate(&migration); err != nil {
+		if err := m.DB.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci").Migrator().CreateTable(&migration); err != nil {
 			console.ExitIf(err)
 			return
 		}

+ 53 - 4
pkg/seed/seeder.go

@@ -1,10 +1,15 @@
 // Package seed 存放数据库填充数据
 package seed
 
-import "gorm.io/gorm"
+import (
+	"gorm.io/gorm"
+
+	"github.com/runningwater/gohub/pkg/console"
+	"github.com/runningwater/gohub/pkg/database"
+)
 
 // 存放所有 Seeder
-var seedrs []Seeder
+var seeders []Seeder
 
 // 按排序执行的 Seeder 数组
 var orderedSeederNames []string
@@ -17,9 +22,9 @@ type Seeder struct {
 	Func SeederFunc
 }
 
-// AddSeeder 添加一个 Seeder
+// Add AddSeeder 添加一个 Seeder
 func Add(name string, fn SeederFunc) {
-	seedrs = append(seedrs, Seeder{
+	seeders = append(seeders, Seeder{
 		Name: name,
 		Func: fn,
 	})
@@ -29,3 +34,47 @@ func Add(name string, fn SeederFunc) {
 func SetRunOrder(names []string) {
 	orderedSeederNames = names
 }
+
+// GetSeeder 获取 Seeder
+func GetSeeder(name string) Seeder {
+	for _, seeder := range seeders {
+		if seeder.Name == name {
+			return seeder
+		}
+	}
+	return Seeder{}
+}
+
+// RunAll 执行所有 Seeder
+func RunAll() {
+	// 先运行 ordered 的
+	executed := make(map[string]string)
+	for _, name := range orderedSeederNames {
+		seeder := GetSeeder(name)
+		if len(seeder.Name) > 0 {
+			if seeder.Func != nil {
+				seeder.Func(database.DB)
+				executed[name] = name
+			}
+		}
+	}
+
+	// 再运行剩下的
+	for _, seeder := range seeders {
+		if _, ok := executed[seeder.Name]; !ok {
+			if seeder.Func != nil {
+				console.Warning("Running Seeder: " + seeder.Name)
+				seeder.Func(database.DB)
+			}
+		}
+	}
+}
+
+// RunSeeder 运行指定的 Seeder
+func RunSeeder(name string) {
+	seeder := GetSeeder(name)
+	if seeder.Func != nil {
+		console.Warning("Running Seeder: " + seeder.Name)
+		seeder.Func(database.DB)
+	}
+}

+ 9 - 1
routes/api.go

@@ -18,7 +18,6 @@ func RegisterAPIRoutes(router *gin.Engine) {
 	// 作为参考 Github API 每小时最多 60 个请求(根据 IP)。
 	// 测试时,可以调高一点。
 	v1.Use(middlewares.LimitIP("200-H"))
-
 	{
 		uc := new(controllers.UsersController)
 
@@ -59,5 +58,14 @@ func RegisterAPIRoutes(router *gin.Engine) {
 		{
 			userGroup.GET("", uc.Index)
 		}
+
+		cgc := new(controllers.CategoriesController)
+		categoryGroup := v1.Group("/categories")
+		{
+			categoryGroup.GET("", cgc.Index)
+			categoryGroup.POST("", middlewares.AuthJWT(), cgc.Save)
+			categoryGroup.PUT("/:id", middlewares.AuthJWT(), cgc.Update)
+			categoryGroup.DELETE("/:id", middlewares.AuthJWT(), cgc.Delete)
+		}
 	}
 }