2024-08-28
Go & 后端
00

目录

1 获取用户排行榜接口
2 认证中间件
3 新增题目接口
* 修改数据库初始化代码
4 获取分类列表接口
5 新增分类端口
6 修改分类接口

MisakaOJ项目,第三次记录内容

1 获取用户排行榜接口

首先,先给用户表加两个字段,用以比较和获取排名。

go
type User struct { gorm.Model Identity string `gorm:"column:identity;type:varchar(36);" json:"identity"` // 用户唯一标识 Name string `gorm:"column:username;type:varchar(100);" json:"name"` // 用户名 Password string `gorm:"column:password;type:varchar(32);" json:"-"` // 密码 加密后 Phone string `gorm:"column:phone;type:varchar(20)" json:"phone"` // 手机号 Mail string `gorm:"column:mail;type:varchar(100);" json:"mail"` // 邮箱地址 FinishQuestionNum int `gorm:"column:finish_question_num;type:int(11);default:0;" json:"finish_question_num"` // 解题数量 SubmitNum int `gorm:"column:submit_num;type:int(11);default:0;" json:"submit_num"` // 提交次数 }

之后写对应接口。注意Model层的那个函数改成了参数column为空字符串时不进行条件查询。

go
// GetRankList // @Tags 公共方法 // @Summary 获取用户排行榜列表 // @Param page query int false "page" // @Param size query int false "size" // @Success 200 {data} json "{"code": "200", "data": ""}" // @Router /rank_list [get] func GetRankList(c *gin.Context) { page, e := strconv.Atoi(c.DefaultQuery("page", constants.DefaultPage)) if e != nil { ErrorHandler(c, "Get Param page Error: "+e.Error()) return } size, e := strconv.Atoi(c.DefaultQuery("size", constants.DefaultSize)) if e != nil { ErrorHandler(c, "Get Param size Error: "+e.Error()) return } page = (page - 1) * size // page到offset的转换 var count int64 data := make([]*models.User, 0) e = models.GetUserDetailByColumn("", "").Count(&count). Order("finish_question_num DESC, submit_num ASC"). Offset(page).Limit(size). Find(&data).Error if e != nil { ErrorHandler(c, errors.Join(constants.DataBaseQueryErr, e).Error()) return } c.JSON(http.StatusOK, gin.H{ "code": 200, "data": map[string]any{ "count": count, "list": data, }, }) }

2 认证中间件

首先,先给用户表新增一个字段,判断是不是管理员,对应的Token生成也得改。

go
type User struct { gorm.Model Identity string `gorm:"column:identity;type:varchar(36);" json:"identity"` // 用户唯一标识 Name string `gorm:"column:username;type:varchar(100);" json:"name"` // 用户名 Password string `gorm:"column:password;type:varchar(32);" json:"-"` // 密码 加密后 Phone string `gorm:"column:phone;type:varchar(20)" json:"phone"` // 手机号 Mail string `gorm:"column:mail;type:varchar(100);" json:"mail"` // 邮箱地址 IsAdmin int `gorm:"column:is_admin;type:tinyint(1);default:0;" json:"is_admin"` // 是否为管理员 如果是则为1 否则为0 默认为0 FinishQuestionNum int `gorm:"column:finish_question_num;type:int(11);default:0;" json:"finish_question_num"` // 解题数量 SubmitNum int `gorm:"column:submit_num;type:int(11);default:0;" json:"submit_num"` // 提交次数 }
go
// GenerateToken 根据给定的用户Identity 生成一个Token 默认情况下过期时间为30分钟 func GenerateToken(identity string, isAdmin int) (string, error) { userClaim := &UserClaim{ Identity: identity, IsAdmin: isAdmin, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(constants.JwtExpiredTime), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, userClaim) tokenString, e := token.SignedString(constants.JwtKey) if e != nil { return "", errors.Join(constants.TokenGenerateErr, e) } return tokenString, nil } // ParseToken 根据给定的token进行解析 返回用户Identity和isAdmin 并且判断是否过期 func ParseToken(token string) (string, int, error) { userClaim := new(UserClaim) claim, e := jwt.ParseWithClaims(token, userClaim, func(token *jwt.Token) (interface{}, error) { return constants.JwtKey, nil }) if e != nil { return "", 0, errors.Join(constants.TokenParseErr, e) } if claim.Valid { // 是否可用 return userClaim.Identity, userClaim.IsAdmin, nil } // 已过期 return "", 0, constants.TokenIsExpired }

之后,写管理员和普通用户用的中间件:

go
// AuthAdminMiddleWare 验证是否为管理员的中间件 func AuthAdminMiddleWare() gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("Authorization") // 不存在即返回空字符串 该函数等效 c.Request.Header.Get(key) if token == "" { c.Abort() // 中止Handler链 但是不会中止本次Handler handler.ErrorHandler(c, constants.AuthorizationAdminFailed.Error()) return } _, isAdmin, e := util.ParseToken(token) if e != nil { c.Abort() handler.ErrorHandler(c, e.Error()) return } if isAdmin == 0 { c.Abort() handler.ErrorHandler(c, constants.AuthorizationAdminFailed.Error()) return } // 认证通过 c.Next() // c.Next()之后的代码会在里面的Handler链全部执行完之后再执行 // 详情见:https://gin-gonic.com/zh-cn/docs/examples/custom-middleware/ } } // AuthUserMiddleWare 验证是否为普通用户的中间件 func AuthUserMiddleWare() gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("Authorization") if token == "" { c.Abort() handler.ErrorHandler(c, constants.AuthorizationUserFailed.Error()) return } _, isAdmin, e := util.ParseToken(token) if e != nil { c.Abort() handler.ErrorHandler(c, e.Error()) return } if isAdmin == 0 { c.Abort() handler.ErrorHandler(c, constants.AuthorizationUserFailed.Error()) return } c.Next() } }

最后在路由规则里用:

go
func Router() *gin.Engine { r := gin.Default() ... // 普通用户路由组 userAuthorization := r.Group("/user", middleware.AuthUserMiddleWare()) userAuthorization.GET("/detail", handler.GetUserDetail) // 管理员路由组 adminAuthorization := r.Group("/admin", middleware.AuthAdminMiddleWare()) adminAuthorization.POST("/add_question") .... }

3 新增题目接口

先改表,分类问题表那里有一些问题,而且还少了一张存储题目测试用例的表。

分类问题那三张表先放这里。

go
type QuestionCategory struct { gorm.Model QuestionId int `gorm:"column:question_id;" json:"question_id"` // 问题ID CategoryId int `gorm:"column:category_id;" json:"category_id"` // 分类ID Category *Category `gorm:"foreignKey:id;references:category_id"` // 设置外键 关联到分类表 } type Category struct { gorm.Model Name string `gorm:"column:name;type:varchar(100);" json:"name"` // 分类名称 ParentId int `gorm:"column:parent_id;type:int(11);default:0;" json:"parent_id"` // 父级ID 自身为顶级是默认为0 } type Question struct { gorm.Model Identity string `gorm:"column:identity;type:varchar(36);" json:"identity"` // 题目唯一标识 QuestionCategories []*QuestionCategory `gorm:"foreignKey:question_id;references:id"` // 关联问题分类表 Title string `gorm:"column:title;type:varchar(255);" json:"title"` // 题目标题 Content string `gorm:"column:content;type:text;" json:"content"` // 题目正文 MaxRuntime int `gorm:"column:max_runtime;type:int(11)" json:"max_runtime"` // 最大运行时长 MaxMem int `gorm:"column:max_memory;type:int(11)" json:"max_mem"` // 最大允许内存 } func (table *Question) TableName() string { return "question" } func GetQuestionList(keyword, categoryIdentity string) *gorm.DB { tx := DB.Model(new(Question)).Preload("QuestionCategories").Preload("QuestionCategories.Category"). Where("title like ? OR content like ?", "%"+keyword+"%", "%"+keyword+"%") if categoryIdentity != "" { tx.Joins("RIGHT JOIN question_category qc on qc.question_id = question.id"). Where("qc.category_id = (SELECT c.id FROM category c WHERE c.id = ?)", categoryIdentity) } return tx }

之后是测试用例表:

go
type TestCase struct { gorm.Model Input string `gorm:"column:input;type:varchar(1000);" json:"input"` // 输入的测试用例 Output string `gorm:"column:input;type:varchar(1000);" json:"output"` // 输出的测试用例 QuestionIdentity string `gorm:"column:question_identity;type:varchar(36);" json:"question_identity"` // 题目唯一标识 } func (table *TestCase) TableName() string { return "test_case" }
go
type Question struct { gorm.Model Identity string `gorm:"column:identity;type:varchar(36);" json:"identity"` // 题目唯一标识 QuestionCategories []*QuestionCategory `gorm:"foreignKey:question_id;references:id"` // 关联问题分类表 Title string `gorm:"column:title;type:varchar(255);" json:"title"` // 题目标题 Content string `gorm:"column:content;type:text;" json:"content"` // 题目正文 MaxRuntime int `gorm:"column:max_runtime;type:int(11)" json:"max_runtime"` // 最大运行时长 MaxMem int `gorm:"column:max_memory;type:int(11)" json:"max_mem"` // 最大允许内存 TestCases []*TestCase `gorm:"foreignKey:question_identity;references:identity"` // 测试用例们 }

最后写对应的接口:

go
// AddQuestion // @Tags 管理员接口 // @Summary 提交新的题目 // @Param authorization header string true "authorization" // @Param title formData string true "title" // @Param content formData string true "content" // @Param max_runtime formData int true "max_runtime" // @Param max_mem formData int true "max_mem" // @Param categories formData array true "categories" // @Param test_cases formData array true "test_cases" // @Success 200 {data} json "{"code": "200", "data": ""}" // @Router /admin/add_question [post] func AddQuestion(c *gin.Context) { // 参数解析 title := c.PostForm("title") if title == "" { ErrorHandler(c, constants.ParameterMissingErr.Error()+"title") return } content := c.PostForm("content") if content == "" { ErrorHandler(c, constants.ParameterMissingErr.Error()+"content") return } maxRuntime, e := strconv.Atoi(c.PostForm("max_runtime")) if e != nil { ErrorHandler(c, constants.ParameterParseErr.Error()+"max_runtime: "+e.Error()) return } if maxRuntime < 0 { ErrorHandler(c, constants.ParameterParseErr.Error()+"max_runtime: less than 0") return } maxMem, e := strconv.Atoi(c.PostForm("max_mem")) if e != nil { ErrorHandler(c, constants.ParameterParseErr.Error()+"max_mem: "+e.Error()) return } if maxMem < 0 { ErrorHandler(c, constants.ParameterParseErr.Error()+"max_mem: less than 0") return } categoryArrayFromUser := c.PostFormArray("categories") if len(categoryArrayFromUser) == 0 { ErrorHandler(c, constants.ParameterMissingErr.Error()+"categories") return } testCaseArrayFromUser := c.PostFormArray("test_cases") if len(testCaseArrayFromUser) == 0 { ErrorHandler(c, constants.ParameterMissingErr.Error()+"test_cases") return } // 生成UUID questionIdentity := util.GenerateUUID() // 先查分类 把分类id拿到 并且写成QuestionCategory的 questionCategoryArray := make([]*models.QuestionCategory, 0) var questionCategory *models.QuestionCategory for i := range categoryArrayFromUser { // todo 分类id和分类的关系也可以放进redis里 加快访问 // 初始化 questionCategory = &models.QuestionCategory{} questionCategory.Category = &models.Category{} // 查询 e = models.GetCateGoryIdByName(categoryArrayFromUser[i]).First(questionCategory.Category).Error if e != nil { if errors.Is(e, gorm.ErrRecordNotFound) { continue } ErrorHandler(c, errors.Join(constants.DataBaseQueryErr, e).Error()) return } // 放结果 questionCategory.CategoryId = questionCategory.Category.ID questionCategoryArray = append(questionCategoryArray, questionCategory) } if len(questionCategoryArray) == 0 { // 说明前端传过来的 categories 无效 ErrorHandler(c, constants.ParameterParseErr.Error()+"categories") return } // 把前端传过来的testCase转换为结构体 testCaseArray := make([]*models.TestCase, 0) var caseMap map[string]string var singleCase *models.TestCase var ok bool for i := range testCaseArrayFromUser { e = json.Unmarshal([]byte(testCaseArrayFromUser[i]), &caseMap) if e != nil { continue } singleCase = &models.TestCase{ QuestionIdentity: questionIdentity, } singleCase.Input, ok = caseMap["input"] if !ok { continue } singleCase.Output, ok = caseMap["output"] if !ok { continue } testCaseArray = append(testCaseArray, singleCase) } if len(testCaseArray) == 0 { // 说明前端传过来的testCase无效 ErrorHandler(c, constants.ParameterParseErr.Error()+"test_cases") return } // 准备插入 newQuestion := &models.Question{ Identity: questionIdentity, QuestionCategories: questionCategoryArray, Title: title, Content: content, MaxRuntime: maxRuntime, MaxMem: maxMem, TestCases: testCaseArray, } e = models.NewQuestion(newQuestion) if e != nil { ErrorHandler(c, errors.Join(constants.DataBaseInsertErr, e).Error()) return } c.JSON(http.StatusOK, gin.H{ "code": 200, "message": "Add New Question Successfully! ", }) } // 还有两个Model层的 func GetCateGoryIdByName(name string) *gorm.DB { return DB.Model(&Category{}).Where("name = ?", name) } func NewQuestion(question *Question) error { return DB.Create(question).Error }

* 修改数据库初始化代码

在调试的时候,发现总会报错:connection.go:49: read tcp 127.0.0.1:7660->127.0.0.1:19327: wsarecv: An established connection was aborted by the software in your host machine.,于是配置连接池和连接最大过期时间。go的数据库连接池居然是sql官方包的时间,惊了。

go
func InitMysql() *gorm.DB { db, e := gorm.Open(mysql.Open("root:123456@tcp(127.0.0.1:19327)/misaka_oj?charset=utf8mb4&parseTime=True&loc=Local"), &gorm.Config{ DisableForeignKeyConstraintWhenMigrating: true, // 禁止自动创建关联对应的外键 Logger: logger.Default.LogMode(logger.Info), // 全局Debug模式 }) if e != nil { log.Panic("Mysql Init Failed: ", e) return nil } var sqlDB *sql.DB sqlDB, e = db.DB() if e != nil { log.Panic("Mysql Init Failed: ", e) return nil } sqlDB.SetConnMaxLifetime(59 * time.Second) // 设置连接过期时间 sqlDB.SetMaxOpenConns(20) // 设置最大连接数 sqlDB.SetMaxIdleConns(10) // 设置最大空闲连接数 return db }

4 获取分类列表接口

按照之前的,直接写。

go
// GetCategoryList // @Tags 公共方法 // @Summary 获取分类列表 // @Param page query int false "page" // @Param size query int false "size" // @Success 200 {data} json "{"code": "200", "data": ""}" // @Router /category_list [get] func GetCategoryList(c *gin.Context) { page, e := strconv.Atoi(c.DefaultQuery("page", constants.DefaultPage)) if e != nil { ErrorHandler(c, "Get Param page Error: "+e.Error()) return } size, e := strconv.Atoi(c.DefaultQuery("size", constants.DefaultSize)) if e != nil { ErrorHandler(c, "Get Param size Error: "+e.Error()) return } page = (page - 1) * size // page到offset的转换 tx := models.GetCategory() data := make([]*models.Category, 0) var count int64 e = tx.Offset(page).Limit(size).Count(&count).Find(&data).Error if e != nil { ErrorHandler(c, errors.Join(constants.DataBaseQueryErr, e).Error()) return } c.JSON(http.StatusOK, gin.H{ "code": 200, "data": map[string]any{ "count": count, "list": data, }, }) } // 对应的Model层 func GetCategory() *gorm.DB { return DB.Model(&Category{}) }

5 新增分类端口

直接写,父级分类名需要先查是否存在。

go
// AddCategory // @Tags 管理员接口 // @Summary 提交新的分类 // @Param authorization header string true "authorization" // @Param name formData string true "name" // @Param parent_name formData string false "parent_name" // @Success 200 {data} json "{"code": "200", "data": ""}" // @Router /admin/add_category [post] func AddCategory(c *gin.Context) { name := c.PostForm("name") if name == "" { ErrorHandler(c, constants.ParameterMissingErr.Error()+"name") return } var parentId uint parentCategory := &models.Category{} var e error parentName := c.PostForm("parent_name") if parentName == "" { // 如果传进来的 parent_name 是空字符串或者直接没有这个字段 就认为该分类没有父级分类 parentId = 0 } else { // 反之 查是否存在这个父级分类 存在的话就存这个父级分类的 id 不存在的话报错 e = models.GetCateGoryByName(parentName).First(parentCategory).Error if e != nil { if errors.Is(e, gorm.ErrRecordNotFound) { // 不存在父级分类 ErrorHandler(c, constants.CategoryNotExistErr.Error()+"name "+parentName) return } ErrorHandler(c, errors.Join(constants.DataBaseQueryErr, e).Error()) return } // 存在父级分类 parentId = parentCategory.ID } newCategory := &models.Category{ Name: name, ParentId: int(parentId), } e = models.NewCategory(newCategory) if e != nil { ErrorHandler(c, errors.Join(constants.DataBaseInsertErr, e).Error()) return } c.JSON(http.StatusOK, gin.H{ "code": 200, "message": "Add New Category Successfully! ", }) } // 对应的Model层 func NewCategory(category *Category) error { return DB.Create(category).Error }

6 修改分类接口

按id查分类,直接写。

go
// ModifyCategory // @Tags 管理员接口 // @Summary 修改分类信息 // @Param authorization header string true "authorization" // @Param category_id formData int true "category_id" // @Param name formData string false "name" // @Param parent_name formData string false "parent_name" // @Success 200 {data} json "{"code": "200", "data": ""}" // @Router /admin/modify_category [post] func ModifyCategory(c *gin.Context) { categoryId, e := strconv.Atoi(c.PostForm("category_id")) if e != nil { ErrorHandler(c, constants.ParameterParseErr.Error()+"category_id: "+e.Error()) return } if categoryId < 0 { ErrorHandler(c, constants.ParameterParseErr.Error()+"category_id: less than 0") return } // 这俩如果全部为空 直接打回 name := c.PostForm("name") parentName := c.PostForm("parent_name") if name == "" && parentName == "" { ErrorHandler(c, constants.ParameterMissingErr.Error()+"name, parent_name") return } // 先把要改的Category查出来 modifyCategory := &models.Category{} tx := models.GetCateGoryById(categoryId) e = tx.First(modifyCategory).Error if e != nil { if errors.Is(e, gorm.ErrRecordNotFound) { ErrorHandler(c, constants.CategoryNotExistErr.Error()+"id "+string(rune(categoryId))) return } ErrorHandler(c, errors.Join(constants.DataBaseQueryErr, e).Error()) return } // 如果parentName不为空 查对应的parentId if parentName != "" { parentCategory := &models.Category{} e = models.GetCateGoryByName(parentName).First(parentCategory).Error if e != nil { if errors.Is(e, gorm.ErrRecordNotFound) { // 不存在父级分类 ErrorHandler(c, constants.ParameterParseErr.Error()+"parent_name") return } ErrorHandler(c, errors.Join(constants.DataBaseQueryErr, e).Error()) return } // 存在父级分类 modifyCategory.ParentId = int(parentCategory.ID) } // 如果 name 不为空 更新 name if name != "" { modifyCategory.Name = name } e = models.ModifyCategory(modifyCategory) if e != nil { ErrorHandler(c, errors.Join(constants.DataBaseUpdateErr, e).Error()) return } c.JSON(http.StatusOK, gin.H{ "code": 200, "message": "Modify Category Successfully! ", }) } // 对应的Model层 func GetCateGoryById(id int) *gorm.DB { return DB.Model(&Category{}).Where("id = ?", id) } func ModifyCategory(category *Category) error { return DB.Save(category).Error }

本文作者:御坂19327号

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!