diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..1f020b7 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/online_code.iml b/.idea/online_code.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/online_code.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/common/constant/rds_key.go b/common/constant/rds_key.go new file mode 100644 index 0000000..6d1864d --- /dev/null +++ b/common/constant/rds_key.go @@ -0,0 +1,6 @@ +package constant + +const ( + OAuth2RedisKey = "oauth:token:" // Token缓存前缀 + OAuth2UserCacheKey = "oauth:user:" // 用户缓存前缀 +) diff --git a/common/constant/user.go b/common/constant/user.go new file mode 100644 index 0000000..47b22e1 --- /dev/null +++ b/common/constant/user.go @@ -0,0 +1,38 @@ +package constant + +// UserStatus 用户状态 +type UserStatus string + +const ( + UserStatusActive UserStatus = "NORMAL" // 用户状态正常 + UserStatusDisabled UserStatus = "DISABLE" // 已禁用用户 +) + +// 状态对应的描述 +var userStatusMap = map[UserStatus]string{ + UserStatusActive: "正常", + UserStatusDisabled: "已禁用", +} + +// 处理为看得懂的状态 +func (s UserStatus) String() string { + if str, ok := userStatusMap[s]; ok { + return str + } + return string(s) +} + +// ===================================================================================================================== + +// UserIdentity 用户身份 +type UserIdentity string + +const ( + UserIdentityAdmin UserIdentity = "admin" // 管理员 + UserIdentityUser UserIdentity = "user" // 普通用户 +) + +// String implements the Stringer interface. +func (t UserIdentity) String() string { + return string(t) +} diff --git a/common/default_keys.go b/common/default_keys.go new file mode 100644 index 0000000..31f9cfe --- /dev/null +++ b/common/default_keys.go @@ -0,0 +1,6 @@ +package common + +const ( + SmsSendKey = "sms:send:" // 短信发送缓存前缀 + Oauth2RedisKey = "oauth:token:" // Token缓存前缀 +) diff --git a/common/types/date.go b/common/types/date.go new file mode 100644 index 0000000..bb842f0 --- /dev/null +++ b/common/types/date.go @@ -0,0 +1,105 @@ +package types + +import ( + "database/sql/driver" + "fmt" + "time" +) + +// 默认时间格式 +const dateFormat = "2006-01-02 15:04:05.000" + +// DateTime 自定义时间类型 +type DateTime time.Time + +// Scan implements the Scanner interface. +func (dt *DateTime) Scan(value interface{}) error { + // mysql 内部日期的格式可能是 2006-01-02 15:04:05 +0800 CST 格式,所以检出的时候还需要进行一次格式化 + tTime, _ := time.Parse("2006-01-02 15:04:05 +0800 CST", value.(time.Time).String()) + *dt = DateTime(tTime) + return nil +} + +// Value implements the driver Valuer interface. +func (dt DateTime) Value() (driver.Value, error) { + // 0001-01-01 00:00:00 属于空值,遇到空值解析成 null 即可 + if dt.String() == "0001-01-01 00:00:00.000" { + return nil, nil + } + return []byte(dt.Format(dateFormat)), nil +} + +// 用于 fmt.Println 和后续验证场景 +func (dt DateTime) String() string { + return dt.Format(dateFormat) +} + +// Format 格式化 +func (dt DateTime) Format(fm string) string { + return time.Time(dt).Format(fm) +} + +// After 时间比较 +func (dt *DateTime) After(now time.Time) bool { + return time.Time(*dt).After(now) +} + +// Before 时间比较 +func (dt *DateTime) Before(now time.Time) bool { + return time.Time(*dt).Before(now) +} + +// IBefore 时间比较 +func (dt *DateTime) IBefore(now DateTime) bool { + return dt.Before(time.Time(now)) +} + +// SubTime 对比 +func (dt DateTime) SubTime(t time.Time) time.Duration { + return dt.ToTime().Sub(t) +} + +// Sub 对比 +func (dt DateTime) Sub(t DateTime) time.Duration { + return dt.ToTime().Sub(t.ToTime()) +} + +// ToTime 转换为golang的时间类型 +func (dt DateTime) ToTime() time.Time { + return time.Time(dt) +} + +// IsNil 是否为空值 +func (dt DateTime) IsNil() bool { + return dt.Format(dateFormat) == "0001-01-01 00:00:00.000" +} + +// Unix 实现Unix函数 +func (dt DateTime) Unix() int64 { + return dt.ToTime().Unix() +} + +// ======== 序列化 ======== + +// MarshalJSON 时间到字符串 +func (dt DateTime) MarshalJSON() ([]byte, error) { + // 过滤掉空数据 + if dt.IsNil() { + return []byte("\"\""), nil + } + output := fmt.Sprintf(`"%s"`, dt.Format("2006-01-02 15:04:05")) + return []byte(output), nil +} + +// UnmarshalJSON 字符串到时间 +func (dt *DateTime) UnmarshalJSON(b []byte) error { + if len(b) == 2 { + *dt = DateTime{} + return nil + } + // 解析指定的格式 + //now, err := time.ParseInLocation(`"`+dateFormat+`"`, string(b), time.Local) + now, err := time.ParseInLocation(dateFormat, string(b), time.Local) + *dt = DateTime(now) + return err +} diff --git a/common/types/model.go b/common/types/model.go new file mode 100644 index 0000000..8982a85 --- /dev/null +++ b/common/types/model.go @@ -0,0 +1,20 @@ +package types + +import ( + "github.com/google/uuid" + "gorm.io/gorm" +) + +// BaseDbModel 数据库通用字段 +type BaseDbModel struct { + Id string `json:"id" gorm:"type:varchar(50);primarykey"` + CreatedAt DateTime `json:"createdAt"` + UpdatedAt DateTime `json:"updatedAt"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index:deleted"` +} + +// BeforeCreate 创建数据库对象之前生成UUID +func (m *BaseDbModel) BeforeCreate(*gorm.DB) (err error) { + m.Id = uuid.New().String() + return +} diff --git a/core/error_handle.go b/core/error_handle.go new file mode 100644 index 0000000..470224e --- /dev/null +++ b/core/error_handle.go @@ -0,0 +1,52 @@ +package core + +import ( + "fmt" + "git.echol.cn/loser/logger/log" + "github.com/gin-gonic/gin" + "net" + "net/http" + "os" + "strings" +) + +// NoMethodHandler 请求方式不对 +func NoMethodHandler() gin.HandlerFunc { + return func(ctx *gin.Context) { + R(ctx).FailWithMessageAndCode(fmt.Sprintf("不支持%v请求", ctx.Request.Method), http.StatusMethodNotAllowed) + ctx.Abort() + } +} + +// NoRouteHandler 404异常处理 +func NoRouteHandler() gin.HandlerFunc { + return func(ctx *gin.Context) { + R(ctx).FailWithMessageAndCode("请求接口不存在", http.StatusNotFound) + ctx.Abort() + } +} + +// Recovery Panic捕获 +func Recovery() gin.HandlerFunc { + return func(ctx *gin.Context) { + defer func() { + if err := recover(); err != nil { + log.Errorf("系统错误: %v", err) + var brokenPipe bool + if ne, ok := err.(*net.OpError); ok { + if se, ok := ne.Err.(*os.SyscallError); ok { + if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { + brokenPipe = true + } + } + } + if brokenPipe { + log.Errorf("%s", err) + } + R(ctx).FailWithMessage("服务器异常,请联系管理员") + ctx.Abort() + } + }() + ctx.Next() + } +} diff --git a/core/response.go b/core/response.go new file mode 100644 index 0000000..086016e --- /dev/null +++ b/core/response.go @@ -0,0 +1,75 @@ +package core + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +// 返回数据包装 +type response struct { + Code int `json:"code"` + Data any `json:"data"` + Msg string `json:"message"` +} + +type rs struct { + ctx *gin.Context +} + +// 定义状态码 +const ( + ERROR = http.StatusInternalServerError + SUCCESS = http.StatusOK +) + +func R(ctx *gin.Context) *rs { + return &rs{ctx} +} + +// Result 手动组装返回结果 +func (r rs) Result(code int, data interface{}, msg string) { + //if data == nil { + // data = map[string]interface{}{} + //} + + r.ctx.JSON(code, response{ + code, + data, + msg, + }) +} + +// Ok 返回无数据的成功 +func (r rs) Ok() { + r.Result(SUCCESS, nil, "操作成功") +} + +// OkWithMessage 返回自定义成功的消息 +func (r rs) OkWithMessage(message string) { + r.Result(SUCCESS, nil, message) +} + +// OkWithData 自定义内容的成功返回 +func (r rs) OkWithData(data interface{}) { + r.Result(SUCCESS, data, "操作成功") +} + +// OkDetailed 自定义消息和内容的成功返回 +func (r rs) OkDetailed(data interface{}, message string) { + r.Result(SUCCESS, data, message) +} + +// Fail 返回默认失败 +func (r rs) Fail() { + r.Result(ERROR, nil, "操作失败") +} + +// FailWithMessage 返回默认状态码自定义消息的失败 +func (r rs) FailWithMessage(message string) { + r.Result(ERROR, nil, message) +} + +// FailWithMessageAndCode 返回自定义消息和状态码的失败 +func (r rs) FailWithMessageAndCode(message string, code int) { + r.Result(code, nil, message) +} diff --git a/core/response_page.go b/core/response_page.go new file mode 100644 index 0000000..eb588f2 --- /dev/null +++ b/core/response_page.go @@ -0,0 +1,10 @@ +package core + +// PageData 分页数据通用结构体 +type PageData struct { + Current int `json:"current"` // 当前页码 + Size int `json:"size"` // 每页数量 + Total int64 `json:"total"` // 总数 + TotalPage int `json:"total_page"` // 总页数 + Records interface{} `json:"records"` // 返回数据 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..029e83e --- /dev/null +++ b/go.mod @@ -0,0 +1,59 @@ +module online_code + +go 1.18 + +require ( + git.echol.cn/loser/logger v1.0.14 + github.com/gin-gonic/gin v1.7.7 + github.com/google/uuid v1.1.2 + gorm.io/gorm v1.23.5 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/caarlos0/env/v6 v6.9.2 // indirect + github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-kit/kit v0.12.0 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/go-playground/locales v0.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect + github.com/go-playground/validator/v10 v10.4.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.4 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/leodido/go-urn v1.2.0 // indirect + github.com/lixh00/loki-client-go v1.0.1 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/natefinch/lumberjack v2.0.0+incompatible // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.12.2 // indirect + github.com/prometheus/client_model v0.2.0 // indirect + github.com/prometheus/common v0.34.0 // indirect + github.com/prometheus/procfs v0.7.3 // indirect + github.com/prometheus/prometheus v1.8.2-0.20201028100903-3245b3267b24 // indirect + github.com/ugorji/go/codec v1.1.7 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.8.0 // indirect + go.uber.org/zap v1.21.0 // indirect + golang.org/x/crypto v0.0.0-20210915214749-c084706c2272 // indirect + golang.org/x/net v0.0.0-20220517181318-183a9ca12b87 // indirect + golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect + golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e // indirect + golang.org/x/text v0.3.7 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3 // indirect + google.golang.org/grpc v1.46.2 // indirect + google.golang.org/protobuf v1.28.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/models/entity/category.go b/models/entity/category.go new file mode 100644 index 0000000..d86a17d --- /dev/null +++ b/models/entity/category.go @@ -0,0 +1,15 @@ +package entity + +import "online_code/common/types" + +// Category 分类表 +type Category struct { + types.BaseDbModel + Identity string `json:"identity" gorm:"column:identity;type:varchar(36);comment:'分类的唯一标识'"` + Name string `json:"name" gorm:"column:name;type:varchar(100);comment:'分类名称'" ` + ParentId int `json:"parent_id" gorm:"column:parent_id;type:int(11);comment:'父级ID'" ` +} + +func (table *Category) TableName() string { + return "category" +} diff --git a/models/entity/problem.go b/models/entity/problem.go new file mode 100644 index 0000000..40183e0 --- /dev/null +++ b/models/entity/problem.go @@ -0,0 +1,21 @@ +package entity + +import "online_code/common/types" + +// Problem 问题表 +type Problem struct { + types.BaseDbModel + Identity string `json:"identity" gorm:"column:identity;type:varchar(36);comment:'问题表的唯一标识'" ` // 问题表的唯一标识 + ProblemCategories []*ProblemCategory `json:"problem_categories" gorm:"foreignKey:problem_id;references:id;comment:'关联问题分类表'" ` // 关联问题分类表 + Title string `json:"title" gorm:"column:title;type:varchar(255);comment:'文章标题'" ` // 文章标题 + Content string `json:"content" gorm:"column:content;type:text;comment:'文章正文'" ` // 文章正文 + MaxRuntime int `json:"max_runtime" gorm:"column:max_runtime;type:int(11);comment:'最大运行时长'" ` // 最大运行时长 + MaxMem int `json:"max_mem" gorm:"column:max_mem;type:int(11);comment:'最大运行内存'" ` // 最大运行内存 + TestCases []*TestCase `json:"test_cases" gorm:"foreignKey:problem_identity;references:identity;comment:'关联测试用例表'" ` // 关联测试用例表 + PassNum int64 `json:"pass_num" gorm:"column:pass_num;type:int(11);comment:'通过次数'" ` // 通过次数 + SubmitNum int64 `json:"submit_num" gorm:"column:submit_num;type:int(11);comment:'提交次数'" ` // 提交次数 +} + +func (table *Problem) TableName() string { + return "problem_basic" +} diff --git a/models/entity/problem_category.go b/models/entity/problem_category.go new file mode 100644 index 0000000..d2a5fef --- /dev/null +++ b/models/entity/problem_category.go @@ -0,0 +1,17 @@ +package entity + +import ( + "online_code/common/types" +) + +// ProblemCategory 问题分类表 +type ProblemCategory struct { + types.BaseDbModel + ProblemId uint `json:"problem_id" gorm:"column:problem_id;type:int(11);comment:'问题的ID'" ` // 问题的ID + CategoryId uint `json:"category_id" gorm:"column:category_id;type:int(11);comment:'分类的ID'" ` // 分类的ID + CategoryBasic *Category `json:"category_basic" gorm:"foreignKey:id;references:category_id;comment:'关联分类的基础信息表'" ` // 关联分类的基础信息表 +} + +func (table *ProblemCategory) TableName() string { + return "problem_category" +} diff --git a/models/entity/submit.go b/models/entity/submit.go new file mode 100644 index 0000000..d58ea43 --- /dev/null +++ b/models/entity/submit.go @@ -0,0 +1,16 @@ +package entity + +// Submit 提交表 +type Submit struct { + Identity string `json:"identity" gorm:"column:identity;type:varchar(36);comment:'用户唯一标识'" ` // 唯一标识 + ProblemIdentity string `json:"problem_identity" gorm:"column:problem_identity;type:varchar(36);comment:'问题表的唯一标识'" ` // 问题表的唯一标识 + ProblemBasic *Problem `json:"problem_basic" gorm:"foreignKey:identity;references:problem_identity;comment:'关联问题基础表'" ` // 关联问题基础表 + UserIdentity string `json:"user_identity" gorm:"column:user_identity;type:varchar(36);comment:'用户表的唯一标识'" ` // 用户表的唯一标识 + UserBasic *User `json:"user_basic" gorm:"foreignKey:identity;references:user_identity;comment:'联用户基础表'" ` // 关联用户基础表 + Path string `json:"path" gorm:"column:path;type:varchar(255);comment:'代码存放路径'" ` // 代码存放路径 + Status int `json:"status" gorm:"column:status;type:tinyint(1);comment:'状态'" ` // 【-1-待判断,1-答案正确,2-答案错误,3-运行超时,4-运行超内存, 5-编译错误】 +} + +func (table *Submit) TableName() string { + return "submit" +} diff --git a/models/entity/test_case.go b/models/entity/test_case.go new file mode 100644 index 0000000..0076d8f --- /dev/null +++ b/models/entity/test_case.go @@ -0,0 +1,16 @@ +package entity + +import "online_code/common/types" + +// TestCase 测试用例 +type TestCase struct { + types.BaseDbModel + Identity string `json:"identity" gorm:"column:identity;type:varchar(255);comment:'用户唯一标识''" ` + ProblemIdentity string `json:"problem_identity" gorm:"column:problem_identity;type:varchar(255);comment:'问题'" ` + Input string `json:"input" gorm:"column:input;type:text;" ` + Output string `json:"output" gorm:"column:output;type:text;" ` +} + +func (table *TestCase) TableName() string { + return "test_case" +} diff --git a/models/entity/user.go b/models/entity/user.go new file mode 100644 index 0000000..516c04f --- /dev/null +++ b/models/entity/user.go @@ -0,0 +1,19 @@ +package entity + +import "online_code/common/types" + +type User struct { + types.BaseDbModel + Identity string `json:"identity" gorm:"column:identity;type:varchar(36);comment:'用户唯一标识'"` + Name string `json:"name" gorm:"column:name;type:varchar(100);not null;comment:'用户名'"` + Password string `json:"password" gorm:"column:password;type:varchar(32);not null;comment:'用户密码'"` + Phone string `json:"phone" gorm:"column:phone;type:varchar(20);not null;comment:'用户手机号'"` + Mail string `json:"mail" gorm:"column:mail;type:varchar(100);not null;comment:'用户邮箱'"` + PassNum int64 `json:"pass_num" gorm:"column:pass_num;type:int(11);comment:'通过的次数'"` + SubmitNum int64 `json:"submit_num" gorm:"column:submit_num;type:int(11);comment:'提交次数'"` + IsAdmin int `json:"is_admin" gorm:"column:is_admin;type:tinyint(1);comment:'是否是管理员【0-否,1-是】'"` +} + +func (table *User) TableName() string { + return "user" +}