commit 866e90b2514589e3f3873f6c343b62e76034a03e Author: xing Date: Fri May 28 18:40:39 2021 +0800 init diff --git a/configs/config.yaml b/configs/config.yaml new file mode 100644 index 0000000..d093da1 --- /dev/null +++ b/configs/config.yaml @@ -0,0 +1,29 @@ +Server: + RunMode: debug + HttpPort: 8080 + ReadTimeout: 60 + WriteTimeout: 60 +App: + DefaultPageSize: 10 + MaxPageSize: 100 + LogSavePath: storage/logs + LogFileName: app + LogFileExt: .log + UploadSavePath: storage/uploads + UploadServerUrl: "http://127.0.0.1:8080/static" + UploadImageMaxSize: 5 # MB + UploadImageAllowExts: + - .jpg + - .jpeg + - .png +Database: + DBType: mysql + Username: xing # 填写你的数据库账号 + Password: xing # 填写你的数据库密码 + Host: 127.0.0.1:3306 + DBName: tt + TablePrefix: blog_ + Charset: utf8mb4 + ParseTime: True + MaxIdleConns: 10 + MaxOpenConns: 30 \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..1362024 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,422 @@ +// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT +// This file was generated by swaggo/swag + +package docs + +import ( + "bytes" + "encoding/json" + "strings" + + "github.com/alecthomas/template" + "github.com/swaggo/swag" +) + +var doc = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{.Description}}", + "title": "{{.Title}}", + "termsOfService": "https://github.com/go-programming-tour-book", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/v1/tags": { + "get": { + "produces": [ + "application/json" + ], + "summary": "获取多个标签", + "parameters": [ + { + "maxLength": 100, + "type": "string", + "description": "标签名称", + "name": "name", + "in": "query" + }, + { + "enum": [ + 0, + 1 + ], + "type": "integer", + "default": 1, + "description": "状态", + "name": "state", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "$ref": "#/definitions/model.TagSwagger" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "summary": "新增标签", + "parameters": [ + { + "maxLength": 100, + "minLength": 3, + "type": "string", + "description": "标签名称", + "name": "name", + "in": "formData", + "required": true + }, + { + "enum": [ + 0, + 1 + ], + "type": "integer", + "default": 1, + "description": "状态", + "name": "state", + "in": "formData" + }, + { + "maxLength": 100, + "minLength": 3, + "type": "string", + "description": "创建者", + "name": "created_by", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "$ref": "#/definitions/model.TagSwagger" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + } + } + } + }, + "/api/v1/tags/{id}": { + "put": { + "produces": [ + "application/json" + ], + "summary": "更新标签", + "parameters": [ + { + "type": "integer", + "description": "标签 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "minLength": 3, + "type": "string", + "description": "标签名称", + "name": "name", + "in": "formData" + }, + { + "enum": [ + 0, + 1 + ], + "type": "integer", + "default": 1, + "description": "状态", + "name": "state", + "in": "formData" + }, + { + "maxLength": 100, + "minLength": 3, + "type": "string", + "description": "修改者", + "name": "modified_by", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.TagSwagger" + } + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "summary": "删除标签", + "parameters": [ + { + "type": "integer", + "description": "标签 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "type": "string" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + } + } + } + }, + "/upload/file": { + "post": { + "produces": [ + "application/json" + ], + "summary": "上传图片", + "parameters": [ + { + "enum": [ + 1, + 2, + 3 + ], + "type": "integer", + "description": "文件类型1图片", + "name": "type", + "in": "formData", + "required": true + }, + { + "type": "file", + "description": "文件", + "name": "state", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "$ref": "#/definitions/service.FileInfo" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + } + } + } + } + }, + "definitions": { + "app.Pager": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total_rows": { + "type": "integer" + } + } + }, + "errorcode.Error": { + "type": "object" + }, + "model.Tag": { + "type": "object", + "properties": { + "created_by": { + "type": "string" + }, + "created_on": { + "type": "integer" + }, + "deleted_on": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "is_del": { + "type": "integer" + }, + "modified_by": { + "type": "string" + }, + "modified_on": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "state": { + "type": "integer" + } + } + }, + "model.TagSwagger": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Tag" + } + }, + "pager": { + "$ref": "#/definitions/app.Pager" + } + } + }, + "service.FileInfo": { + "type": "object", + "properties": { + "accessUrl": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } +}` + +type swaggerInfo struct { + Version string + Host string + BasePath string + Schemes []string + Title string + Description string +} + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = swaggerInfo{ + Version: "1.0", + Host: "", + BasePath: "", + Schemes: []string{}, + Title: "博客系统", + Description: "Go 语言编程之旅:一起用 Go 做项目", +} + +type s struct{} + +func (s *s) ReadDoc() string { + sInfo := SwaggerInfo + sInfo.Description = strings.Replace(sInfo.Description, "\n", "\\n", -1) + + t, err := template.New("swagger_info").Funcs(template.FuncMap{ + "marshal": func(v interface{}) string { + a, _ := json.Marshal(v) + return string(a) + }, + }).Parse(doc) + if err != nil { + return doc + } + + var tpl bytes.Buffer + if err := t.Execute(&tpl, sInfo); err != nil { + return doc + } + + return tpl.String() +} + +func init() { + swag.Register(swag.Name, &s{}) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..993bc8a --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,358 @@ +{ + "swagger": "2.0", + "info": { + "description": "Go 语言编程之旅:一起用 Go 做项目", + "title": "博客系统", + "termsOfService": "https://github.com/go-programming-tour-book", + "contact": {}, + "version": "1.0" + }, + "paths": { + "/api/v1/tags": { + "get": { + "produces": [ + "application/json" + ], + "summary": "获取多个标签", + "parameters": [ + { + "maxLength": 100, + "type": "string", + "description": "标签名称", + "name": "name", + "in": "query" + }, + { + "enum": [ + 0, + 1 + ], + "type": "integer", + "default": 1, + "description": "状态", + "name": "state", + "in": "query" + }, + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 10, + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "$ref": "#/definitions/model.TagSwagger" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + } + } + }, + "post": { + "produces": [ + "application/json" + ], + "summary": "新增标签", + "parameters": [ + { + "maxLength": 100, + "minLength": 3, + "type": "string", + "description": "标签名称", + "name": "name", + "in": "formData", + "required": true + }, + { + "enum": [ + 0, + 1 + ], + "type": "integer", + "default": 1, + "description": "状态", + "name": "state", + "in": "formData" + }, + { + "maxLength": 100, + "minLength": 3, + "type": "string", + "description": "创建者", + "name": "created_by", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "$ref": "#/definitions/model.TagSwagger" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + } + } + } + }, + "/api/v1/tags/{id}": { + "put": { + "produces": [ + "application/json" + ], + "summary": "更新标签", + "parameters": [ + { + "type": "integer", + "description": "标签 ID", + "name": "id", + "in": "path", + "required": true + }, + { + "minLength": 3, + "type": "string", + "description": "标签名称", + "name": "name", + "in": "formData" + }, + { + "enum": [ + 0, + 1 + ], + "type": "integer", + "default": 1, + "description": "状态", + "name": "state", + "in": "formData" + }, + { + "maxLength": 100, + "minLength": 3, + "type": "string", + "description": "修改者", + "name": "modified_by", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/model.TagSwagger" + } + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + } + } + }, + "delete": { + "produces": [ + "application/json" + ], + "summary": "删除标签", + "parameters": [ + { + "type": "integer", + "description": "标签 ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "type": "string" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + } + } + } + }, + "/upload/file": { + "post": { + "produces": [ + "application/json" + ], + "summary": "上传图片", + "parameters": [ + { + "enum": [ + 1, + 2, + 3 + ], + "type": "integer", + "description": "文件类型1图片", + "name": "type", + "in": "formData", + "required": true + }, + { + "type": "file", + "description": "文件", + "name": "state", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "成功", + "schema": { + "$ref": "#/definitions/service.FileInfo" + } + }, + "400": { + "description": "请求错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + }, + "500": { + "description": "内部错误", + "schema": { + "$ref": "#/definitions/errorcode.Error" + } + } + } + } + } + }, + "definitions": { + "app.Pager": { + "type": "object", + "properties": { + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total_rows": { + "type": "integer" + } + } + }, + "errorcode.Error": { + "type": "object" + }, + "model.Tag": { + "type": "object", + "properties": { + "created_by": { + "type": "string" + }, + "created_on": { + "type": "integer" + }, + "deleted_on": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "is_del": { + "type": "integer" + }, + "modified_by": { + "type": "string" + }, + "modified_on": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "state": { + "type": "integer" + } + } + }, + "model.TagSwagger": { + "type": "object", + "properties": { + "list": { + "type": "array", + "items": { + "$ref": "#/definitions/model.Tag" + } + }, + "pager": { + "$ref": "#/definitions/app.Pager" + } + } + }, + "service.FileInfo": { + "type": "object", + "properties": { + "accessUrl": { + "type": "string" + }, + "name": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..9dc2271 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,241 @@ +definitions: + app.Pager: + properties: + page: + type: integer + page_size: + type: integer + total_rows: + type: integer + type: object + errorcode.Error: + type: object + model.Tag: + properties: + created_by: + type: string + created_on: + type: integer + deleted_on: + type: integer + id: + type: integer + is_del: + type: integer + modified_by: + type: string + modified_on: + type: integer + name: + type: string + state: + type: integer + type: object + model.TagSwagger: + properties: + list: + items: + $ref: '#/definitions/model.Tag' + type: array + pager: + $ref: '#/definitions/app.Pager' + type: object + service.FileInfo: + properties: + accessUrl: + type: string + name: + type: string + type: object +info: + contact: { } + description: Go 语言编程之旅:一起用 Go 做项目 + termsOfService: https://github.com/go-programming-tour-book + title: 博客系统 + version: "1.0" +paths: + /api/v1/tags: + get: + parameters: + - description: 标签名称 + in: query + maxLength: 100 + name: name + type: string + - default: 1 + description: 状态 + enum: + - 0 + - 1 + in: query + name: state + type: integer + - default: 1 + description: 页码 + in: query + name: page + type: integer + - default: 10 + description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 成功 + schema: + $ref: '#/definitions/model.TagSwagger' + "400": + description: 请求错误 + schema: + $ref: '#/definitions/errorcode.Error' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/errorcode.Error' + summary: 获取多个标签 + post: + parameters: + - description: 标签名称 + in: formData + maxLength: 100 + minLength: 3 + name: name + required: true + type: string + - default: 1 + description: 状态 + enum: + - 0 + - 1 + in: formData + name: state + type: integer + - description: 创建者 + in: formData + maxLength: 100 + minLength: 3 + name: created_by + required: true + type: string + produces: + - application/json + responses: + "200": + description: 成功 + schema: + $ref: '#/definitions/model.TagSwagger' + "400": + description: 请求错误 + schema: + $ref: '#/definitions/errorcode.Error' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/errorcode.Error' + summary: 新增标签 + /api/v1/tags/{id}: + delete: + parameters: + - description: 标签 ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 成功 + schema: + type: string + "400": + description: 请求错误 + schema: + $ref: '#/definitions/errorcode.Error' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/errorcode.Error' + summary: 删除标签 + put: + parameters: + - description: 标签 ID + in: path + name: id + required: true + type: integer + - description: 标签名称 + in: formData + minLength: 3 + name: name + type: string + - default: 1 + description: 状态 + enum: + - 0 + - 1 + in: formData + name: state + type: integer + - description: 修改者 + in: formData + maxLength: 100 + minLength: 3 + name: modified_by + required: true + type: string + produces: + - application/json + responses: + "200": + description: 成功 + schema: + items: + $ref: '#/definitions/model.TagSwagger' + type: array + "400": + description: 请求错误 + schema: + $ref: '#/definitions/errorcode.Error' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/errorcode.Error' + summary: 更新标签 + /upload/file: + post: + parameters: + - description: 文件类型1图片 + enum: + - 1 + - 2 + - 3 + in: formData + name: type + required: true + type: integer + - description: 文件 + in: formData + name: state + required: true + type: file + produces: + - application/json + responses: + "200": + description: 成功 + schema: + $ref: '#/definitions/service.FileInfo' + "400": + description: 请求错误 + schema: + $ref: '#/definitions/errorcode.Error' + "500": + description: 内部错误 + schema: + $ref: '#/definitions/errorcode.Error' + summary: 上传图片 +swagger: "2.0" diff --git a/global/db.go b/global/db.go new file mode 100644 index 0000000..a6d82dd --- /dev/null +++ b/global/db.go @@ -0,0 +1,7 @@ +package global + +import "github.com/jinzhu/gorm" + +var ( + DBEngine *gorm.DB +) diff --git a/global/setting.go b/global/setting.go new file mode 100644 index 0000000..2cdeb77 --- /dev/null +++ b/global/setting.go @@ -0,0 +1,13 @@ +package global + +import ( + "blog/pkg/logger" + "blog/pkg/setting" +) + +var ( + ServerSetting *setting.ServerSettingS + AppSetting *setting.AppSettingS + DatabaseSetting *setting.DatabaseSettingS + Logger *logger.Logger +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cf34a83 --- /dev/null +++ b/go.mod @@ -0,0 +1,46 @@ +module blog + +go 1.16 + +require ( + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect + github.com/gin-gonic/gin v1.7.2 + github.com/go-openapi/spec v0.20.3 // indirect + github.com/go-openapi/swag v0.19.15 // 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 v9.31.0+incompatible // indirect + github.com/go-playground/validator/v10 v10.6.1 // indirect + github.com/go-sql-driver/mysql v1.6.0 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/jinzhu/gorm v1.9.16 + github.com/json-iterator/go v1.1.11 // indirect + github.com/leodido/go-urn v1.2.1 // indirect + github.com/magiconair/properties v1.8.5 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-isatty v0.0.13 // indirect + github.com/mitchellh/mapstructure v1.4.1 // indirect + github.com/pelletier/go-toml v1.9.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/afero v1.6.0 // indirect + github.com/spf13/cast v1.3.1 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.7.1 + github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 // indirect + github.com/swaggo/gin-swagger v1.3.0 // indirect + github.com/swaggo/swag v1.7.0 // indirect + github.com/ugorji/go v1.2.6 // indirect + github.com/urfave/cli v1.20.0 // indirect + golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect + golang.org/x/net v0.0.0-20210525063256-abc453219eb5 // indirect + golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea // indirect + golang.org/x/text v0.3.6 // indirect + golang.org/x/tools v0.1.2 // indirect + gopkg.in/go-playground/validator.v9 v9.31.0 // indirect + gopkg.in/ini.v1 v1.62.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/internal/dao/dao.go b/internal/dao/dao.go new file mode 100644 index 0000000..50bc02c --- /dev/null +++ b/internal/dao/dao.go @@ -0,0 +1,11 @@ +package dao + +import "github.com/jinzhu/gorm" + +type Dao struct { + engine *gorm.DB +} + +func New(engine *gorm.DB) *Dao { + return &Dao{engine: engine} +} diff --git a/internal/dao/tag.go b/internal/dao/tag.go new file mode 100644 index 0000000..60f1579 --- /dev/null +++ b/internal/dao/tag.go @@ -0,0 +1,47 @@ +package dao + +import ( + "blog/internal/model" + "blog/pkg/app" +) + +func (d *Dao) CountTag(name string, state uint8) (int, error) { + tag := model.Tag{Name: name, State: state} + return tag.Count(d.engine) +} + +func (d *Dao) GetTagList(name string, state uint8, page, pageSize int) ([]*model.Tag, error) { + tag := model.Tag{Name: name, State: state} + pageOffset := app.GetPageOffset(page, pageSize) + return tag.List(d.engine, pageOffset, pageSize) +} + +func (d *Dao) CreateTag(name string, state uint8, createdBy string) error { + tag := model.Tag{ + Name: name, + State: state, + Model: &model.Model{CreatedBy: createdBy}, + } + + return tag.Create(d.engine) +} + +func (d *Dao) UpdateTag(id uint32, name string, state *uint8, modifiedBy string) error { + tag := model.Tag{ + Model: &model.Model{ID: id}, + } + values := map[string]interface{}{ + "state": *state, + "modified_by": modifiedBy, + } + if name != "" { + values["name"] = name + } + + return tag.Update(d.engine, values) +} + +func (d *Dao) DeleteTag(id uint32) error { + tag := model.Tag{Model: &model.Model{ID: id}} + return tag.Delete(d.engine) +} diff --git a/internal/middleware/translations.go b/internal/middleware/translations.go new file mode 100644 index 0000000..e98f968 --- /dev/null +++ b/internal/middleware/translations.go @@ -0,0 +1,38 @@ +package middleware + +import ( + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "github.com/go-playground/locales/en" + "github.com/go-playground/locales/zh" + "github.com/go-playground/locales/zh_Hant_TW" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + enTranslations "github.com/go-playground/validator/v10/translations/en" + zhTranslations "github.com/go-playground/validator/v10/translations/zh" +) + +func Translations() gin.HandlerFunc { + return func(c *gin.Context) { + uni := ut.New(en.New(), zh.New(), zh_Hant_TW.New()) + locale := c.GetHeader("locale") + trans, _ := uni.GetTranslator(locale) + v, ok := binding.Validator.Engine().(*validator.Validate) + if ok { + switch locale { + case "zh": + _ = zhTranslations.RegisterDefaultTranslations(v, trans) + break + case "en": + _ = enTranslations.RegisterDefaultTranslations(v, trans) + break + default: + _ = zhTranslations.RegisterDefaultTranslations(v, trans) + break + } + c.Set("trans", trans) + } + + c.Next() + } +} diff --git a/internal/model/article.go b/internal/model/article.go new file mode 100644 index 0000000..31ec7ca --- /dev/null +++ b/internal/model/article.go @@ -0,0 +1,21 @@ +package model + +import "blog/pkg/app" + +type Article struct { + *Model + Title string `json:"title"` + Desc string `json:"desc"` + Content string `json:"content"` + CoverImageUrl string `json:"cover_image_url"` + State uint8 `json:"state"` +} + +func (a Article) TableName() string { + return "blog_article" +} + +type ArticleSwagger struct { + List []*Article + Pager *app.Pager +} diff --git a/internal/model/article_tag.go b/internal/model/article_tag.go new file mode 100644 index 0000000..c9fd5cd --- /dev/null +++ b/internal/model/article_tag.go @@ -0,0 +1,11 @@ +package model + +type ArticleTag struct { + *Model + TagID uint32 `json:"tag_id"` + ArticleID uint32 `json:"article_id"` +} + +func (a ArticleTag) TableName() string { + return "blog_article_tag" +} diff --git a/internal/model/model.go b/internal/model/model.go new file mode 100644 index 0000000..15ee893 --- /dev/null +++ b/internal/model/model.go @@ -0,0 +1,106 @@ +package model + +import ( + "blog/global" + "blog/pkg/setting" + "fmt" + "github.com/jinzhu/gorm" + "time" +) + +type Model struct { + ID uint32 `gorm:"primary_key" json:"id"` + CreatedBy string `json:"created_by"` + ModifiedBy string `json:"modified_by"` + CreatedOn uint32 `json:"created_on"` + ModifiedOn uint32 `json:"modified_on"` + DeletedOn uint32 `json:"deleted_on"` + IsDel uint8 `json:"is_del"` +} + +func NewDBEngine(databaseSetting *setting.DatabaseSettingS) (*gorm.DB, error) { + db, err := gorm.Open(databaseSetting.DBType, fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=%s&parseTime=%t&loc=Local", + databaseSetting.UserName, + databaseSetting.Password, + databaseSetting.Host, + databaseSetting.DBName, + databaseSetting.Charset, + databaseSetting.ParseTime, + )) + if err != nil { + return nil, err + } + + if global.ServerSetting.RunMode == "debug" { + db.LogMode(true) + } + db.SingularTable(true) + db.DB().SetMaxIdleConns(databaseSetting.MaxIdleConns) + db.DB().SetMaxOpenConns(databaseSetting.MaxOpenConns) + db.Callback().Create().Replace("gorm:update_time_stamp", updateTimeStampForCreateCallback) + db.Callback().Update().Replace("gorm:update_time_stamp", updateTimeStampForUpdateCallback) + db.Callback().Delete().Replace("gorm:delete", deleteCallback) + return db, nil +} + +func updateTimeStampForCreateCallback(scope *gorm.Scope) { + if !scope.HasError() { + nowTime := time.Now().Unix() + if createTimeField, ok := scope.FieldByName("CreatedOn"); ok { + if createTimeField.IsBlank { + _ = createTimeField.Set(nowTime) + } + } + + if modifyTimeField, ok := scope.FieldByName("ModifiedOn"); ok { + if modifyTimeField.IsBlank { + _ = modifyTimeField.Set(nowTime) + } + } + } +} + +func updateTimeStampForUpdateCallback(scope *gorm.Scope) { + if _, ok := scope.Get("gorm:update_column"); !ok { + _ = scope.SetColumn("ModifiedOn", time.Now().Unix()) + } +} + +func deleteCallback(scope *gorm.Scope) { + if !scope.HasError() { + var extraOption string + if str, ok := scope.Get("gorm:delete_option"); ok { + extraOption = fmt.Sprint(str) + } + + deletedOnField, hasDeletedOnField := scope.FieldByName("DeletedOn") + isDelField, hasIsDelField := scope.FieldByName("IsDel") + if !scope.Search.Unscoped && hasDeletedOnField && hasIsDelField { + now := time.Now().Unix() + scope.Raw(fmt.Sprintf( + "UPDATE %v SET %v=%v,%v=%v%v%v", + scope.QuotedTableName(), + scope.Quote(deletedOnField.DBName), + scope.AddToVars(now), + scope.Quote(isDelField.DBName), + scope.AddToVars(1), + addExtraSpaceIfExist(scope.CombinedConditionSql()), + addExtraSpaceIfExist(extraOption), + )).Exec() + } else { + scope.Raw(fmt.Sprintf( + "DELETE FROM %v%v%v", + scope.QuotedTableName(), + addExtraSpaceIfExist(scope.CombinedConditionSql()), + addExtraSpaceIfExist(extraOption), + )).Exec() + } + } +} + +func addExtraSpaceIfExist(str string) string { + if str != "" { + return " " + str + } + return "" +} diff --git a/internal/model/tag.go b/internal/model/tag.go new file mode 100644 index 0000000..2571b9e --- /dev/null +++ b/internal/model/tag.go @@ -0,0 +1,67 @@ +package model + +import ( + "blog/pkg/app" + "github.com/jinzhu/gorm" +) + +type Tag struct { + *Model + Name string `json:"name"` + State uint8 `json:"state"` +} + +func (t Tag) TableName() string { + return "blog_tag" +} + +type TagSwagger struct { + List []*Tag + Pager *app.Pager +} + +func (t Tag) Count(db *gorm.DB) (int, error) { + var count int + if t.Name != "" { + db = db.Where("name = ?", t.Name) + } + db = db.Where("state = ?", t.State) + if err := db.Model(&t).Where("is_del = ?", 0).Count(&count).Error; err != nil { + return 0, err + } + + return count, nil +} + +func (t Tag) List(db *gorm.DB, pageOffset, pageSize int) ([]*Tag, error) { + var tags []*Tag + var err error + if pageOffset >= 0 && pageSize > 0 { + db = db.Offset(pageOffset).Limit(pageSize) + } + if t.Name != "" { + db = db.Where("name = ?", t.Name) + } + db = db.Where("state = ?", t.State) + if err = db.Where("is_del = ?", 0).Find(&tags).Error; err != nil { + return nil, err + } + + return tags, nil +} + +func (t Tag) Create(db *gorm.DB) error { + return db.Create(&t).Error +} + +func (t Tag) Update(db *gorm.DB, values interface{}) error { + if err := db.Model(t).Where("id = ? AND is_del = ?", t.ID, 0).Updates(values).Error; err != nil { + return err + } + + return nil +} + +func (t Tag) Delete(db *gorm.DB) error { + return db.Where("id = ? AND is_del = ?", t.Model.ID, 0).Delete(&t).Error +} diff --git a/internal/routess/api/upload.go b/internal/routess/api/upload.go new file mode 100644 index 0000000..7c278c9 --- /dev/null +++ b/internal/routess/api/upload.go @@ -0,0 +1,52 @@ +package api + +import ( + "blog/global" + "blog/internal/service" + "blog/pkg/app" + "blog/pkg/convert" + "blog/pkg/errorcode" + "blog/pkg/upload" + "github.com/gin-gonic/gin" +) + +type Upload struct{} + +func NewUpload() Upload { + return Upload{} +} + +// @Summary 上传图片 +// @Produce json +// @Param type formData int true "文件类型1图片" required Enums(1,2,3) +// @Param state formData file true "文件" +// @Success 200 {object} service.FileInfo "成功" +// @Failure 400 {object} errorcode.Error "请求错误" +// @Failure 500 {object} errorcode.Error "内部错误" +// @Router /upload/file [post] +func (u Upload) UploadFile(c *gin.Context) { + response := app.NewResponse(c) + file, fileHeader, err := c.Request.FormFile("file") + if err != nil { + response.ToErrorResponse(errorcode.InvalidParams.WithDetails(err.Error())) + return + } + + fileType := convert.StrTo(c.PostForm("type")).MustInt() + if fileHeader == nil || fileType <= 0 { + response.ToErrorResponse(errorcode.InvalidParams) + return + } + + svc := service.New(c.Request.Context()) + fileInfo, err := svc.UploadFile(upload.FileType(fileType), file, fileHeader) + if err != nil { + global.Logger.Errorf("svc.UploadFile err: %v", err) + response.ToErrorResponse(errorcode.ErrorUploadFileFail.WithDetails(err.Error())) + return + } + + response.ToResponse(gin.H{ + "file_access_url": fileInfo.AccessUrl, + }) +} diff --git a/internal/routess/api/v1/article.go b/internal/routess/api/v1/article.go new file mode 100644 index 0000000..455adaa --- /dev/null +++ b/internal/routess/api/v1/article.go @@ -0,0 +1,21 @@ +package v1 + +import ( + "blog/pkg/app" + "blog/pkg/errorcode" + "github.com/gin-gonic/gin" +) + +type Article struct{} + +func NewArticle() Article { + return Article{} +} + +func (a Article) Get(c *gin.Context) { + app.NewResponse(c).ToErrorResponse(errorcode.ServerError) +} +func (a Article) List(c *gin.Context) {} +func (a Article) Create(c *gin.Context) {} +func (a Article) Update(c *gin.Context) {} +func (a Article) Delete(c *gin.Context) {} diff --git a/internal/routess/api/v1/tag.go b/internal/routess/api/v1/tag.go new file mode 100644 index 0000000..d1bbeb5 --- /dev/null +++ b/internal/routess/api/v1/tag.go @@ -0,0 +1,146 @@ +package v1 + +import ( + "blog/global" + "blog/internal/service" + "blog/pkg/app" + "blog/pkg/convert" + "blog/pkg/errorcode" + "github.com/gin-gonic/gin" +) + +type Tag struct { +} + +func NewTag() Tag { + return Tag{} +} + +func (t Tag) Get(c *gin.Context) {} + +// @Summary 获取多个标签 +// @Produce json +// @Param name query string false "标签名称" maxlength(100) +// @Param state query int false "状态" Enums(0, 1) default(1) +// @Param page query int false "页码" default(1) +// @Param page_size query int false "每页数量" default(10) +// @Success 200 {object} model.TagSwagger "成功" +// @Failure 400 {object} errorcode.Error "请求错误" +// @Failure 500 {object} errorcode.Error "内部错误" +// @Router /api/v1/tags [get] +func (t Tag) List(c *gin.Context) { + param := service.TagListRequest{} + response := app.NewResponse(c) + valid, errs := app.BindAndValid(c, ¶m) + if !valid { + global.Logger.Infof("app.BindAndValid errs: %v", errs) + response.ToErrorResponse(errorcode.InvalidParams.WithDetails(errs.Errors()...)) + return + } + + svc := service.New(c.Request.Context()) + pager := app.Pager{Page: app.GetPage(c), PageSize: app.GetPageSize(c)} + totalRows, err := svc.CountTag(&service.CountTagRequest{Name: param.Name, State: param.State}) + if err != nil { + global.Logger.Errorf("svc.CountTag err: %v", err) + response.ToErrorResponse(errorcode.ErrorCountTagFail) + return + } + + tags, err := svc.GetTagList(¶m, &pager) + if err != nil { + global.Logger.Errorf("svc.GetTagList err: %v", err) + response.ToErrorResponse(errorcode.ErrorGetTagListFail) + return + } + + response.ToResponseList(tags, totalRows) +} + +// @Summary 新增标签 +// @Produce json +// @Param name formData string true "标签名称" minlength(3) maxlength(100) +// @Param state formData int false "状态" Enums(0, 1) default(1) +// @Param created_by formData string true "创建者" minlength(3) maxlength(100) +// @Success 200 {object} model.TagSwagger "成功" +// @Failure 400 {object} errorcode.Error "请求错误" +// @Failure 500 {object} errorcode.Error "内部错误" +// @Router /api/v1/tags [post] +func (t Tag) Create(c *gin.Context) { + param := service.CreateTagRequest{} + response := app.NewResponse(c) + valid, errs := app.BindAndValid(c, ¶m) + if !valid { + global.Logger.Infof("app.BindAndValid errs: %v", errs) + response.ToErrorResponse(errorcode.InvalidParams.WithDetails(errs.Errors()...)) + return + } + + svc := service.New(c.Request.Context()) + err := svc.CreateTag(¶m) + if err != nil { + global.Logger.Errorf("svc.CreateTag err: %v", err) + response.ToErrorResponse(errorcode.ErrorCreateTagFail) + return + } + + response.ToResponse(gin.H{}) + return +} + +// @Summary 更新标签 +// @Produce json +// @Param id path int true "标签 ID" +// @Param name formData string false "标签名称" minlength(3),maxlength(100) +// @Param state formData int false "状态" Enums(0, 1) default(1) +// @Param modified_by formData string true "修改者" minlength(3) maxlength(100) +// @Success 200 {array} model.TagSwagger "成功" +// @Failure 400 {object} errorcode.Error "请求错误" +// @Failure 500 {object} errorcode.Error "内部错误" +// @Router /api/v1/tags/{id} [put] +func (t Tag) Update(c *gin.Context) { + param := service.UpdateTagRequest{ID: convert.StrTo(c.Param("id")).MustUInt32()} + response := app.NewResponse(c) + valid, errs := app.BindAndValid(c, ¶m) + if !valid { + global.Logger.Infof("app.BindAndValid errs: %v", errs) + response.ToErrorResponse(errorcode.InvalidParams.WithDetails(errs.Errors()...)) + } + + svc := service.New(c.Request.Context()) + err := svc.UpdateTag(¶m) + if err != nil { + global.Logger.Errorf("svc.UpdateTag err: %v", err) + response.ToErrorResponse(errorcode.ErrorUpdateTagFail) + return + } + + response.ToResponse(gin.H{}) +} + +// @Summary 删除标签 +// @Produce json +// @Param id path int true "标签 ID" +// @Success 200 {string} string "成功" +// @Failure 400 {object} errorcode.Error "请求错误" +// @Failure 500 {object} errorcode.Error "内部错误" +// @Router /api/v1/tags/{id} [delete] +func (t Tag) Delete(c *gin.Context) { + param := service.DeleteTagRequest{ID: convert.StrTo(c.Param("id")).MustUInt32()} + response := app.NewResponse(c) + valid, errs := app.BindAndValid(c, ¶m) + if !valid { + global.Logger.Infof("app.BindAndValid errs: %v", errs) + response.ToErrorResponse(errorcode.InvalidParams.WithDetails(errs.Errors()...)) + } + + svc := service.New(c.Request.Context()) + err := svc.DeleteTag(¶m) + if err != nil { + global.Logger.Errorf("svc.DeleteTag err: %v", err) + response.ToErrorResponse(errorcode.ErrorDeleteTagFail) + return + } + + response.ToResponse(gin.H{}) +} diff --git a/internal/routess/router.go b/internal/routess/router.go new file mode 100644 index 0000000..e1d945c --- /dev/null +++ b/internal/routess/router.go @@ -0,0 +1,39 @@ +package routess + +import ( + _ "blog/docs" + "blog/internal/middleware" + "blog/internal/routess/api" + v1 "blog/internal/routess/api/v1" + "github.com/gin-gonic/gin" + swaggerFiles "github.com/swaggo/files" + ginSwagger "github.com/swaggo/gin-swagger" +) + +func NewRouter() *gin.Engine { + r := gin.New() + r.Use(gin.Logger()) + r.Use(gin.Recovery()) + r.Use(middleware.Translations()) + article := v1.NewArticle() + tag := v1.NewTag() + upload := api.NewUpload() + r.POST("/upload/file", upload.UploadFile) + r.GET("/doc/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) + apiv1 := r.Group("/api/v1") + { + apiv1.POST("/tags", tag.Create) + apiv1.DELETE("/tags/:id", tag.Delete) + apiv1.PUT("/tags/:id", tag.Update) + apiv1.PATCH("/tags/:id/state", tag.Update) + apiv1.GET("/tags", tag.List) + + apiv1.POST("/articles", article.Create) + apiv1.DELETE("/articles/:id", article.Delete) + apiv1.PUT("/articles/:id", article.Update) + apiv1.PATCH("/articles/:id/state", article.Update) + apiv1.GET("/articles/:id", article.Get) + apiv1.GET("/articles", article.List) + } + return r +} diff --git a/internal/service/service.go b/internal/service/service.go new file mode 100644 index 0000000..b6329a1 --- /dev/null +++ b/internal/service/service.go @@ -0,0 +1,18 @@ +package service + +import ( + "blog/global" + "blog/internal/dao" + "context" +) + +type Service struct { + ctx context.Context + dao *dao.Dao +} + +func New(ctx context.Context) Service { + svc := Service{ctx: ctx} + svc.dao = dao.New(global.DBEngine) + return svc +} diff --git a/internal/service/tag.go b/internal/service/tag.go new file mode 100644 index 0000000..bffd462 --- /dev/null +++ b/internal/service/tag.go @@ -0,0 +1,53 @@ +package service + +import ( + "blog/internal/model" + "blog/pkg/app" +) + +type CountTagRequest struct { + Name string `form:"name" binding:"max=100"` + State uint8 `form:"state,default=1" binding:"oneof=0 1"` +} + +type TagListRequest struct { + Name string `form:"name" binding:"max=100"` + State uint8 `form:"state,default=1" binding:"oneof=0 1"` +} + +type CreateTagRequest struct { + Name string `form:"name" binding:"required,min=3,max=100"` + CreatedBy string `form:"created_by" binding:"required,min=3,max=100"` + State uint8 `form:"state,default=1" binding:"oneof=0 1"` +} + +type UpdateTagRequest struct { + ID uint32 `form:"id" binding:"required,gte=1"` + Name string `form:"name" binding:"min=3,max=100"` + State *uint8 `form:"state" binding:"required,oneof=0 1"` + ModifiedBy string `form:"modified_by" binding:"required,min=3,max=100"` +} + +type DeleteTagRequest struct { + ID uint32 `form:"id" binding:"required,gte=1"` +} + +func (svc *Service) CountTag(param *CountTagRequest) (int, error) { + return svc.dao.CountTag(param.Name, param.State) +} + +func (svc *Service) GetTagList(param *TagListRequest, pager *app.Pager) ([]*model.Tag, error) { + return svc.dao.GetTagList(param.Name, param.State, pager.Page, pager.PageSize) +} + +func (svc *Service) CreateTag(param *CreateTagRequest) error { + return svc.dao.CreateTag(param.Name, param.State, param.CreatedBy) +} + +func (svc *Service) UpdateTag(param *UpdateTagRequest) error { + return svc.dao.UpdateTag(param.ID, param.Name, param.State, param.ModifiedBy) +} + +func (svc *Service) DeleteTag(param *DeleteTagRequest) error { + return svc.dao.DeleteTag(param.ID) +} diff --git a/internal/service/upload.go b/internal/service/upload.go new file mode 100644 index 0000000..be740d7 --- /dev/null +++ b/internal/service/upload.go @@ -0,0 +1,42 @@ +package service + +import ( + "blog/global" + "blog/pkg/upload" + "errors" + "mime/multipart" + "os" +) + +type FileInfo struct { + Name string + AccessUrl string +} + +func (svc *Service) UploadFile(fileType upload.FileType, file multipart.File, fileHeader *multipart.FileHeader) (*FileInfo, error) { + fileName := upload.GetFileName(fileHeader.Filename) + if !upload.CheckContainExt(fileType, fileName) { + return nil, errors.New("file suffix is not supported") + } + if upload.CheckMaxSize(fileType, file) { + return nil, errors.New("exceeded maximum file limit") + } + + uploadSavePath := upload.GetSavePath() + if upload.CheckSavePath(uploadSavePath) { + if err := upload.CreateSavePath(uploadSavePath, os.ModePerm); err != nil { + return nil, errors.New("failed to create save directory") + } + } + if upload.CheckPermission(uploadSavePath) { + return nil, errors.New("insufficient file permissions") + } + + dst := uploadSavePath + "/" + fileName + if err := upload.SaveFile(fileHeader, dst); err != nil { + return nil, err + } + + accessUrl := global.AppSetting.UploadServerUrl + "/" + fileName + return &FileInfo{Name: fileName, AccessUrl: accessUrl}, nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..15d1663 --- /dev/null +++ b/main.go @@ -0,0 +1,90 @@ +package main + +import ( + "blog/global" + "blog/internal/model" + "blog/internal/routess" + "blog/pkg/logger" + "blog/pkg/setting" + "github.com/gin-gonic/gin" + _ "github.com/go-sql-driver/mysql" + "gopkg.in/natefinch/lumberjack.v2" + "log" + "net/http" + "time" +) + +func init() { + err := setupSetting() + if err != nil { + log.Fatalf("init.setupSetting err: %v", err) + } + err = setupDBEngine() + if err != nil { + log.Fatalf("init.setupDBEngine err: %v", err) + } + err = setupLogger() + if err != nil { + log.Fatalf("init.setupLogger err: %v", err) + } +} +func setupDBEngine() error { + var err error + global.DBEngine, err = model.NewDBEngine(global.DatabaseSetting) + if err != nil { + return err + } + return nil +} +func setupLogger() error { + global.Logger = logger.NewLogger(&lumberjack.Logger{ + Filename: global.AppSetting.LogSavePath + "/" + global.AppSetting.LogFileName + global.AppSetting.LogFileExt, + MaxSize: 600, + MaxAge: 10, + LocalTime: true, + }, "", log.LstdFlags).WithCaller(2) + + return nil +} + +func setupSetting() error { + newSetting, err := setting.NewSetting() + if err != nil { + return err + } + err = newSetting.ReadSection("Server", &global.ServerSetting) + if err != nil { + return err + } + err = newSetting.ReadSection("App", &global.AppSetting) + if err != nil { + return err + } + err = newSetting.ReadSection("Database", &global.DatabaseSetting) + if err != nil { + return err + } + + global.ServerSetting.ReadTimeout *= time.Second + global.ServerSetting.WriteTimeout *= time.Second + return nil +} + +// @title 博客系统 +// @version 1.0 +// @description Go 语言编程之旅:一起用 Go 做项目 +// @termsOfService https://github.com/go-programming-tour-book +func main() { + gin.SetMode(global.ServerSetting.RunMode) + global.Logger.Infof("%s: go-programming-tour-book/%s", "eddycjy", "blog-service") + + r := routess.NewRouter() + s := &http.Server{ + Addr: ":8080", + Handler: r, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + s.ListenAndServe() +} diff --git a/storage/logs/app.log b/storage/logs/app.log new file mode 100644 index 0000000..2cfb486 --- /dev/null +++ b/storage/logs/app.log @@ -0,0 +1,50 @@ +2021/05/27 11:05:37 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622084737609101710} +2021/05/27 11:21:12 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622085672929232372} +2021/05/27 11:51:10 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622087470164603650} +2021/05/27 11:51:27 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622087487441027324} +2021/05/27 11:54:41 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622087681328002588} +2021/05/27 11:57:54 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622087874303187563} +2021/05/27 14:07:35 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622095655653485221} +2021/05/27 14:08:02 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622095682368233943} +2021/05/27 14:08:25 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622095705608187304} +2021/05/27 14:37:06 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622097426493197748} +2021/05/27 14:37:14 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State必须是[0 1]中的一个","time":1622097434325394979} +2021/05/27 14:38:00 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State必须是[0 1]中的一个","time":1622097480138488964} +2021/05/27 16:02:37 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622102557922696550} +2021/05/27 16:06:13 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: ","time":1622102773119591365} +2021/05/27 16:17:48 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622103468619951851} +2021/05/27 16:20:57 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622103657062869940} +2021/05/27 16:39:06 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622104746415966590} +2021/05/27 16:46:18 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622105178606120868} +2021/05/27 16:47:02 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622105222324148943} +2021/05/27 19:32:06 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622115126240365402} +2021/05/27 19:38:00 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622115480450697332} +2021/05/27 19:56:13 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622116573249023863} +2021/05/27 19:57:06 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622116626105455018} +2021/05/27 19:59:00 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622116740320292554} +2021/05/27 19:59:17 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622116757447119782} +2021/05/27 19:59:51 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622116791393630451} +2021/05/28 10:18:28 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622168308865618331} +2021/05/28 11:01:23 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622170883605638925} +2021/05/28 11:21:46 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622172106599262267} +2021/05/28 11:31:47 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622172707489849654} +2021/05/28 11:31:50 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622172710615258635} +2021/05/28 11:31:53 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622172713867713030} +2021/05/28 11:33:49 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622172829474375529} +2021/05/28 11:34:18 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622172858829135730} +2021/05/28 11:34:19 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622172859814946252} +2021/05/28 11:35:47 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622172947398620704} +2021/05/28 11:38:50 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622173130414371110} +2021/05/28 11:41:36 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622173296327015991} +2021/05/28 16:28:31 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622190511003684831} +2021/05/28 16:53:48 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622192028460804461} +2021/05/28 17:02:49 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622192569815835955} +2021/05/28 17:04:13 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622192653208795175} +2021/05/28 17:05:45 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622192745007678561} +2021/05/28 17:05:49 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622192749363800854} +2021/05/28 17:06:02 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"app.BindAndValid errs: State为必填字段","time":1622192762407831575} +2021/05/28 17:09:42 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622192982235099983} +2021/05/28 17:10:37 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622193037895200224} +2021/05/28 17:22:05 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622193725365423500} +2021/05/28 18:35:09 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622198109257587951} +2021/05/28 18:36:38 {"callers":["/home/xing/IdeaProjects/blog/main.go: 26 main.init.0"],"level":"info","message":"eddycjy: go-programming-tour-book/blog-service","time":1622198198869750999}