4 Incheckningar c28ba06818 ... 8c6699e87a

Upphovsman SHA1 Meddelande Datum
  runningwater 8c6699e87a feat: 上传头像 3 månader sedan
  runningwater 105e3923c3 feat: 编辑个人资料 3 månader sedan
  runningwater 3f701ef103 feat: 编辑个人资料 3 månader sedan
  runningwater f8184a4cdf fix: fix format 3 månader sedan

+ 1 - 1
.air.toml

@@ -7,7 +7,7 @@ tmp_dir = "tmp"
   bin = "./tmp/gohub"
   cmd = "go build -o ./tmp/gohub ."
   delay = 1000
-  exclude_dir = ["assets", "tmp", "vendor", "testdata"]
+  exclude_dir = ["assets", "tmp", "vendor", "testdata","storage"]
   exclude_file = []
   exclude_regex = ["_test.go"]
   exclude_unchanged = false

+ 1 - 0
.gitignore

@@ -3,6 +3,7 @@ tmp
 gohub
 .DS_Store
 .history
+*.rdb
 
 # Golang #
 ######################

+ 28 - 28
LICENSE

@@ -1,28 +1,28 @@
-BSD 3-Clause License
-
-Copyright (c) 2025, runningwater
-
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
-
-1. Redistributions of source code must retain the above copyright notice, this
-   list of conditions and the following disclaimer.
-
-2. Redistributions in binary form must reproduce the above copyright notice,
-   this list of conditions and the following disclaimer in the documentation
-   and/or other materials provided with the distribution.
-
-3. Neither the name of the copyright holder nor the names of its
-   contributors may be used to endorse or promote products derived from
-   this software without specific prior written permission.
-
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+BSD 3-Clause License
+
+Copyright (c) 2025, runningwater
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+   contributors may be used to endorse or promote products derived from
+   this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

+ 4 - 0
README.md

@@ -52,6 +52,7 @@ UNIQUE KEY `migration` (`migration`)
 
 #### 🐛 Bug 修复
 
+- Fix format
 - Fix typing error
 - Fix automigrate charset for mysql
 - Golangci-lint run fix
@@ -76,6 +77,9 @@ UNIQUE KEY `migration` (`migration`)
 
 #### 🚀 新功能
 
+- 上传头像
+- 编辑个人资料
+- 编辑个人资料
 - *(Command)* Cache forget 命令
 - *(Command)* Cache clear 命令
 - 缓存友情链接列表

+ 2 - 1
app/cmd/serve.go

