From 9e40e7a290d8410bdd9cf1fbc2a8e28c69fcebc1 Mon Sep 17 00:00:00 2001 From: han-joker Date: Tue, 28 Mar 2023 13:23:06 +0800 Subject: [PATCH] basic code --- .gitignore | 2 + api/home/category.go | 95 +++++++ api/home/content.go | 107 ++++++++ api/home/models/category.go | 19 ++ api/home/models/content.go | 27 ++ api/home/models/pagination.go | 7 + api/home/models/sorter.go | 6 + api/moo/cachem/cache.go | 47 ++++ api/moo/cachem/cache_test.go | 30 +++ api/moo/confm/config.go | 435 +++++++++++++++++++++++++++++++++ api/moo/confm/config_test.go | 20 ++ api/moo/dbm/db.go | 68 ++++++ api/moo/logm/log.go | 233 ++++++++++++++++++ api/moo/logm/log_test.go | 54 ++++ api/moo/server.go | 32 +++ api/moo/serverHttp.go | 11 + api/moo/serverWebsocket.go | 41 ++++ api/moo/toolm/default.go | 25 ++ api/moo/toolm/slice.go | 10 + api/moo/toolm/string.go | 49 ++++ api/panel/category.go | 265 ++++++++++++++++++++ api/panel/content.go | 260 ++++++++++++++++++++ api/panel/middlewares/jwt.go | 94 +++++++ api/panel/models/category.go | 27 ++ api/panel/models/content.go | 33 +++ api/panel/models/pagination.go | 7 + api/panel/models/sorter.go | 6 + api/panel/models/user.go | 18 ++ api/panel/upload.go | 173 +++++++++++++ api/panel/user.go | 175 +++++++++++++ api/tables/category.go | 18 ++ api/tables/content.go | 34 +++ api/tables/model.go | 13 + api/tables/role.go | 14 ++ api/tables/user.go | 15 ++ go.mod | 14 ++ go.sum | 94 +++++++ main.go | 10 + migrate.go | 102 ++++++++ 39 files changed, 2690 insertions(+) create mode 100644 .gitignore create mode 100644 api/home/category.go create mode 100644 api/home/content.go create mode 100644 api/home/models/category.go create mode 100644 api/home/models/content.go create mode 100644 api/home/models/pagination.go create mode 100644 api/home/models/sorter.go create mode 100644 api/moo/cachem/cache.go create mode 100644 api/moo/cachem/cache_test.go create mode 100644 api/moo/confm/config.go create mode 100644 api/moo/confm/config_test.go create mode 100644 api/moo/dbm/db.go create mode 100644 api/moo/logm/log.go create mode 100644 api/moo/logm/log_test.go create mode 100644 api/moo/server.go create mode 100644 api/moo/serverHttp.go create mode 100644 api/moo/serverWebsocket.go create mode 100644 api/moo/toolm/default.go create mode 100644 api/moo/toolm/slice.go create mode 100644 api/moo/toolm/string.go create mode 100644 api/panel/category.go create mode 100644 api/panel/content.go create mode 100644 api/panel/middlewares/jwt.go create mode 100644 api/panel/models/category.go create mode 100644 api/panel/models/content.go create mode 100644 api/panel/models/pagination.go create mode 100644 api/panel/models/sorter.go create mode 100644 api/panel/models/user.go create mode 100644 api/panel/upload.go create mode 100644 api/panel/user.go create mode 100644 api/tables/category.go create mode 100644 api/tables/content.go create mode 100644 api/tables/model.go create mode 100644 api/tables/role.go create mode 100644 api/tables/user.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 migrate.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..237f0b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea +logs \ No newline at end of file diff --git a/api/home/category.go b/api/home/category.go new file mode 100644 index 0000000..26e1749 --- /dev/null +++ b/api/home/category.go @@ -0,0 +1,95 @@ +package home + +import ( + "github.com/gin-gonic/gin" + "github.com/han-joker/moo-layout/api/home/models" + "github.com/han-joker/moo-layout/api/moo/dbm" + "github.com/han-joker/moo-layout/api/moo/logm" + "github.com/han-joker/moo-layout/api/tables" + "net/http" + "strconv" +) + +func CategoryGets(c *gin.Context) { + query := models.CategoryQuery{} + if err := c.ShouldBind(&query); err != nil { + logm.Get().Info(err.Error()) + } + db := dbm.Get().Model(&tables.Category{}) + if len(query.Filters.Status) > 0 { + db.Where("status IN ?", query.Filters.Status) + } + + rows := []tables.Category{} + if err := db.Order("sorter asc").Find(&rows).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + nested := make([]models.CategoryNested, 0) + + for _, row := range rows { + if row.ID == query.Filters.ID { + nested = append(nested, models.CategoryNested{ + Category: row, + }) + break + } + } + + if len(nested) == 0 { + nested = categoryChildren(rows, query.Filters.ID) + } else if len(nested) == 1 { + nested[0].Children = categoryChildren(rows, query.Filters.ID) + } + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "rows": nested, + }) +} + +func categoryChildren(rows []tables.Category, parentID uint) []models.CategoryNested { + children := make([]models.CategoryNested, 0) + + for _, row := range rows { + if row.ParentID == parentID { + item := models.CategoryNested{ + Category: row, + } + item.Children = categoryChildren(rows, row.ID) + children = append(children, item) + } + } + + return children +} + +func CategoryGet(c *gin.Context) { + + id, err := strconv.Atoi(c.Query("id")) + if err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + row := tables.Category{} + if err := dbm.Get().Where("id=?", id).First(&row).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(200, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "row": row, + }) +} diff --git a/api/home/content.go b/api/home/content.go new file mode 100644 index 0000000..018e540 --- /dev/null +++ b/api/home/content.go @@ -0,0 +1,107 @@ +package home + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/han-joker/moo-layout/api/home/models" + "github.com/han-joker/moo-layout/api/moo/dbm" + "github.com/han-joker/moo-layout/api/moo/logm" + "github.com/han-joker/moo-layout/api/tables" + "net/http" + "strings" +) + +const ( + contentsLimit = 10 +) + +func ContentGets(c *gin.Context) { + query := models.ContentsQuery{} + if err := c.ShouldBind(&query); err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + rows := make([]tables.Content, 0) + db := dbm.Get().Model(&tables.Content{}) + db.Where("status=?", tables.ContentStatusPublish) + + if len(query.Filters.IDs) > 0 { + db.Where("id IN ?", query.Filters.IDs) + } + if query.Filters.CategoryID != 0 { + db.Where("category_id=?", query.Filters.CategoryID) + } + if len(query.Filters.Status) > 0 { + db.Where("status IN ?", query.Filters.Status) + } + if len(query.Filters.Promotes) > 0 { + pc := make([]string, len(query.Filters.Promotes)) + pv := make([]interface{}, len(query.Filters.Promotes)) + for i, p := range query.Filters.Promotes { + pc[i] = "promotes & ? > 0" + pv[i] = p + } + db.Where(strings.Join(pc, " OR "), pv...) + } + + if query.Sorter.Field != "" && query.Sorter.Order != "" { + db.Order(fmt.Sprintf("%s %s", query.Sorter.Field, models.OrderText[query.Sorter.Order])). + Order("updated_at desc") + } else { + db.Order("updated_at desc") + } + + db.Count(&query.Pagination.Total) + + if query.Pagination.PageSize > 0 { + offset := (query.Pagination.Current - 1) * query.Pagination.PageSize + db.Limit(query.Pagination.PageSize).Offset(offset) + } + + db.Find(&rows) + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "rows": rows, + "pagination": query.Pagination, + "filters": query.Filters, + "sorter": query.Sorter, + }) +} + +func ContentGet(c *gin.Context) { + + query := models.ContentQuery{} + if err := c.ShouldBind(&query); err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + row := tables.Content{} + if err := dbm.Get().Where("status=?", tables.ContentStatusPublish).Where("id=?", query.ID).First(&row).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + if query.WithCategory { + dbm.Get().Model(&row).Association("Category").Find(&row.Category) + } + if query.WithUser { + dbm.Get().Model(&row).Association("User").Find(&row.User) + } + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "row": row, + }) +} \ No newline at end of file diff --git a/api/home/models/category.go b/api/home/models/category.go new file mode 100644 index 0000000..2564dc2 --- /dev/null +++ b/api/home/models/category.go @@ -0,0 +1,19 @@ +package models + +import "github.com/han-joker/moo-layout/api/tables" + +type CategoryQueryFilters struct { + ID uint `json:"id" form:"id"` + Status []int `json:"status" form:"status"` +} + +type CategoryQuery struct { + Pagination `json:"pagination" form:"pagination"` + Sorter `json:"sorter" form:"sorter"` + Filters CategoryQueryFilters `json:"filters" form:"filters"` +} + +type CategoryNested struct { + tables.Category + Children []CategoryNested `json:"children"` +} diff --git a/api/home/models/content.go b/api/home/models/content.go new file mode 100644 index 0000000..64c9ed1 --- /dev/null +++ b/api/home/models/content.go @@ -0,0 +1,27 @@ +package models + +var OrderText = map[string]string{ + "ascend": "ASC", + "desend": "DESC", +} + +type ContentsQueryFilters struct { + IDs []uint `json:"ids" form:"ids"` + CategoryID uint `json:"categoryId" form:"categoryId"` + Status []uint `json:"status" form:"status"` + Promotes []int `json:"promotes" form:"promotes"` +} + +type ContentsQuery struct { + Pagination Pagination `json:"pagination" form:"pagination"` + Sorter Sorter `json:"sorter" form:"sorter"` + Filters ContentsQueryFilters `json:"filters" form:"filters"` + WithCategory bool `json:"withCategory" form:"withCategory"'` + WithUser bool `json:"withUser" form:"withUser"'` +} + +type ContentQuery struct { + ID uint `json:"id" form:"id"` + WithCategory bool `json:"withCategory" form:"withCategory"'` + WithUser bool `json:"withUser" form:"withUser"'` +} diff --git a/api/home/models/pagination.go b/api/home/models/pagination.go new file mode 100644 index 0000000..a20d000 --- /dev/null +++ b/api/home/models/pagination.go @@ -0,0 +1,7 @@ +package models + +type Pagination struct { + Current int `json:"current" form:"current"` + Total int64 `json:"total" form:"total"` + PageSize int `json:"pageSize" form:"pageSize"` +} diff --git a/api/home/models/sorter.go b/api/home/models/sorter.go new file mode 100644 index 0000000..dea6a50 --- /dev/null +++ b/api/home/models/sorter.go @@ -0,0 +1,6 @@ +package models + +type Sorter struct { + Field string `json:"field" form:"field"` + Order string `json:"order" form:"order"` +} diff --git a/api/moo/cachem/cache.go b/api/moo/cachem/cache.go new file mode 100644 index 0000000..cb9adf2 --- /dev/null +++ b/api/moo/cachem/cache.go @@ -0,0 +1,47 @@ +package cachem + +var pool = map[string]*cache{} + +var optionDefault = Option{} + +type cache struct { + option Option +} + +type Option struct { + Name string +} + +// New 创建对象 +func New(option ...Option) *cache { + verifiedOption := optionVerify(option...) + return create(verifiedOption) +} + +// Get 存在直接返回,否则创建、存储再返回 +func Get(option ...Option) *cache { + verifiedOption := optionVerify(option...) + if !Has(verifiedOption.Name) { + pool[verifiedOption.Name] = create(verifiedOption) + } + return pool[verifiedOption.Name] +} + +// Has 存在返回 true,否则返回 false +func Has(name string) bool { + _, has := pool[name] + return has +} + +func create(option Option) *cache { + return &cache{ + option: option, + } +} +func optionVerify(option ...Option) Option { + opt := optionDefault + if len(option) > 0 { + opt = option[0] + } + return opt +} diff --git a/api/moo/cachem/cache_test.go b/api/moo/cachem/cache_test.go new file mode 100644 index 0000000..d77b66f --- /dev/null +++ b/api/moo/cachem/cache_test.go @@ -0,0 +1,30 @@ +package cachem + +import ( + "testing" +) + +func TestNew(t *testing.T) { + one := New() + two := New() + if one == two { + t.Error("new() same instance") + } +} + +func TestGet(t *testing.T) { + one := Get() + two := Get() + if one != two { + t.Error("get not single instance") + } + three := Get(Option{ + Name: "three", + }) + four := Get(Option{ + Name: "four", + }) + if three == four { + t.Error("get() same instance") + } +} diff --git a/api/moo/confm/config.go b/api/moo/confm/config.go new file mode 100644 index 0000000..edfc245 --- /dev/null +++ b/api/moo/confm/config.go @@ -0,0 +1,435 @@ +package confm + +import ( + "errors" + "gopkg.in/yaml.v3" + "log" + "os" + "strconv" + "strings" +) + +//变量 +//实例池 +var pool = map[string]*config{} + +//默认选项值 +var optionDefault = Option{ + Path: "./configs/", + Ext: ".yml", + Sep: ".", +} + +//类型 +//配置内容 +type contents = map[string]map[string]interface{} +type config struct { + option Option + //配置文件内容解码缓存 + contents +} +type Option struct { + Name string + + Path string + Ext string + Sep string +} + +// New 创建对象 +func New(option ...Option) *config { + verifiedOption := optionVerify(option...) + return create(verifiedOption) +} + +// Get 存在直接返回,否则创建、存储再返回 +func Get(option ...Option) *config { + verifiedOption := optionVerify(option...) + if !Has(verifiedOption.Name) { + pool[verifiedOption.Name] = create(verifiedOption) + } + return pool[verifiedOption.Name] +} + +// Has 存在返回 true,否则返回 false +func Has(name string) bool { + _, has := pool[name] + return has +} + +func create(option Option) *config { + return &config{ + option: option, + contents: contents{}, + } +} +func optionVerify(option ...Option) Option { + opt := optionDefault + if len(option) > 0 { + opt.Name = option[0].Name + if option[0].Path != "" { + opt.Path = option[0].Path + } + if option[0].Ext != "" { + opt.Ext = option[0].Ext + if !strings.HasPrefix(opt.Ext, ".") { + opt.Ext = "." + opt.Ext + } + } + if option[0].Sep != "" { + opt.Sep = option[0].Sep + } + } + return opt +} + +//可导出包方法 + +// Bool Getter|Setter +func (c config) Bool(key string) bool { + valueIf, err := c.value(key) + if err != nil { + return false + } + value, ok := valueIf.(bool) + if !ok { + return false + } + return value +} + +func (c config) String(key string) string { + valueIf, err := c.value(key) + if err != nil { + return "" + } + return assertString(valueIf) +} + +func (c config) Int(key string) int { + valueIf, err := c.value(key) + if err != nil { + log.Println(err) + return 0 + } + value, ok := valueIf.(int) + if !ok { + return 0 + } + return value +} + +func (c config) Uint(key string) uint { + valueIf, err := c.value(key) + if err != nil { + log.Println(err) + return 0 + } + value, ok := valueIf.(uint) + if !ok { + return 0 + } + return value +} + +func (c config) Float64(key string) float64 { + valueIf, err := c.value(key) + if err != nil { + return 0 + } + return assertFloat64(valueIf) +} + +func (c config) Float32(key string) float32 { + valueIf, err := c.value(key) + if err != nil { + return 0 + } + return assertFloat32(valueIf) +} + +func (c config) BoolSlice(key string) []bool { + valueIf, err := c.value(key) + if err != nil { + return []bool{} + } + // + sli, ok := valueIf.([]interface{}) + if !ok { + return []bool{} + } + value := make([]bool, len(sli)) + for i, vi := range sli { + v, ok := vi.(bool) + if ok { + value[i] = v + } + } + return value +} + +func (c config) IntSlice(key string) []int { + valueIf, err := c.value(key) + if err != nil { + return []int{} + } + // + sli, ok := valueIf.([]interface{}) + if !ok { + return []int{} + } + value := make([]int, len(sli)) + for i, vi := range sli { + v, ok := vi.(int) + if ok { + value[i] = v + } + } + return value +} + +func (c config) StringSlice(key string) []string { + valueIf, err := c.value(key) + if err != nil { + return []string{} + } + // + sli, ok := valueIf.([]interface{}) + if !ok { + return []string{} + } + value := make([]string, len(sli)) + for i, vi := range sli { + value[i] = assertString(vi) + } + return value +} + +func (c config) Float64Slice(key string) []float64 { + valueIf, err := c.value(key) + if err != nil { + return []float64{} + } + // + sli, ok := valueIf.([]interface{}) + if !ok { + return []float64{} + } + value := make([]float64, len(sli)) + for i, vi := range sli { + value[i] = assertFloat64(vi) + } + return value +} + +func (c config) Float32Slice(key string) []float32 { + valueIf, err := c.value(key) + if err != nil { + return []float32{} + } + // + sli, ok := valueIf.([]interface{}) + if !ok { + return []float32{} + } + + value := make([]float32, len(sli)) + for i, vi := range sli { + value[i] = assertFloat32(vi) + } + return value +} + +func (c config) BoolMap(key string) map[string]bool { + valueIf, err := c.value(key) + if err != nil { + return map[string]bool{} + } + // + mp, ok := valueIf.(map[string]interface{}) + if !ok { + return map[string]bool{} + } + value := map[string]bool{} + for k, vk := range mp { + value[k] = false + v, ok := vk.(bool) + if ok { + value[k] = v + } + } + return value +} + +func (c config) IntMap(key string) map[string]int { + valueIf, err := c.value(key) + if err != nil { + return map[string]int{} + } + // + mp, ok := valueIf.(map[string]interface{}) + if !ok { + return map[string]int{} + } + value := map[string]int{} + for k, vk := range mp { + value[k] = 0 + v, ok := vk.(int) + if ok { + value[k] = v + } + } + return value +} + +func (c config) Float64Map(key string) map[string]float64 { + valueIf, err := c.value(key) + if err != nil { + return map[string]float64{} + } + // + mp, ok := valueIf.(map[string]interface{}) + if !ok { + return map[string]float64{} + } + + value := map[string]float64{} + for k, vk := range mp { + value[k] = assertFloat64(vk) + } + return value +} + +func (c config) Float32Map(key string) map[string]float32 { + valueIf, err := c.value(key) + if err != nil { + return map[string]float32{} + } + // + mp, ok := valueIf.(map[string]interface{}) + if !ok { + return map[string]float32{} + } + + value := map[string]float32{} + for k, vk := range mp { + value[k] = assertFloat32(vk) + } + return value +} + +func (c config) StringMap(key string) map[string]string { + valueIf, err := c.value(key) + if err != nil { + return map[string]string{} + } + // + mp, ok := valueIf.(map[string]interface{}) + if !ok { + return map[string]string{} + } + value := map[string]string{} + for k, vk := range mp { + value[k] = assertString(vk) + } + return value +} + +//非导出方法 + +//利用 key 获取 值 +func (c config) value(key string) (interface{}, error) { + filename, keys := c.parseKey(key) + if data, exists := c.contents[filename]; !exists { + var ( + content = []byte{} + err error + ) + if content, err = c.getContent(filename); err != nil { + log.Println(err) + return nil, err + } + if err = yaml.Unmarshal(content, &data); err != nil { + log.Println(err) + return nil, err + } + c.contents[filename] = data + } + + //解析 + for i, l, currLevel := 0, len(keys), c.contents[filename]; i < l; i++ { + //最后一级 key + if i == l-1 { + if value, exists := currLevel[keys[i]]; exists { + return value, nil + } else { + return nil, errors.New("key is not exists") + } + } + + //不是最后一级,继续解析 + var exists bool + currLevel, exists = currLevel[keys[i]].(map[string]interface{}) + if !exists { + return nil, errors.New("key is not exists") + } + } + return nil, nil +} + +//解析 key +func (c config) parseKey(key string) (string, []string) { + strs := strings.Split(key, c.option.Sep) + if len(strs) == 1 { + strs = append([]string{"app"}, strs...) + } + return strs[0], strs[1:] +} + +//读取配置文件 +func (c config) getContent(filename string) ([]byte, error) { + return os.ReadFile(c.option.Path + filename + c.option.Ext) +} + +//断言 Interface{} to string +func assertString(valueIf interface{}) (value string) { + switch v := valueIf.(type) { + case string: + value = v + case int: + value = strconv.FormatInt(int64(v), 10) //string(v) + case float64: + value = strconv.FormatFloat(v, 'f', -1, 64) + case float32: + value = strconv.FormatFloat(float64(v), 'f', -1, 64) + case bool: + value = strconv.FormatBool(v) + } + return +} + +//断言 Interface{} to float32 +func assertFloat32(valueIf interface{}) (value float32) { + switch v := valueIf.(type) { + case float64: + value = float32(v) + case float32: + value = v + case int: + value = float32(v) + } + return +} + +//断言 Interface{} to float64 +func assertFloat64(valueIf interface{}) (value float64) { + switch v := valueIf.(type) { + case float64: + value = v + case float32: + value = float64(v) + case int: + value = float64(v) + } + return +} diff --git a/api/moo/confm/config_test.go b/api/moo/confm/config_test.go new file mode 100644 index 0000000..c4e048d --- /dev/null +++ b/api/moo/confm/config_test.go @@ -0,0 +1,20 @@ +package confm + +import ( + "testing" +) + +func TestInstance(t *testing.T) { + m1 := Get() + m2 := Get() + if m1 != m2 { + t.Error("no singleton") + } +} + +func BenchmarkInstance(b *testing.B) { + for i := 0; i < b.N; i++ { + //New() + Get() + } +} diff --git a/api/moo/dbm/db.go b/api/moo/dbm/db.go new file mode 100644 index 0000000..4cadc4c --- /dev/null +++ b/api/moo/dbm/db.go @@ -0,0 +1,68 @@ +package dbm + +import ( + "fmt" + "github.com/han-joker/moo-layout/api/moo/confm" + "github.com/han-joker/moo-layout/api/moo/logm" + "github.com/han-joker/moo-layout/api/moo/toolm" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +const defaultType = "sqlite" + +type DSN = string + +var pool = map[DSN]*gorm.DB{} + +type Option struct { + DSN + Options []gorm.Option +} + +// New 创建对象 +func New(options ...Option) *gorm.DB { + option := verifyOption(options) + return create(option) +} + +// Get 存在直接返回,否则创建、存储再返回 +func Get(options ...Option) *gorm.DB { + option := verifyOption(options) + if !Has(option.DSN) { + pool[option.DSN] = create(option) + } + return pool[option.DSN] + +} + +func verifyOption(options []Option) Option { + t := toolm.StringDefault(confm.Get().String("app.databaseType"), defaultType) + option := Option{ + DSN: confm.Get().String(fmt.Sprintf("%s.DSN", t)), + Options: []gorm.Option{}, + } + if len(options) > 0 { + if options[0].DSN != "" { + option.DSN = options[0].DSN + } + if options[0].Options != nil { + option.Options = options[0].Options + } + } + return option +} + +// Has 存在返回 true,否则返回 false +func Has(dialector DSN) bool { + _, has := pool[dialector] + return has && pool[dialector] != nil +} + +func create(option Option) *gorm.DB { + db, err := gorm.Open(sqlite.Open(option.DSN), option.Options...) + if err != nil { + logm.Get().Error(err) + } + return db +} diff --git a/api/moo/logm/log.go b/api/moo/logm/log.go new file mode 100644 index 0000000..57fadb3 --- /dev/null +++ b/api/moo/logm/log.go @@ -0,0 +1,233 @@ +package logm + +import ( + "fmt" + toolm2 "github.com/han-joker/moo-layout/api/moo/toolm" + "github.com/sirupsen/logrus" + "io" + "os" + "path" + "time" +) + +//常量 + +//日志文件后缀 +const fileExt = ".log" + +//日志格式 +const ( + Text = iota + Json +) + +// OutMode 预设值 +const ( + Console = iota // 控制台 + File // 文件,单文件模式 + FilePerDay // 每天一个文件 + FilePerWeek // 每周一个文件 + FilePerMonth // 每月一个文件 + FilePerHour // 每小时一个文件 + FilePerSize // 文件固定大小 + User +) + +//变量 + +//池 +var pool = map[string]*log{} + +//格式集合 +var fmtContainer = []int{Text, Json} + +//输出模式 +var outModeContainer = []int{Console, File, FilePerDay, FilePerWeek, FilePerMonth, FilePerHour, FilePerSize, User} + +//默认选项 +var optionDefault = Option{ + Fmt: Text, + Caller: false, + OutMode: Console, + Path: "./logs/", + FilePrefix: "moo", + SizeMax: 100 * 1024 * 1024, // 100 M Bytes + Output: os.Stdout, +} + +//类型 + +//日志 +type log struct { + *logrus.Logger + option Option + // cache + writers map[string]*os.File +} + +// Option 选项 +type Option struct { + Name string + + Fmt int // Text, Json + Caller bool // true, false + OutMode int // Console, FILE, User + Path string // some path + FilePrefix string // + SizeMax int64 + Output io.Writer +} + +type Fields = logrus.Fields + +// New 创建对象 +func New(option ...Option) *log { + verifiedOption := optionVerify(option...) + l := create(verifiedOption) + l.refreshOutMode() + return l +} + +// Get 存在直接返回,否则创建、存储再返回 +func Get(option ...Option) *log { + verifiedOption := optionVerify(option...) + if !Has(verifiedOption.Name) { + pool[verifiedOption.Name] = create(verifiedOption) + } + pool[verifiedOption.Name].refreshOutMode() + return pool[verifiedOption.Name] +} + +// Has 存在返回 true,否则返回 false +func Has(name string) bool { + _, has := pool[name] + return has +} + +func (l *log) syncLoggerOption() *log { + switch l.option.Fmt { + case Json: + l.Logger.SetFormatter(&logrus.JSONFormatter{}) + case Text: + l.Logger.SetFormatter(&logrus.TextFormatter{}) + default: + l.Logger.SetFormatter(&logrus.TextFormatter{}) + } + + l.Logger.SetReportCaller(l.option.Caller) + + l.Logger.SetOutput(l.option.Output) + + return l +} +func (l *log) refreshOutMode() *log { + // 初始默认值 + l.Logger.SetOutput(optionDefault.Output) + filename := "" + switch l.option.OutMode { + case File: + filename = l.option.Path + "/" + l.option.FilePrefix + fileExt + case FilePerHour: + now := time.Now() + filename = l.option.Path + "/" + + l.option.FilePrefix + + fmt.Sprintf("%04d-%02d-%02d-%02d", now.Year(), now.Month(), now.Day(), now.Hour()) + + fileExt + case FilePerDay: + now := time.Now() + filename = l.option.Path + "/" + + l.option.FilePrefix + + fmt.Sprintf("%04d-%02d-%02d", now.Year(), now.Month(), now.Day()) + + fileExt + case FilePerMonth: + now := time.Now() + filename = l.option.Path + "/" + + l.option.FilePrefix + + fmt.Sprintf("%04d-%02d", now.Year(), now.Month()) + + fileExt + case FilePerWeek: + now := time.Now() + year, week := now.ISOWeek() + filename = l.option.Path + "/" + + l.option.FilePrefix + + fmt.Sprintf("%04d-%02d", year, week) + + fileExt + case FilePerSize: + filename = l.option.Path + "/" + l.option.FilePrefix + fileExt + if fileinfo, err := os.Stat(filename); err == nil { + if fileinfo.Size() >= l.option.SizeMax { + basename := path.Base(filename) + if file, exists := l.writers[basename]; exists { + if err := file.Close(); err != nil { + l.Info("can not close file") + } else { + delete(l.writers, basename) + } + } + now := time.Now() + newFilename := l.option.Path + "/" + + l.option.FilePrefix + + fmt.Sprintf("%s", now.Format("2006-01-02-15-04-05")) + + fileExt + if err := os.Rename(filename, newFilename); err != nil { + l.Info("can not rename log file") + } + } + } + case User: + l.Logger.SetOutput(l.option.Output) + default: + l.Logger.SetOutput(optionDefault.Output) + } + if filename != "" { + filepath := path.Dir(filename) + if err := os.MkdirAll(filepath, 0644); err == nil || os.IsExist(err) { + basename := path.Base(filename) + file, exists := l.writers[basename] + if !exists { + if file, err = os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666); err != nil { + l.Info("can not create log file") + } + } + if file != nil { + l.Logger.SetOutput(file) + l.writers[basename] = file + } else { + l.Info("can not open log file") + } + } else { + l.Info("can not make log path") + } + + } + + return l +} +func create(option Option) *log { + return (&log{ + Logger: logrus.New(), + option: option, + writers: map[string]*os.File{}, + }).syncLoggerOption() +} +func optionVerify(option ...Option) Option { + opt := optionDefault + if len(option) > 0 { + //设置选项 + opt.Name = option[0].Name + if toolm2.IntSliceContains(option[0].Fmt, fmtContainer) { + opt.Fmt = option[0].Fmt + } + opt.Caller = option[0].Caller + if toolm2.IntSliceContains(option[0].OutMode, outModeContainer) { + opt.OutMode = option[0].OutMode + } + opt.Path = option[0].Path + opt.FilePrefix = toolm2.StringDefault(option[0].FilePrefix, opt.Name, optionDefault.FilePrefix) + opt.SizeMax = toolm2.Int64Default(option[0].SizeMax, optionDefault.SizeMax) + if _, ok := option[0].Output.(io.Writer); ok { + opt.Output = option[0].Output + } + } + return opt +} diff --git a/api/moo/logm/log_test.go b/api/moo/logm/log_test.go new file mode 100644 index 0000000..bac7a07 --- /dev/null +++ b/api/moo/logm/log_test.go @@ -0,0 +1,54 @@ +package logm + +import ( + "testing" +) + +// +func TestNew(t *testing.T) { + one := New() + two := New() + if one == two { + t.Error("new error") + } + ins := New(Option{ + OutMode: File, + Path: "./logs", + }) + ins.Info("some message") +} + +func TestGet(t *testing.T) { + one := Get() + two := Get() + if one != two { + t.Error("get error") + } +} +func TestField(t *testing.T) { + one := Get() + one.WithField("key", []int{1, 2, 3}).Info("Fields") + one.WithFields(Fields{"key": "one", "hello": 123, "xxx": []int{1, 2, 3}}).Info("Fields") +} + +func BenchmarkNew(b *testing.B) { + for i := 0; i < b.N; i++ { + New() + } +} +func BenchmarkGet(b *testing.B) { + for i := 0; i < b.N; i++ { + Get() + } +} + +func BenchmarkLog_Info(b *testing.B) { + for i := 0; i < b.N; i++ { + ins := Get(Option{ + OutMode: FilePerSize, + SizeMax: 0.5 * 1024 * 1024, + Path: "./logs", + }) + ins.Info("some message") + } +} diff --git a/api/moo/server.go b/api/moo/server.go new file mode 100644 index 0000000..3e003ca --- /dev/null +++ b/api/moo/server.go @@ -0,0 +1,32 @@ +package moo + +type server struct { + // server kind + Kind string +} + +// 服务单例池 +var serverPool = map[string]*server{} + +// 单例工厂,标识于 Kind +func Server(kind string) *server { + instance, exists := serverPool[kind] + if !exists { + instance = &server{ + Kind: kind, + } + serverPool[kind] = instance + } + return instance +} + +func (s *server) Start() { + switch s.Kind { + case "http": + s.StartHttp() + case "websocket": + fallthrough + case "ws": + s.StartWebSocket() + } +} diff --git a/api/moo/serverHttp.go b/api/moo/serverHttp.go new file mode 100644 index 0000000..4b1994b --- /dev/null +++ b/api/moo/serverHttp.go @@ -0,0 +1,11 @@ +package moo + +import ( + "github.com/han-joker/moo-layout/api/moo/confm" + "github.com/han-joker/moo-layout/routers" +) + +func (s *server) StartHttp() { + r := routers.RouterInit() + r.Run(confm.Get().String("http.addr")) +} diff --git a/api/moo/serverWebsocket.go b/api/moo/serverWebsocket.go new file mode 100644 index 0000000..6e908ef --- /dev/null +++ b/api/moo/serverWebsocket.go @@ -0,0 +1,41 @@ +package moo + +import ( + "github.com/gorilla/websocket" + "github.com/han-joker/moo-layout/api/moo/confm" + "log" + "net/http" +) + +func (s *server) StartWebSocket() { + http.HandleFunc("/", echo) + log.Fatal(http.ListenAndServe(confm.Get().String("websocket.addr"), nil)) +} + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func echo(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Print("upgrade:", err) + return + } + defer c.Close() + for { + mt, message, err := c.ReadMessage() + if err != nil { + log.Println("read:", err) + break + } + log.Printf("recv: %s", message) + err = c.WriteMessage(mt, message) + if err != nil { + log.Println("write:", err) + break + } + } +} diff --git a/api/moo/toolm/default.go b/api/moo/toolm/default.go new file mode 100644 index 0000000..c325076 --- /dev/null +++ b/api/moo/toolm/default.go @@ -0,0 +1,25 @@ +package toolm + +func StringDefault(value string, defValues ...string) string { + if value != "" { + return value + } + for _, v := range defValues { + if v != "" { + return v + } + } + return "" +} + +func Int64Default(value int64, defValues ...int64) int64 { + if value != 0 { + return value + } + for _, v := range defValues { + if v != 0 { + return v + } + } + return 0 +} diff --git a/api/moo/toolm/slice.go b/api/moo/toolm/slice.go new file mode 100644 index 0000000..42a2050 --- /dev/null +++ b/api/moo/toolm/slice.go @@ -0,0 +1,10 @@ +package toolm + +func IntSliceContains(value int, container []int) bool { + for _, v := range container { + if v == value { + return true + } + } + return false +} diff --git a/api/moo/toolm/string.go b/api/moo/toolm/string.go new file mode 100644 index 0000000..0509b6c --- /dev/null +++ b/api/moo/toolm/string.go @@ -0,0 +1,49 @@ +package toolm + +import ( + "crypto/hmac" + "crypto/sha256" + "fmt" + "math/rand" + "time" +) + +const ( + CharsAll = 1 << iota + Digit + LowerCase + UpperCase +) + +const ( + digit = "0123456789" + lowercase = "abcdefghijklmnopqrstuvwxyz" + uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + //octal = "01234567" + //hex = "0123456789abcdefg" +) + +func RandString(l, chars int) string { + charset := "" + if chars&CharsAll > 0 { + charset = digit + lowercase + uppercase + } + + charsetLen := len(charset) + + seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) + + b := make([]byte, l) + for i := range b { + b[i] = charset[seededRand.Intn(charsetLen)] + } + + return string(b) +} + +func Sha256HMacString(message, key string) string { + mac := hmac.New(sha256.New, []byte(key)) + mac.Write([]byte(message)) + expectedMAC := mac.Sum(nil) + return fmt.Sprintf("%X", expectedMAC) +} diff --git a/api/panel/category.go b/api/panel/category.go new file mode 100644 index 0000000..954a22f --- /dev/null +++ b/api/panel/category.go @@ -0,0 +1,265 @@ +package panel + +import ( + "errors" + "github.com/gin-gonic/gin" + "github.com/han-joker/moo-layout/api/moo/dbm" + "github.com/han-joker/moo-layout/api/moo/logm" + "github.com/han-joker/moo-layout/api/panel/models" + "github.com/han-joker/moo-layout/api/tables" + "net/http" + "strconv" +) + +func CategoryPost(c *gin.Context) { + req := models.CategoryReq{} + if err := c.ShouldBind(&req); err != nil { + c.JSON(200, gin.H{ + "error": err.Error(), + }) + return + } + + row := &tables.Category{ + Name: req.Name, + Description: req.Description, + ParentID: req.ParentID, + Sorter: req.Sorter, + Status: req.Status, + } + + if err := dbm.Get().Create(&row).Error; err != nil { + c.JSON(200, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(200, gin.H{ + "error": nil, + "row": row, + }) +} + +func CategoryPut(c *gin.Context) { + id, err := strconv.Atoi(c.Query("id")) + if err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + row := tables.Category{} + if err := dbm.Get().Where("id=?", id).First(&row).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + req := models.CategoryReq{} + if err := c.ShouldBind(&req); err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + row.Name = req.Name + row.Description = req.Description + row.Status = req.Status + row.Sorter = req.Sorter + row.ParentID = req.ParentID + + if err := dbm.Get().Save(&row).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "row": row, + }) +} + +func CategoryGets(c *gin.Context) { + query := models.CategoryQuery{} + if err := c.ShouldBind(&query); err != nil { + } + + rows := []tables.Category{} + if err := dbm.Get().Order("sorter asc").Find(&rows).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + nested := make([]models.CategoryNested, 0) + for _, row := range rows { + if row.ID == query.Filters.ID { + nested = append(nested, models.CategoryNested{ + Category: row, + }) + break + } + } + if len(nested) == 0 { + nested = categoryChildren(rows, query.Filters.ID) + } else if len(nested) == 1 { + nested[0].Children = categoryChildren(rows, query.Filters.ID) + } + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "rows": nested, + }) +} + +func categoryChildren(rows []tables.Category, parentID uint) []models.CategoryNested { + children := make([]models.CategoryNested, 0) + + for _, row := range rows { + if row.ParentID == parentID { + item := models.CategoryNested{ + Category: row, + } + item.Children = categoryChildren(rows, row.ID) + children = append(children, item) + } + } + + return children +} + +func CategoryGet(c *gin.Context) { + + id, err := strconv.Atoi(c.Query("id")) + if err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error":err.Error(), + }) + return + } + + row := tables.Category{} + if err := dbm.Get().Where("id=?", id).First(&row).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "row": row, + }) +} + +func CategoryDelete(c *gin.Context) { + id, err := strconv.Atoi(c.Query("id")) + if err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + row := tables.Category{} + if err := dbm.Get().Where("id=?", id).First(&row).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + childrenSize := int64(0) + if err := dbm.Get().Model(&tables.Category{}).Where("parent_id=?", row.ID).Count(&childrenSize).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + if childrenSize > 0 { + err := errors.New("has children") + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + if err := dbm.Get().Delete(&row).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "row": row, + }) + return + +} + +func CategoryCount(c *gin.Context) { + + query := models.CategoryQuery{} + if err := c.ShouldBind(&query); err != nil { + } + + + + rows := []tables.Category{} + if err := dbm.Get().Find(&rows).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + count := categoryCount(rows, query) + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "count": count, + }) +} + +func categoryCount(rows []tables.Category, query models.CategoryQuery) int64 { + count := int64(0) + + for _, row := range rows { + statusCond := true + if query.Filters.Status > 0 { + statusCond = row.Status == query.Filters.Status + } + + if row.ParentID == query.Filters.ParentID && statusCond { + count += 1 + count += categoryCount(rows, models.CategoryQuery{ + Filters: models.CategoryQueryFilters{ + ParentID: row.ID, + Status: query.Filters.Status, + }, + }) + } + } + + return count +} diff --git a/api/panel/content.go b/api/panel/content.go new file mode 100644 index 0000000..31b8f24 --- /dev/null +++ b/api/panel/content.go @@ -0,0 +1,260 @@ +package panel + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/han-joker/moo-layout/api/moo/dbm" + "github.com/han-joker/moo-layout/api/moo/logm" + "github.com/han-joker/moo-layout/api/panel/models" + "github.com/han-joker/moo-layout/api/tables" + "net/http" + "strconv" + "strings" + "time" +) + +const ( + contentsLimit = 10 +) + +func ContentPost(c *gin.Context) { + req := models.ContentReq{} + if err := c.ShouldBind(&req); err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + row := &tables.Content{ + Subject: req.Subject, + Content: req.Content, + Referer: req.Referer, + CategoryID: req.CategoryID, + Cover: req.Cover, + Status: req.Status, + Sorter: req.Sorter, + UserID: 1, + } + if row.Status == tables.ContentStatusPublish { + row.PublishTime = time.Now() + } + for _, p := range req.Promotes { + row.Promotes |= p + } + + if err := dbm.Get().Create(&row).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "row": row, + }) +} + +func ContentPut(c *gin.Context) { + id, err := strconv.Atoi(c.Query("id")) + if err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + row := tables.Content{} + if err := dbm.Get().Where("id=?", id).First(&row).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + req := models.ContentReq{} + if err := c.ShouldBind(&req); err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + row.Subject = req.Subject + row.Content = req.Content + row.Referer = req.Referer + row.CategoryID = req.CategoryID + row.Cover = req.Cover + row.Status = req.Status + row.Sorter = req.Sorter + if row.Status == tables.ContentStatusPublish { + row.PublishTime = time.Now() + } + row.Promotes = 0 + for _, p := range req.Promotes { + row.Promotes |= p + } + + if err := dbm.Get().Save(&row).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "row": row, + }) +} + +func ContentGets(c *gin.Context) { + query := models.ContentsQuery{} + if err := c.ShouldBind(&query); err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + rows := make([]tables.Content, 0) + db := dbm.Get().Model(&tables.Content{}) + + if len(query.Filters.IDs) > 0 { + db.Where("id IN ?", query.Filters.IDs) + } + if query.Filters.CategoryID != 0 { + db.Where("category_id=?", query.Filters.CategoryID) + } + if len(query.Filters.Status) > 0 { + db.Where("status IN ?", query.Filters.Status) + } + if len(query.Filters.Promotes) > 0 { + pc := make([]string, len(query.Filters.Promotes)) + pv := make([]interface{}, len(query.Filters.Promotes)) + for i, p := range query.Filters.Promotes { + pc[i] = "promotes & ? > 0" + pv[i] = p + } + db.Where(strings.Join(pc, " OR "), pv...) + } + + if query.Sorter.Field != "" && query.Sorter.Order != "" { + db.Order(fmt.Sprintf("%s %s", query.Sorter.Field, models.OrderText[query.Sorter.Order])). + Order("updated_at desc") + } else { + db.Order("updated_at desc") + } + + db.Count(&query.Pagination.Total) + + if query.Pagination.PageSize > 0 { + offset := (query.Pagination.Current - 1) * query.Pagination.PageSize + db.Limit(query.Pagination.PageSize).Offset(offset) + } + + db.Find(&rows) + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "rows": rows, + "pagination": query.Pagination, + "filters": query.Filters, + "sorter": query.Sorter, + }) +} + +func ContentGet(c *gin.Context) { + + id, err := strconv.Atoi(c.Query("id")) + if err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + row := tables.Content{} + if err := dbm.Get().Where("id=?", id).First(&row).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "row": row, + }) +} + +func ContentDelete(c *gin.Context) { + id, err := strconv.Atoi(c.Query("id")) + if err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + row := tables.Content{} + if err := dbm.Get().Where("id=?", id).First(&row).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + if err := dbm.Get().Delete(&row).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "row": row, + }) +} + +func ContentCount(c *gin.Context) { + + query := models.ContentsQuery{} + if err := c.ShouldBind(&query); err != nil { + } + + db := dbm.Get().Model(&tables.Content{}) + + if query.Filters.CategoryID != 0 { + db.Where("category_id=?", query.Filters.CategoryID) + } + if len(query.Filters.Status) != 0 { + db.Where("status IN ?", query.Filters.Status) + } + count := int64(0) + if err := db.Debug().Count(&count).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "count": count, + }) + +} diff --git a/api/panel/middlewares/jwt.go b/api/panel/middlewares/jwt.go new file mode 100644 index 0000000..bf4996a --- /dev/null +++ b/api/panel/middlewares/jwt.go @@ -0,0 +1,94 @@ +package middlewares + +import ( + "errors" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt" + "github.com/han-joker/moo-layout/api/moo/confm" + "github.com/han-joker/moo-layout/api/moo/dbm" + "github.com/han-joker/moo-layout/api/moo/logm" + "github.com/han-joker/moo-layout/api/panel/models" + "github.com/han-joker/moo-layout/api/tables" + "net/http" + "strconv" + "strings" +) + +func JwtToken(c *gin.Context) { + // before request + header := models.UserJwtTokenHeader{} + if err := c.ShouldBindHeader(&header); err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + c.Abort() + return + } + + tokenString := strings.Replace(header.Authorization, "Bearer ", "", 1) + token, err := jwt.ParseWithClaims(tokenString, &jwt.StandardClaims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(confm.Get().String("app.signingKey")), nil + }) + if err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + c.Abort() + return + } + + claims, ok := token.Claims.(*jwt.StandardClaims) + if !ok { + err := errors.New("token claim type error") + logm.Get().Info(err.Error()) + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + c.Abort() + return + } + + if !token.Valid { + err := errors.New("token valid error") + logm.Get().Info(err.Error()) + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + c.Abort() + return + } + + id, err := strconv.ParseUint(claims.Audience, 10, 0) + if err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + c.Abort() + return + } + user := tables.User{} + if err := dbm.Get().Where("id=?", id).First(&user).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + c.Abort() + return + } + + if user.JWTToken != tokenString { + err := errors.New("token error") + logm.Get().Info(err.Error()) + c.JSON(http.StatusForbidden, gin.H{ + "error": err.Error(), + }) + c.Abort() + return + } + + c.Set("user", user) + c.Next() +} diff --git a/api/panel/models/category.go b/api/panel/models/category.go new file mode 100644 index 0000000..d5b706f --- /dev/null +++ b/api/panel/models/category.go @@ -0,0 +1,27 @@ +package models + +import "github.com/han-joker/moo-layout/api/tables" + +type CategoryQueryFilters struct { + ID uint `json:"id" form:"id"` + ParentID uint `json:"parent_id" form:"parent_id"` + Status int `json:"status" form:"status"` +} + +type CategoryQuery struct { + Filters CategoryQueryFilters `json:"filters" form:"filters"` +} + +type CategoryNested struct { + tables.Category + Children []CategoryNested `json:"children"` +} + +type CategoryReq struct { + ID uint `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + ParentID uint `json:"parent_id" form:"parent_id"` + Sorter int `json:"sorter"` + Status int `json:"status"` +} diff --git a/api/panel/models/content.go b/api/panel/models/content.go new file mode 100644 index 0000000..d6f982a --- /dev/null +++ b/api/panel/models/content.go @@ -0,0 +1,33 @@ +package models + +type ContentReq struct { + ID uint `json:"id" form:"id"` + Subject string `json:"subject" form:"subject"` + Content string `json:"content" form:"content"` + Referer string `json:"referer" form:"referer"` + CategoryID uint `json:"categoryId" form:"categoryId"` + Status int `json:"status" form:"status"` + Sorter int `json:"sorter" form:"sorter"` + Cover string `json:"cover" form:"cover"` + Promotes []int `json:"promotes" form:"promotes"` +} + +type ContentsQueryFilters struct { + IDs []uint `json:"ids" form:"ids"` + CategoryID uint `json:"categoryId" form:"categoryId"` + Status []uint `json:"status" form:"status"` + Promotes []int `json:"promotes" form:"promotes"` +} + +type ContentsQuery struct { + Pagination Pagination `json:"pagination" form:"pagination"` + Sorter Sorter `json:"sorter" form:"sorter"` + Filters ContentsQueryFilters `json:"filters" form:"filters"` + WithCategory bool `json:"withCategory" form:"withCategory"'` + WithUser bool `json:"withUser" form:"withUser"'` +} + +var OrderText = map[string]string{ + "ascend": "ASC", + "desend": "DESC", +} diff --git a/api/panel/models/pagination.go b/api/panel/models/pagination.go new file mode 100644 index 0000000..a20d000 --- /dev/null +++ b/api/panel/models/pagination.go @@ -0,0 +1,7 @@ +package models + +type Pagination struct { + Current int `json:"current" form:"current"` + Total int64 `json:"total" form:"total"` + PageSize int `json:"pageSize" form:"pageSize"` +} diff --git a/api/panel/models/sorter.go b/api/panel/models/sorter.go new file mode 100644 index 0000000..dea6a50 --- /dev/null +++ b/api/panel/models/sorter.go @@ -0,0 +1,6 @@ +package models + +type Sorter struct { + Field string `json:"field" form:"field"` + Order string `json:"order" form:"order"` +} diff --git a/api/panel/models/user.go b/api/panel/models/user.go new file mode 100644 index 0000000..0590748 --- /dev/null +++ b/api/panel/models/user.go @@ -0,0 +1,18 @@ +package models + +type UserSignIn struct { + Username string `json:"username" form:"username"` + Password string `json:"password" form:"password"` + AutoSignIn bool `json:"autoSignIn" form:"autoSignIn"` +} + +type UserJwtTokenHeader struct { + Authorization string `header:"Authorization" form:"authorization"` +} + +type UserChangePassword struct { + ID uint `json:"id" form:"id"` + Password string `json:"password" form:"password"` + NewPassword string `json:"newPassword" form:"newPassword"` + NewPasswordConfirm string `json:"newPasswordConfirm" form:"newPasswordConfirm"` +} diff --git a/api/panel/upload.go b/api/panel/upload.go new file mode 100644 index 0000000..08bc27a --- /dev/null +++ b/api/panel/upload.go @@ -0,0 +1,173 @@ +package panel + +import ( + "crypto/md5" + "errors" + "fmt" + "io" + "net/http" + "os" + "path" + + "github.com/gin-gonic/gin" + "github.com/han-joker/moo-layout/api/moo/confm" + "github.com/han-joker/moo-layout/api/moo/logm" + "github.com/han-joker/moo-layout/api/moo/toolm" +) + +func uploadConfig(c *gin.Context) { + + c.JSONP(http.StatusOK, gin.H{ + /* 上传图片配置项 */ + "imageActionName": "uploadimage", /* 执行上传图片的action名称 */ + "imageFieldName": "upfile", /* 提交的图片表单名称 */ + "imageMaxSize": 2048000, /* 上传大小限制,单位B */ + "imageAllowFiles": []string{".png", ".jpg", ".jpeg", ".gif", ".bmp"}, /* 上传图片格式显示 */ + "imageCompressEnable": true, /* 是否压缩图片,默认是true */ + "imageCompressBorder": 1600, /* 图片压缩最长边限制 */ + "imageInsertAlign": "none", /* 插入的图片浮动方式 */ + "imageUrlPrefix": "", /* 图片访问路径前缀 */ + "imagePathFormat": "/ueditor/php/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */ + /* {filename} 会替换成原文件名,配置这项需要注意中文乱码问题 */ + /* {rand:6} 会替换成随机数,后面的数字是随机数的位数 */ + /* {time} 会替换成时间戳 */ + /* {yyyy} 会替换成四位年份 */ + /* {yy} 会替换成两位年份 */ + /* {mm} 会替换成两位月份 */ + /* {dd} 会替换成两位日期 */ + /* {hh} 会替换成两位小时 */ + /* {ii} 会替换成两位分钟 */ + /* {ss} 会替换成两位秒 */ + /* 非法字符 \ : * ? " < > | */ + /* 具请体看线上文档: fex.baidu.com/ueditor/#use-format_upload_filename */ + + /* 涂鸦图片上传配置项 */ + "scrawlActionName": "uploadscrawl", /* 执行上传涂鸦的action名称 */ + "scrawlFieldName": "upfile", /* 提交的图片表单名称 */ + "scrawlPathFormat": "/ueditor/php/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */ + "scrawlMaxSize": 2048000, /* 上传大小限制,单位B */ + "scrawlUrlPrefix": "", /* 图片访问路径前缀 */ + "scrawlInsertAlign": "none", + + /* 截图工具上传 */ + "snapscreenActionName": "uploadimage", /* 执行上传截图的action名称 */ + "snapscreenPathFormat": "/ueditor/php/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */ + "snapscreenUrlPrefix": "", /* 图片访问路径前缀 */ + "snapscreenInsertAlign": "none", /* 插入的图片浮动方式 */ + + /* 抓取远程图片配置 */ + "catcherLocalDomain": []string{"127.0.0.1", "localhost", "img.baidu.com"}, + "catcherActionName": "catchimage", /* 执行抓取远程图片的action名称 */ + "catcherFieldName": "source", /* 提交的图片列表表单名称 */ + "catcherPathFormat": "/ueditor/php/upload/image/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */ + "catcherUrlPrefix": "", /* 图片访问路径前缀 */ + "catcherMaxSize": 2048000, /* 上传大小限制,单位B */ + "catcherAllowFiles": []string{".png", ".jpg", ".jpeg", ".gif", ".bmp"}, /* 抓取图片格式显示 */ + + /* 上传视频配置 */ + "videoActionName": "uploadvideo", /* 执行上传视频的action名称 */ + "videoFieldName": "upfile", /* 提交的视频表单名称 */ + "videoPathFormat": "/ueditor/php/upload/video/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */ + "videoUrlPrefix": "", /* 视频访问路径前缀 */ + "videoMaxSize": 102400000, /* 上传大小限制,单位B,默认100MB */ + "videoAllowFiles": []string{ + ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg", + ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid"}, /* 上传视频格式显示 */ + + /* 上传文件配置 */ + "fileActionName": "uploadfile", /* controller里,执行上传视频的action名称 */ + "fileFieldName": "upfile", /* 提交的文件表单名称 */ + "filePathFormat": "/ueditor/php/upload/file/{yyyy}{mm}{dd}/{time}{rand:6}", /* 上传保存路径,可以自定义保存路径和文件名格式 */ + "fileUrlPrefix": "", /* 文件访问路径前缀 */ + "fileMaxSize": 51200000, /* 上传大小限制,单位B,默认50MB */ + "fileAllowFiles": []string{ + ".png", ".jpg", ".jpeg", ".gif", ".bmp", + ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg", + ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid", + ".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso", + ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml", + }, /* 上传文件格式显示 */ + + /* 列出指定目录下的图片 */ + "imageManagerActionName": "listimage", /* 执行图片管理的action名称 */ + "imageManagerListPath": "/ueditor/php/upload/image/", /* 指定要列出图片的目录 */ + "imageManagerListSize": 20, /* 每次列出文件数量 */ + "imageManagerUrlPrefix": "", /* 图片访问路径前缀 */ + "imageManagerInsertAlign": "none", /* 插入的图片浮动方式 */ + "imageManagerAllowFiles": []string{".png", ".jpg", ".jpeg", ".gif", ".bmp"}, /* 列出的文件类型 */ + + /* 列出指定目录下的文件 */ + "fileManagerActionName": "listfile", /* 执行文件管理的action名称 */ + "fileManagerListPath": "/ueditor/php/upload/file/", /* 指定要列出文件的目录 */ + "fileManagerUrlPrefix": "", /* 文件访问路径前缀 */ + "fileManagerListSize": 20, /* 每次列出文件数量 */ + "fileManagerAllowFiles": []string{ + ".png", ".jpg", ".jpeg", ".gif", ".bmp", + ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg", + ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid", + ".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso", + ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml", + }, /* 列出的文件类型 */ + }) + +} +func UploadUeditor(c *gin.Context) { + action := c.Query("action") + + switch action { + case "config": + uploadConfig(c) + } +} + +func Image(c *gin.Context) { + name := c.DefaultQuery("name", "file") + file, err := c.FormFile(name) + if err != nil { + logm.Get().Info("upload file not found") + c.JSON(http.StatusOK, gin.H{ + "error": errors.New("code: upload file not found"), + }) + return + } + dst := toolm.StringDefault(confm.Get().String("app.uploadPath"), "./") + dst += "contents/" + if err := os.MkdirAll(dst, 0755); err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + f, err := file.Open() + if err != nil { + logm.Get().Info("upload file open failed") + c.JSON(http.StatusOK, gin.H{ + "error": errors.New("code: upload file open failed"), + }) + return + } + defer f.Close() + h := md5.New() + if _, err := io.Copy(h, f); err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + filename := fmt.Sprintf("%x", h.Sum(nil)) + path.Ext(file.Filename) + dst += filename + + if err := c.SaveUploadedFile(file, dst); err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "path": "contents/" + filename, + "url": confm.Get().String("app.resourceUrlBase") + "contents/" + filename, + }) +} diff --git a/api/panel/user.go b/api/panel/user.go new file mode 100644 index 0000000..936674c --- /dev/null +++ b/api/panel/user.go @@ -0,0 +1,175 @@ +package panel + +import ( + "errors" + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt" + "github.com/han-joker/moo-layout/api/moo/confm" + "github.com/han-joker/moo-layout/api/moo/dbm" + "github.com/han-joker/moo-layout/api/moo/logm" + "github.com/han-joker/moo-layout/api/moo/toolm" + "github.com/han-joker/moo-layout/api/panel/models" + "github.com/han-joker/moo-layout/api/tables" + "gorm.io/gorm" + "net/http" + "strconv" + "time" +) + +func UserSignIn(c *gin.Context) { + userSignIn := models.UserSignIn{} + if err := c.ShouldBind(&userSignIn); err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + user := tables.User{} + if err := dbm.Get().Where("username = ?", userSignIn.Username).First(&user).Error; + errors.Is(err, gorm.ErrRecordNotFound) { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + if user.Password != toolm.Sha256HMacString(userSignIn.Password, user.PasswordSalt) { + err := errors.New("password error") + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + signingKey := []byte(confm.Get().String("app.signingKey")) + // Create the Claims + claims := &jwt.StandardClaims{ + Audience: strconv.FormatUint(uint64(user.ID), 10), + ExpiresAt: time.Now().Add(30*24*3600 * time.Second).UnixNano(), + Issuer: confm.Get().String("app.issuer"), + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString(signingKey) + if err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + user.JWTToken = tokenString + if err := dbm.Get().Save(&user).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "user": user, + }) +} + +func UserCheckJwtToken(c *gin.Context) { + + user := tables.User{} + if userInterface, exists := c.Get("user"); !exists { + err := errors.New("user not found") + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } else { + user = userInterface.(tables.User) + } + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "user": user, + }) +} + +func UserSignOut(c *gin.Context) { + + user := tables.User{} + if userInterface, exists := c.Get("user"); !exists { + err := errors.New("user not found") + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } else { + user = userInterface.(tables.User) + } + + user.JWTToken = "" + dbm.Get().Save(&user) + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "user": user, + }) +} + +func UserChangePassword(c *gin.Context) { + req := models.UserChangePassword{} + if err := c.ShouldBind(&req); err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + if req.NewPassword != req.NewPasswordConfirm { + err := errors.New("password not equal error") + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + user := tables.User{} + if userInterface, exists := c.Get("user"); !exists { + err := errors.New("user not found") + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } else { + user = userInterface.(tables.User) + } + + if user.Password != toolm.Sha256HMacString(req.Password, user.PasswordSalt) { + err := errors.New("password error") + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + user.Password = toolm.Sha256HMacString(req.NewPassword, user.PasswordSalt) + if err := dbm.Get().Save(&user).Error; err != nil { + logm.Get().Info(err.Error()) + c.JSON(http.StatusOK, gin.H{ + "error": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "error": nil, + "user": user, + }) +} diff --git a/api/tables/category.go b/api/tables/category.go new file mode 100644 index 0000000..05a3c76 --- /dev/null +++ b/api/tables/category.go @@ -0,0 +1,18 @@ +package tables + +const ( + CategoryStatusPublish = iota + 1 // 发布 + CategoryStatusDraft // 草稿 +) + +type Category struct { + Model + + Name string `gorm:"uniqueIndex" json:"name"` + Description string `gorm:"" json:"description"` + ParentID uint `gorm:"index" json:"parent_id"` + Status int `gorm:"" json:"status"` + Sorter int `gorm:"index" json:"sorter"` + + Contents []Content `json:"contents"` +} diff --git a/api/tables/content.go b/api/tables/content.go new file mode 100644 index 0000000..f4a8c52 --- /dev/null +++ b/api/tables/content.go @@ -0,0 +1,34 @@ +package tables + +import ( + "time" +) + +const ( + ContentStatusPublish = 1 + iota + ContentStatusDraft + +) + +const ( + ContentPromoteLatest = 1 << iota + ContentPromoteIndexImage +) + +type Content struct { + Model + + Subject string `gorm:"" json:"subject"` + Content string `gorm:"" json:"content"` + Cover string `gorm:"" json:"cover"` + Referer string `gorm:"" json:"referer"` + PublishTime time.Time `gorm:"" json:"publish_time"` + Status int `gorm:"" json:"status"` // 1 草稿,2 发布 + UserID uint `gorm:"index" json:"user_id"` + CategoryID uint `gorm:"index" json:"categoryId"` + Sorter int `gorm:"index" json:"sorter"` + Promotes int `gorm:"" json:"promotes"` + + User User `json:"user"` + Category Category `json:"category"` +} diff --git a/api/tables/model.go b/api/tables/model.go new file mode 100644 index 0000000..154f26c --- /dev/null +++ b/api/tables/model.go @@ -0,0 +1,13 @@ +package tables + +import ( + "gorm.io/gorm" + "time" +) + +type Model struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` +} diff --git a/api/tables/role.go b/api/tables/role.go new file mode 100644 index 0000000..7f2ffc5 --- /dev/null +++ b/api/tables/role.go @@ -0,0 +1,14 @@ +package tables + +import ( + "gorm.io/gorm" +) + +type Role struct { + gorm.Model + + Name string `gorm:"uniqueIndex;" json:"name"` + Key string `gorm:"uniqueIndex;" json:"key"` + + Users []User `gorm:"many2many:role_user"` +} diff --git a/api/tables/user.go b/api/tables/user.go new file mode 100644 index 0000000..e710fed --- /dev/null +++ b/api/tables/user.go @@ -0,0 +1,15 @@ +package tables + +type User struct { + Model + + Username string `gorm:"uniqueIndex;" json:"username"` + Password string `gorm:"" json:"-"` + PasswordSalt string `gorm:"" json:"-"` + JWTToken string `gorm:"" json:"jwtToken"` + Name string `gorm:"uniqueIndex;" json:"name"` + Email string `gorm:"uniqueIndex;" json:"email"` + Telephone string `gorm:"uniqueIndex;" json:"telephone"` + + Roles []Role `gorm:"many2many:role_user;" json:"roles"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..785ece4 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/han-joker/moo-layout + +go 1.16 + +require ( + github.com/gin-contrib/cors v1.3.1 + github.com/gin-gonic/gin v1.7.4 + github.com/golang-jwt/jwt v3.2.2+incompatible + github.com/gorilla/websocket v1.4.2 + github.com/sirupsen/logrus v1.8.1 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b + gorm.io/driver/sqlite v1.1.4 + gorm.io/gorm v1.21.14 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..20eaf6f --- /dev/null +++ b/go.sum @@ -0,0 +1,94 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/cors v1.3.1 h1:doAsuITavI4IOcd0Y19U4B+O0dNWihRyX//nn4sEmgA= +github.com/gin-contrib/cors v1.3.1/go.mod h1:jjEJ4268OPZUcU7k9Pm653S7lXUGcqMADzFA61xsmDk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM= +github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= +github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ= +github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= +gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= +gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.21.14 h1:NAR9A/3SoyiPVHouW/rlpMUZvuQZ6Z6UYGz+2tosSQo= +gorm.io/gorm v1.21.14/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..6c33ce7 --- /dev/null +++ b/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "github.com/han-joker/moo-layout/api/moo" +) + +func main() { + //启动 websocket服务 + moo.Server("http").Start() +} diff --git a/migrate.go b/migrate.go new file mode 100644 index 0000000..735c249 --- /dev/null +++ b/migrate.go @@ -0,0 +1,102 @@ +package main + +import ( + "github.com/han-joker/moo-layout/api/moo/confm" + "github.com/han-joker/moo-layout/api/moo/toolm" + "github.com/han-joker/moo-layout/api/tables" + "log" + "time" +) + +import ( + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +func main() { + // connect + db, err := gorm.Open(sqlite.Open(confm.Get().String("sqlite.DSN")), &gorm.Config{}) + if err != nil { + log.Fatal(err) + } + + // all tables + tbs := []interface{}{ + &tables.User{}, + &tables.Role{}, + &tables.Category{}, + &tables.Content{}, + } + + // drop tables + if err := db.Migrator().DropTable(tbs...); err != nil { + log.Fatal(err) + } + + // create tables + if err := db.AutoMigrate(tbs...); err != nil { + log.Fatal(err) + } + + // basic seed + // role + root := tables.Role{Name: "根", Key: "root"} + if result := db.Create(&root); result.Error != nil { + log.Fatal(result.Error) + } + // user + users := []tables.User{ + {Username: "admin", Password: "", PasswordSalt: "", Name: "Admin", Roles: []tables.Role{root}}, + } + for i, _ := range users { + users[i].PasswordSalt = toolm.RandString(6, toolm.CharsAll) + users[i].Password = toolm.Sha256HMacString("admin", users[i].PasswordSalt) + } + if result := db.Create(&users); result.Error != nil { + log.Fatal(result.Error) + } + // category + cats := []tables.Category{ + {Model: tables.Model{ID: 1}, Name: "未分类", Status: tables.CategoryStatusPublish}, + {Model: tables.Model{ID: 2}, Name: "新闻公告", Status: tables.CategoryStatusPublish}, + {Model: tables.Model{ID: 3}, Name: "游戏介绍", Status: tables.CategoryStatusPublish}, + {Model: tables.Model{ID: 4}, Name: "游戏特色", Status: tables.CategoryStatusPublish}, + {Model: tables.Model{ID: 5}, Name: "特定内容", Status: tables.CategoryStatusPublish}, + {Model: tables.Model{ID: 6}, Name: "怪物介绍", Status: tables.CategoryStatusPublish, ParentID: 3}, + {Model: tables.Model{ID: 7}, Name: "NPC介绍", Status: tables.CategoryStatusPublish, ParentID: 3}, + {Model: tables.Model{ID: 8}, Name: "武功介绍", Status: tables.CategoryStatusPublish, ParentID: 3}, + {Model: tables.Model{ID: 9}, Name: "系统介绍", Status: tables.CategoryStatusPublish, ParentID: 3}, + {Model: tables.Model{ID: 10}, Name: "装备预览", Status: tables.CategoryStatusPublish, ParentID: 3}, + {Model: tables.Model{ID: 11}, Name: "武功", Status: tables.CategoryStatusPublish, ParentID: 4}, + {Model: tables.Model{ID: 12}, Name: "离线挂机", Status: tables.CategoryStatusPublish, ParentID: 4}, + {Model: tables.Model{ID: 13}, Name: "境界", Status: tables.CategoryStatusPublish, ParentID: 4}, + {Model: tables.Model{ID: 14}, Name: "副本", Status: tables.CategoryStatusPublish, ParentID: 4}, + } + if result := db.Create(&cats); result.Error != nil { + log.Fatal(result.Error) + } + + contents := []tables.Content{ + {Subject: "家长监护工程", Content: "

家长监护工程(请自行编辑)

", CategoryID: 5, Status: tables.ContentStatusPublish, PublishTime: time.Now()}, + {Subject: "联系我们", Content: "

联系我们(请自行编辑)

", CategoryID: 5, Status: tables.ContentStatusPublish, PublishTime: time.Now()}, + {Subject: "商务合作", Content: "

商务合作(请自行编辑)

", CategoryID: 5, Status: tables.ContentStatusPublish, PublishTime: time.Now()}, + {Subject: "用户纠纷处理", Content: "

用户纠纷处理(请自行编辑)

", CategoryID: 5, Status: tables.ContentStatusPublish, PublishTime: time.Now()}, + {Subject: "用户协议", Content: "

用户协议(请自行编辑)

", CategoryID: 5, Status: tables.ContentStatusPublish, PublishTime: time.Now()}, + {Subject: "QRCode", Content: "

QRCode(请自行编辑)

", Cover: "contents/d45d56cdc40ff9ad6245b6a30b5c10a9.png", CategoryID: 5, Status: tables.ContentStatusPublish, PublishTime: time.Now()}, + {Subject: "App Store 下载", Content: "

App Store 下载(请自行编辑)

", Cover: "contents/6ac066a9023d893deb030d8d04faaf4d.png", CategoryID: 5, Status: tables.ContentStatusPublish, PublishTime: time.Now()}, + {Subject: "安卓下载", Content: "

安卓下载(请自行编辑)

", Cover: "contents/1d6e08823657c551a66bb25693e69678.png", CategoryID: 5, Status: tables.ContentStatusPublish, PublishTime: time.Now()}, + {Subject: "Logo", Content: "

Logo(请自行编辑)

", Cover: "contents/fbc658d2b9c8209a0eaa9b9721e426c2.png", CategoryID: 5, Status: tables.ContentStatusPublish, PublishTime: time.Now()}, + {Subject: "底部文字", Content: "健康游戏忠告:抵制不良游戏,拒绝盗版游戏。注意自我保护,谨防受骗上当。适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活", CategoryID: 5, Status: tables.ContentStatusPublish, PublishTime: time.Now()}, + {Subject: "Video Logo", Content: "

请自行编辑

", Cover: "contents/41af642d078b5cd5049f8eb4ee2472ed.png", CategoryID: 5, Status: tables.ContentStatusPublish, PublishTime: time.Now()}, + {Subject: "维护公告", Content: "

测试内容1

", Cover: "contents/e842edc945d88a4f3fa49a39a22c8920.png", CategoryID: 2, Status: tables.ContentStatusPublish, PublishTime: time.Now(), Promotes: tables.ContentPromoteIndexImage | tables.ContentPromoteLatest}, + {Subject: "攻击类", Content: "

测试内容2

", Cover: "contents/25510c2d7e8c3dcc8c4e17b5787e1765.png", CategoryID: 11, Status: tables.ContentStatusPublish, PublishTime: time.Now(), Promotes: tables.ContentPromoteIndexImage}, + {Subject: "步伐类", Content: "

测试内容3

", Cover: "contents/ca2d7c6650b11683d5af710583c5dbf0.png", CategoryID: 11, Status: tables.ContentStatusPublish, PublishTime: time.Now()}, + {Subject: "境界系统", Content: "

测试内容4

", Cover: "contents/9b17f01564d8a207de42e114aa5c9b2e.png", CategoryID: 13, Status: tables.ContentStatusPublish, PublishTime: time.Now()}, + {Subject: "出入境", Content: "

测试内容5

", Cover: "contents/cac4665a7441c3ceeea3b19bdd3d0ffd.png", CategoryID: 13, Status: tables.ContentStatusPublish, PublishTime: time.Now()}, + } + if result := db.Create(&contents); result.Error != nil { + log.Fatal(result.Error) + } + + log.Println("Migrated") +}