MisakaOJ项目,第三次记录内容
首先,先给用户表加两个字段,用以比较和获取排名。
gotype 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,
},
})
}
首先,先给用户表新增一个字段,判断是不是管理员,对应的Token生成也得改。
gotype 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()
}
}
最后在路由规则里用:
gofunc 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")
....
}
先改表,分类问题表那里有一些问题,而且还少了一张存储题目测试用例的表。
分类问题那三张表先放这里。
gotype 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
}
之后是测试用例表:
gotype 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"
}
gotype 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官方包的时间,惊了。
gofunc 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
}
按照之前的,直接写。
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{})
}
直接写,父级分类名需要先查是否存在。
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
}
按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 许可协议。转载请注明出处!