@@ -2,11 +2,12 @@ package cmd
 
 import (
 	"github.com/gin-gonic/gin"
+	"github.com/spf13/cobra"
+
 	"github.com/runningwater/gohub/bootstrap"
 	"github.com/runningwater/gohub/pkg/config"
 	"github.com/runningwater/gohub/pkg/console"
 	"github.com/runningwater/gohub/pkg/logger"
-	"github.com/spf13/cobra"
 )
 
 var CmdServe = &cobra.Command{

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

@@ -4,6 +4,8 @@ import (
 	"github.com/runningwater/gohub/app/models/user"
 	"github.com/runningwater/gohub/app/requests"
 	"github.com/runningwater/gohub/pkg/auth"
+	"github.com/runningwater/gohub/pkg/config"
+	"github.com/runningwater/gohub/pkg/file"
 	"github.com/runningwater/gohub/pkg/response"
 
 	"github.com/gin-gonic/gin"
@@ -21,7 +23,6 @@ func (ctrl *UsersController) CurrentUser(c *gin.Context) {
 
 // Index 所有用户
 func (ctrl *UsersController) Index(c *gin.Context) {
-	// data := user.All()
 	// 输入参数校验
 	request := requests.PaginationRequest{}
 	if ok := requests.Validate(c, &request, requests.Pagination); !ok {
@@ -33,3 +34,56 @@ func (ctrl *UsersController) Index(c *gin.Context) {
 		"pager": pager,
 	})
 }
+
+// UpdateProfile 更新用户信息
+func (ctrl *UsersController) UpdateProfile(c *gin.Context) {
+	request := requests.UserUpdateProfileRequest{}
+	if ok := requests.Validate(c, &request, requests.UserUpdateProfile); !ok {
+		return
+	}
+	currentUser := auth.CurrentUser(c)
+	currentUser.Name = request.Name
+	currentUser.City = request.City
+	currentUser.Introduction = request.Introduction
+	affected := currentUser.Save()
+	if affected > 0 {
+		response.Data(c, currentUser)
+	} else {
+		response.Abort500(c, "更新失败,请稍后尝试")
+	}
+}
+
+// UpdateEmail 更新用户邮箱
+func (ctrl *UsersController) UpdateEmail(c *gin.Context) {
+	request := requests.UserUpdateEmailRequest{}
+	if ok := requests.Validate(c, &request, requests.UserUpdateEmail); !ok {
+		return
+	}
+	currentUser := auth.CurrentUser(c)
+	currentUser.Email = request.Email
+	affected := currentUser.Save()
+	if affected > 0 {
+		response.Success(c)
+		return
+	}
+	response.Abort500(c, "更新失败,请稍后尝试")
+}
+
+// UpdateAvatar 更新用户头像
+func (ctrl *UsersController) UpdateAvatar(c *gin.Context) {
+	request := requests.UserUpdateAvatarRequest{}
+	if ok := requests.Validate(c, &request, requests.UserUpdateAvatar); !ok {
+		return
+	}
+	avatar, err := file.SaveUploadAvatar(c, request.Avatar)
+	if err != nil {
+		response.Abort500(c, "上传头像失败,请稍后尝试")
+		return
+	}
+
+	currentUser := auth.CurrentUser(c)
+	currentUser.Avatar = config.GetString("app.url") + "/" + avatar
+	_ = currentUser.Save()
+
+	response.Data(c, currentUser)
+}

+ 6 - 1
app/models/user/user_model.go

@@ -11,7 +11,12 @@ import (
 type User struct {
 	models.BaseModel
 
-	Name     string `json:"name,omitempty"`
+	Name string `json:"name,omitempty"`
+
+	City         string `json:"city,omitempty"`
+	Introduction string `json:"introduction,omitempty"`
+	Avatar       string `json:"avatar,omitempty"`
+
 	Email    string `json:"-"`
 	Phone    string `json:"-"`
 	Password string `json:"-"`

+ 27 - 6
app/requests/requests.go

@@ -3,8 +3,9 @@ package requests
 
 import (
 	"github.com/gin-gonic/gin"
-	"github.com/runningwater/gohub/pkg/response"
 	"github.com/thedevsaddam/govalidator"
+
+	"github.com/runningwater/gohub/pkg/response"
 )
 
 // ValidatorFunc 定义验证函数类型
@@ -52,11 +53,12 @@ func Validate(c *gin.Context, obj any, handler ValidatorFunc) bool {
 }
 
 // validate 函数用于执行数据验证
-// 它接受数据、验证规则和自定义错误信息,并返回验证结果
-// data: 需要验证的数据
-// rules: 验证规则
-// messages: 自定义错误信息
-// 返回值是一个映射,包含字段名和对应的错误信息
+//
+//	它接受数据、验证规则和自定义错误信息,并返回验证结果
+//	data: 需要验证的数据
+//	rules: 验证规则
+//	messages: 自定义错误信息
+//	返回值是一个映射,包含字段名和对应的错误信息
 func validate(data any, rules, messages govalidator.MapData) map[string][]string {
 
 	opts := govalidator.Options{
@@ -68,3 +70,22 @@ func validate(data any, rules, messages govalidator.MapData) map[string][]string
 
 	return govalidator.New(opts).ValidateStruct()
 }
+
+// validateFile 函数用于验证文件上传
+//
+//	它接受上下文对象、数据、验证规则和自定义错误信息,并返回验证结果
+//	c: gin.Context 上下文对象
+//	data: 需要验证的数据
+//	rules: 验证规则
+//	messages: 自定义错误信息
+//	返回值是一个映射,包含字段名和对应的错误信息
+func validateFile(c *gin.Context, data any, rules, messages govalidator.MapData) map[string][]string {
+	opts := govalidator.Options{
+		Request:       c.Request,
+		Rules:         rules,
+		TagIdentifier: "valid", // 使用结构体中的valid标签
+		Messages:      messages,
+	}
+
+	return govalidator.New(opts).Validate()
+}

+ 115 - 0
app/requests/user_request.go

@@ -0,0 +1,115 @@
+package requests
+
+import (
+	"mime/multipart"
+
+	"github.com/gin-gonic/gin"
+	"github.com/thedevsaddam/govalidator"
+
+	"github.com/runningwater/gohub/app/requests/validators"
+	"github.com/runningwater/gohub/pkg/auth"
+)
+
+type UserUpdateProfileRequest struct {
+	Name         string `valid:"name" json:"name"`
+	City         string `valid:"city" json:"city"`
+	Introduction string `valid:"introduction" json:"introduction"`
+}
+
+func UserUpdateProfile(data any, c *gin.Context) map[string][]string {
+	// 查询用户名重复时,过滤当前用户ID
+	uid := auth.CurrentUID(c)
+	rules := govalidator.MapData{
+		"name":         []string{"required", "alpha_num", "between:3,20", "not_exists:users,name," + uid},
+		"city":         []string{"min_cn:2", "max_cn:20"},
+		"introduction": []string{"min_cn:4", "max_cn:240"},
+	}
+	messages := govalidator.MapData{
+		"name": []string{
+			"required:用户名不能为空",
+			"alpha_num:用户名格式错误",
+			"between:3,20:用户名长度需在 3-20 之间",
+			"not_exists:用户名已存在",
+		},
+		"city": []string{
+			"min_cn:城市长度需至少 2 个字符",
+			"max_cn:城市长度不能超过 20 个字符",
+		},
+		"introduction": []string{
+			"min_cn:简介长度需至少 4 个字符",
+			"max_cn:简介长度不能超过 240 个字符",
+		},
+	}
+	return validate(data, rules, messages)
+}
+
+type UserUpdateEmailRequest struct {
+	Email      string `valid:"email" json:"email"`
+	VerifyCode string `valid:"verify_code" json:"verify_code,omitempty"`
+}
+
+// UserUpdateEmail 处理用户邮箱更新请求
+// 参数:
+//
+//	data - 请求数据,应为 UserUpdateEmailRequest 类型
+//	c - Gin 上下文对象
+//
+// 返回值:
+//
+//	map[string][]string - 包含验证错误的映射
+func UserUpdateEmail(data any, c *gin.Context) map[string][]string {
+	currentUser := auth.CurrentUser(c)
+	rules := govalidator.MapData{
+		"email": []string{
+			"required",
+			"min:4",
+			"max:30",
+			"email",
+			"not_exists:users,email," + currentUser.GetStringID(),
+			"not_in:" + currentUser.Email,
+		},
+		"verify_code": []string{"required", "digits:6"},
+	}
+	messages := govalidator.MapData{
+		"email": []string{
+			"required:邮箱不能为空",
+			"min:邮箱长度需至少 4 个字符",
+			"max:邮箱长度不能超过 30 个字符",
+			"email:邮箱格式错误",
+			"not_exists:邮箱已存在",
+			"not_in:新邮箱与当前邮箱相同",
+		},
+		"verify_code": []string{
+			"required:验证码不能为空",
+			"digits:6:验证码长度必须为 6 位数字",
+		},
+	}
+	errs := validate(data, rules, messages)
+	_data := data.(*UserUpdateEmailRequest)
+	errs = validators.ValidateVerifyCode(_data.Email, _data.VerifyCode, errs)
+
+	return errs
+}
+
+type UserUpdateAvatarRequest struct {
+	Avatar *multipart.FileHeader `valid:"avatar" form:"avatar"`
+}
+
+func UserUpdateAvatar(data any, c *gin.Context) map[string][]string {
+	// size 的单位为 bytes
+	// - 1024 bytes 为 1 KB
+	// - 1024 * 1024 bytes 为 1 MB
+	// - 1024 * 1024 * 1024 bytes 为 1 GB
+	// - 20971520 bytes 为 20 MB
+	rules := govalidator.MapData{
+		"file:avatar": []string{"ext:png,jpg,jpeg", "size:20971520", "required"},
+	}
+	messages := govalidator.MapData{
+		"file:avatar": []string{
+			"ext:头像格式错误,只能为 png,jpg,jpeg 一种",
+			"siz:头像大小不能超过 20 MB",
+			"required:头像不能为空",
+		},
+	}
+	return validateFile(c, data, rules, messages)
+}

+ 41 - 0
database/migrations/2025_07_22_100425_add_fields_to_user.go

@@ -0,0 +1,41 @@
+package migrations
+
+import (
+	"gorm.io/gorm"
+
+	"github.com/runningwater/gohub/pkg/console"
+	"github.com/runningwater/gohub/pkg/migrate"
+)
+
+func init() {
+
+	type User struct {
+		City         string `gorm:"type:varchar(10);"`
+		Introduction string `gorm:"type:varchar(255);"`
+		Avatar       string `gorm:"type:varchar(255);default:null"`
+	}
+
+	up := func(migrator gorm.Migrator, DB *gorm.DB) {
+		_ = DB.Set("gorm:table_options", "ENGINE=InnoDB CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci").AutoMigrate(&User{})
+	}
+
+	down := func(migrator gorm.Migrator, DB *gorm.DB) {
+		err := migrator.DropColumn(&User{}, "city")
+		if err != nil {
+			console.Error(err.Error())
+			return
+		}
+		err = migrator.DropColumn(&User{}, "introduction")
+		if err != nil {
+			console.Error(err.Error())
+			return
+		}
+		err = migrator.DropColumn(&User{}, "avatar")
+		if err != nil {
+			console.Error(err.Error())
+			return
+		}
+	}
+
+	migrate.Add(up, down, "2025_07_22_100425_add_fields_to_user")
+}

+ 22 - 4
gohub.http

@@ -55,7 +55,7 @@ POST {{base_url}}/v1/auth/signup/using-email HTTP/1.1
 Content-Type: application/json
 
 {
-  "name": "summer3",
+  "name": "summer",
   "password": "secret",
   "password_confirm": "secret",
   "verify_code": "123123",
@@ -96,6 +96,18 @@ Authorization: Bearer {{access_token}}
 ### 用户列表
 GET {{base_url}}/v1/users HTTP/1.1
 
+### 更新用户信息
+PUT {{base_url}}/v1/users HTTP/1.1
+Authorization: Bearer {{access_token}}
+Content-Type: application/json
+
+{
+  "name": "simon",
+  "city": "杭州",
+  "introduction": "这里是个人描述"
+}
+
+
 ### 创建分类
 POST {{base_url}}/v1/categories HTTP/1.1
 Authorization: Bearer {{access_token}}
@@ -146,11 +158,17 @@ Content-Type: application/json
   "body": "话题1内容, 这里是帖子描述内容帖子描述内容",
   "category_id": "3"
 }
-
-### 显示话题
-GET {{base_url}}/v1/topics/2 HTTP/1.1
+### 更新邮箱
+POST {{base_url}}/v1/users/email HTTP/1.1
+Authorization: Bearer {{access_token}}
 Content-Type: application/json
 
+{
+  "email": "summer@testing.com",
+  "verify_code": "123123"
+}
+
+
 ### 话题列表
 GET {{base_url}}/v1/topics HTTP/1.1
 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.eyJ1c2VyX2lkIjoiMSIsInVzZXJfbmFtZSI6InN1bW1lciIsImV4cGlyZV90aW1lIjoxNzU3ODE2NDI3LCJpc3MiOiJHb2h1YiIsInN1YiI6IjEiLCJhdWQiOlsic3VtbWVyIl0sImV4cCI6MTc1NzgxNjQyNywibmJmIjoxNzUyNjMyNDI3LCJpYXQiOjE3NTI2MzI0Mjd9.9tYiBN5J4PVblgM1vk09s8sPq2Hqsvm5YimJcV19iaw"
+    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIiLCJ1c2VyX25hbWUiOiJzdW1tZXIiLCJleHBpcmVfdGltZSI6MTc1ODkzNjk2OCwiaXNzIjoiR29odWIiLCJzdWIiOiIxMiIsImF1ZCI6WyJzdW1tZXIiXSwiZXhwIjoxNzU4OTM2OTY4LCJuYmYiOjE3NTM3NTI5NjgsImlhdCI6MTc1Mzc1Mjk2OH0.3n3ki71OUZWH66fC1u2Qno1lugWJz-lJW54etfXpViU"
   }
 }

+ 31 - 0
pkg/file/file.go

@@ -1,9 +1,17 @@
 package file
 
 import (
+	"fmt"
+	"mime/multipart"
 	"os"
 	"path/filepath"
 	"strings"
+
+	"github.com/gin-gonic/gin"
+
+	"github.com/runningwater/gohub/pkg/app"
+	"github.com/runningwater/gohub/pkg/auth"
+	"github.com/runningwater/gohub/pkg/helpers"
 )
 
 // Put 将数据写入文件
@@ -32,3 +40,26 @@ func Exists(path string) bool {
 func NameWithoutExtension(fileName string) string {
 	return strings.TrimSuffix(fileName, filepath.Ext(fileName))
 }
+
+func SaveUploadAvatar(c *gin.Context, file *multipart.FileHeader) (string, error) {
+
+	var avatar string
+	// 确保目录存在,不存在创建
+	publicPath := "public"
+	dirName := fmt.Sprintf("/uploads/avatars/%s/%s/", app.TimenowInTimezone().Format("2006/01/02"), auth.CurrentUID(c))
+	if err := os.MkdirAll(publicPath+dirName, 0755); err != nil {
+		return "", err
+	}
+	// 保存文件
+	fileName := randomNameFromUploadFile(file)
+	avatarPath := publicPath + dirName + fileName
+	if err := c.SaveUploadedFile(file, avatarPath); err != nil {
+		return avatar, err
+	}
+
+	return avatarPath, nil
+}
+
+func randomNameFromUploadFile(file *multipart.FileHeader) string {
+	return helpers.RandomString(16) + filepath.Ext(file.Filename)
+}

+ 1 - 1
pkg/helpers/helpers.go

@@ -47,7 +47,7 @@ func MicrosecondsStr(elapsed time.Duration) string {
 	return fmt.Sprintf("%.3fms", float64(elapsed.Nanoseconds())/1e6)
 }
 
-// 生成随机数字符串
+// RandomNumber 生成随机数字符串
 // length: 字符串长度
 func RandomNumber(length int) string {
 	table := [...]byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0'}

+ 9 - 8
pkg/jwt/jwt.go

@@ -7,6 +7,7 @@ import (
 
 	"github.com/gin-gonic/gin"
 	jwtpkg "github.com/golang-jwt/jwt/v5"
+
 	"github.com/runningwater/gohub/pkg/app"
 	"github.com/runningwater/gohub/pkg/config"
 	"github.com/runningwater/gohub/pkg/logger"
@@ -31,8 +32,8 @@ type JWT struct {
 	MaxRefresh time.Duration
 }
 
-// JWTCustomClaims 自定义的 JWT 载荷结构体,包含用户 ID、用户名 和过期时间
-type JWTCustomClaims struct {
+// CustomClaims 自定义的 JWT 载荷结构体,包含用户 ID、用户名 和过期时间
+type CustomClaims struct {
 	// 用户 ID
 	UserID string `json:"user_id"`
 	// 用户名
@@ -60,7 +61,7 @@ func NewJWT() *JWT {
 }
 
 // ParseToken 解析 Token
-func (j *JWT) ParseToken(c *gin.Context) (*JWTCustomClaims, error) {
+func (j *JWT) ParseToken(c *gin.Context) (*CustomClaims, error) {
 
 	// 1. 获取请求头中的 Token
 	tokenString, err := j.getTokenFromHeader(c)
@@ -90,7 +91,7 @@ func (j *JWT) ParseToken(c *gin.Context) (*JWTCustomClaims, error) {
 		// return nil, ErrTokenInvalid
 	}
 
-	if claims, ok := token.Claims.(*JWTCustomClaims); ok && token.Valid {
+	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
 		return claims, nil
 	}
 	return nil, ErrTokenInvalid
@@ -102,7 +103,7 @@ func (j *JWT) IssueToken(userID, userName string) string {
 
 	// 1. 构造用户 claims 信息(负荷)
 	expireTime := j.expireAtTime()
-	claims := JWTCustomClaims{
+	claims := CustomClaims{
 		userID,
 		userName,
 		expireTime.Unix(),
@@ -147,7 +148,7 @@ func (j *JWT) RefreshToken(c *gin.Context) (string, error) {
 	}
 
 	// 4. 解析 claims
-	claims := token.Claims.(*JWTCustomClaims)
+	claims := token.Claims.(*CustomClaims)
 
 	// 5. 判断是否过了最大允许刷新的时间
 	x := app.TimenowInTimezone().Add(-j.MaxRefresh).Unix()
@@ -162,7 +163,7 @@ func (j *JWT) RefreshToken(c *gin.Context) (string, error) {
 }
 
 // generateToken 生成 Token, 内部使用,外部请调用 IssueToken
-func (j *JWT) generateToken(claims JWTCustomClaims) (string, error) {
+func (j *JWT) generateToken(claims CustomClaims) (string, error) {
 
 	// 创建一个新的 Token 对象,使用 HMAC SHA256 签名方法
 	token := jwtpkg.NewWithClaims(jwtpkg.SigningMethodHS256, claims)
@@ -213,7 +214,7 @@ func (j *JWT) parseTokenString(tokenString string) (*jwtpkg.Token, error) {
 
 	return jwtpkg.ParseWithClaims(
 		tokenString,
-		&JWTCustomClaims{},
+		&CustomClaims{},
 		func(token *jwtpkg.Token) (any, error) {
 			return j.SigningKey, nil
 		},

+ 2 - 0
public/uploads/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 3 - 0
routes/api.go

@@ -57,6 +57,9 @@ func RegisterAPIRoutes(router *gin.Engine) {
 		userGroup := v1.Group("/users")
 		{
 			userGroup.GET("", uc.Index)
+			userGroup.PUT("", middlewares.AuthJWT(), uc.UpdateProfile)
+			userGroup.POST("/email", middlewares.AuthJWT(), uc.UpdateEmail)
+			userGroup.POST("/avatar", middlewares.AuthJWT(), uc.UpdateAvatar)
 		}
 		cgc := new(controllers.CategoriesController)
 		categoryGroup := v1.Group("/categories")