├── .ai └── tpl │ ├── controller.tpl │ ├── cron.tpl │ ├── ddl.tpl │ ├── dml.tpl │ ├── errcode.tpl │ ├── event.tpl │ ├── listener.tpl │ ├── logic.tpl │ ├── middleware.tpl │ ├── model.tpl │ ├── route.tpl │ ├── task.tpl │ └── typing.tpl ├── .air.toml ├── .devcontainer └── devcontainer.json ├── .env ├── .gitignore ├── .windsurfrules ├── Readme.md ├── cmd ├── api │ └── main.go ├── cron │ └── main.go ├── migrate │ └── main.go └── queue │ └── main.go ├── config ├── app.go ├── config.go ├── env.go ├── monitor.go ├── svc.go ├── util.go └── var.go ├── const ├── enum │ ├── order_status.go │ ├── user_status.go │ └── usertype.go └── errcode │ ├── user.go │ └── util.go ├── controller ├── api_controller.go ├── login_controller.go └── user_controller.go ├── cron ├── db_check.go ├── init.go ├── sample.go └── sample_func.go ├── event ├── demo_event.go ├── init.go ├── listener │ ├── demo_a_listener.go │ ├── sample_a_listener.go │ └── sample_b_listener.go └── sample_event.go ├── go.mod ├── go.sum ├── internal ├── component │ ├── db │ │ ├── db.go │ │ ├── db_log.go │ │ └── init.go │ ├── logx │ │ ├── console_writer.go │ │ ├── file_writer.go │ │ ├── init.go │ │ ├── log.go │ │ ├── logger.go │ │ └── tracing_hook.go │ └── redisx │ │ ├── error_hook.go │ │ ├── init.go │ │ └── log_hook.go ├── cronx │ ├── builder.go │ ├── init.go │ └── var.go ├── environment │ ├── init.go │ └── timezone.go ├── errorx │ ├── biz_error.go │ ├── db_error.go │ ├── redis_error.go │ ├── server_error.go │ ├── util.go │ └── var.go ├── etype │ ├── enum.go │ ├── enum_map.go │ ├── num_enum.go │ └── util.go ├── eventbus │ ├── event.go │ ├── listener.go │ ├── manager.go │ └── util.go ├── file │ └── file.go ├── g │ └── g.go ├── httpc │ ├── client.go │ ├── hook.go │ ├── httpc.go │ ├── svc.go │ └── util.go ├── httpx │ ├── context.go │ ├── db_check.go │ ├── debug.go │ ├── engine.go │ ├── handler.go │ ├── method.go │ ├── mode.go │ ├── recover_log.go │ ├── request_log.go │ ├── response.go │ ├── routergroup.go │ ├── traceId.go │ └── validators │ │ ├── default_validator.go │ │ └── init.go ├── migration │ ├── global.go │ ├── interface.go │ ├── manager.go │ ├── migrator.go │ └── model.go ├── queue │ ├── client.go │ ├── config.go │ ├── option.go │ ├── server.go │ ├── task.go │ ├── task_handler.go │ └── util.go ├── token │ └── token.go ├── traceid │ └── traceid.go └── util │ └── util.go ├── logic ├── adduser_logic.go ├── getusers_logic.go ├── index_logic.go ├── login_logic.go └── multiadduser_logic.go ├── middleware ├── after_sample_a.go ├── after_sample_b.go ├── before_sample_a.go ├── before_sample_b.go ├── init.go └── token_check.go ├── migration ├── ddl │ ├── create_users_20240101120000.go │ └── init.go └── dml │ ├── deploy_20240101_120000.go │ └── init.go ├── model └── user_model.go ├── rest ├── login │ ├── init.go │ ├── response.go │ ├── svc.go │ └── svc.impl.go ├── mylogin │ ├── init.go │ ├── response.go │ ├── svc.go │ └── svc.impl.go └── user │ ├── init.go │ ├── response.go │ ├── svc.go │ └── svc.impl.go ├── router ├── api.go ├── common.go ├── demo.go ├── init.go ├── login.go └── user.go ├── task ├── init.go ├── sample.go └── sampleB.go ├── test └── bool_test.go ├── transformer └── user.go ├── typing ├── query │ └── .gitkeep └── user.go └── util ├── array.go ├── bool.go └── jsonx └── json.go /.ai/tpl/controller.tpl: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "go-gin/internal/httpx" 5 | "go-gin/logic" 6 | ) 7 | 8 | type xxxController struct { 9 | } 10 | 11 | var XxxController = &xxxController{} 12 | 13 | func (c *xxxController) TT(ctx *httpx.Context) (any, error) { 14 | return httpx.ShouldBindHandle(ctx, logic.NewTTLogic()) 15 | } -------------------------------------------------------------------------------- /.ai/tpl/cron.tpl: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type XxxJob struct{} 8 | 9 | func (j *XxxJob) Name() string { 10 | return "xxx job" 11 | } 12 | 13 | func (j *XxxJob) Handle(ctx context.Context) error { 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /.ai/tpl/ddl.tpl: -------------------------------------------------------------------------------- 1 | package ddl 2 | 3 | import ( 4 | "go-gin/internal/migration" 5 | ) 6 | 7 | func init() { 8 | migration.RegisterDDL(&CreateXxx年月日时分秒{}) 9 | } 10 | 11 | // CreateXxx年月日时分秒 12 | type CreateXxx年月日时分秒 struct{} 13 | 14 | // Up 执行迁移 15 | func (m *CreateXxx年月日时分秒) Up(migrator *migration.DDLMigrator) error { 16 | return migrator.Exec(sql) 17 | } 18 | -------------------------------------------------------------------------------- /.ai/tpl/dml.tpl: -------------------------------------------------------------------------------- 1 | package dml 2 | 3 | import ( 4 | "go-gin/internal/migration" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | func init() { 10 | migration.RegisterDML(&Deploy年月日时分秒{}) 11 | } 12 | 13 | // Deploy年月日时分秒 14 | type Deploy年月日时分秒 struct{} 15 | 16 | // Handle 执行迁移 17 | func (m *Deploy年月日时分秒) Handle(db *gorm.DB) error { 18 | return db.Exec(sql).Error 19 | } 20 | 21 | // Desc 获取迁移描述 22 | func (m *Deploy年月日时分秒) Desc() string { 23 | return "desc" 24 | } 25 | -------------------------------------------------------------------------------- /.ai/tpl/errcode.tpl: -------------------------------------------------------------------------------- 1 | package errcode 2 | 3 | import ( 4 | "go-gin/internal/errorx" 5 | ) 6 | 7 | var ( 8 | ErrXXX = errorx.New(number, message) 9 | ) 10 | -------------------------------------------------------------------------------- /.ai/tpl/event.tpl: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "go-gin/internal/eventbus" 5 | ) 6 | 7 | var XxxEventName = "event.xxx" 8 | 9 | func NewXxxEvent(payload 参数类型) *eventbus.Event { 10 | return eventbus.NewEvent(XxxEventName, payload) 11 | } 12 | -------------------------------------------------------------------------------- /.ai/tpl/listener.tpl: -------------------------------------------------------------------------------- 1 | package listener 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go-gin/internal/eventbus" 7 | ) 8 | 9 | type XxxBListener struct { 10 | } 11 | 12 | func (l *XxxBListener) Handle(ctx context.Context, e *eventbus.Event) error { 13 | return nil 14 | } 15 | -------------------------------------------------------------------------------- /.ai/tpl/logic.tpl: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go-gin/model" 7 | "go-gin/typing" 8 | ) 9 | 10 | type XxxLogic struct { 11 | model *model.模型名称 12 | } 13 | 14 | func NewXxxLogic() *XxxLogic { 15 | return &XxxLogic{ 16 | model: model.New模型名称(), 17 | } 18 | } 19 | 20 | func (l *XxxLogic) Handle(ctx context.Context, req typing.xxxReq) (resp *typing.xxxResp, err error) { 21 | resp = &typing.xxxResp{ 22 | } 23 | return 24 | } 25 | -------------------------------------------------------------------------------- /.ai/tpl/middleware.tpl: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "go-gin/internal/httpx" 6 | ) 7 | 8 | func Xxx() httpx.HandlerFunc { 9 | return func(ctx *httpx.Context) (any, error) { 10 | return nil, nil 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.ai/tpl/model.tpl: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Xxx struct { 4 | Id int64 5 | Name string `gorm:"column:snake_case" json:"snake_case"` 6 | } 7 | 8 | func (u *Xxx) TableName() string { 9 | return `tableName` 10 | } 11 | 12 | type XxxModel struct { 13 | } 14 | 15 | func NewXxxModel() *XxxModel { 16 | return &XxxModel{} 17 | } -------------------------------------------------------------------------------- /.ai/tpl/route.tpl: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "go-gin/controller" 5 | "go-gin/internal/httpx" 6 | ) 7 | 8 | func RegisterXxxRoutes(r *httpx.RouterGroup) { 9 | } 10 | -------------------------------------------------------------------------------- /.ai/tpl/task.tpl: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go-gin/internal/taskx" 7 | ) 8 | 9 | const TypeXxxTask = "taskName" 10 | 11 | func NewXxxTask(payload string) *queue.Task { 12 | return queue.NewTask(TypeXxxTask, payload) 13 | } 14 | 15 | func NewXxxTaskHandler() *queue.TaskHandler { 16 | return queue.NewTaskHandler(TypeXxxTask, func(ctx context.Context, data []byte) error { 17 | return nil 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /.ai/tpl/typing.tpl: -------------------------------------------------------------------------------- 1 | package typing 2 | 3 | type XxxReq struct { 4 | Name string `form:"name" binding:"required" label:"姓名"` 5 | } 6 | 7 | type XxxResp struct { 8 | Message string `json:"message"` 9 | } -------------------------------------------------------------------------------- /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | args_bin = [] 7 | bin = "./tmp/main" 8 | cmd = "go build -o ./tmp/main ./cmd/api" 9 | delay = 1000 10 | exclude_dir = ["assets", "tmp", "vendor", "testdata"] 11 | exclude_file = [] 12 | exclude_regex = ["_test.go"] 13 | exclude_unchanged = false 14 | follow_symlink = false 15 | full_bin = "" 16 | include_dir = [] 17 | include_ext = ["go", "tpl", "tmpl", "html"] 18 | include_file = [] 19 | kill_delay = "0s" 20 | log = "build-errors.log" 21 | poll = false 22 | poll_interval = 0 23 | post_cmd = [] 24 | pre_cmd = [] 25 | rerun = false 26 | rerun_delay = 500 27 | send_interrupt = false 28 | stop_on_error = true 29 | 30 | [color] 31 | app = "" 32 | build = "yellow" 33 | main = "magenta" 34 | runner = "green" 35 | watcher = "cyan" 36 | 37 | [log] 38 | main_only = false 39 | silent = false 40 | time = false 41 | 42 | [misc] 43 | clean_on_exit = false 44 | 45 | [proxy] 46 | app_port = 0 47 | enabled = false 48 | proxy_port = 0 49 | 50 | [screen] 51 | clear_on_rebuild = true 52 | keep_scroll = true 53 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/go 3 | { 4 | "name": "go-gin_1_23", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "golang:1.23-alpine", 7 | "settings": { 8 | "go.toolsManagement.checkForUpdates": "local", 9 | "go.useLanguageServer": true 10 | }, 11 | 12 | // Features to add to the dev container. More info: https://containers.dev/features. 13 | // "features": {}, 14 | 15 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 16 | // "forwardPorts": [], 17 | 18 | // Use 'postCreateCommand' to run commands after the container is created. 19 | "postCreateCommand": "go install github.com/air-verse/air@latest" 20 | 21 | // Configure tool-specific properties. 22 | // "customizations": {}, 23 | 24 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 25 | // "remoteUser": "root" 26 | } 27 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | app: 2 | name: "hello" 3 | port: ":8088" 4 | mode: "debug" # debug,test,release 5 | timezone: "Asia/Shanghai" 6 | timeformat: "2006-01-02 15:04:05" 7 | 8 | db: 9 | dsn: "root:root@tcp(host.docker.internal:3306)/demo?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai" 10 | max-idle-conn: 64 11 | max-open-conn: 64 12 | log-level: "debug" 13 | 14 | redis: 15 | addr: "host.docker.internal:6379" 16 | username: 17 | password: 18 | db: 0 19 | 20 | monitor: 21 | white_ip_list: 22 | - 127.0.0.1 23 | - 192.168.1.100 24 | 25 | log: 26 | level: "debug" # 可以为debug,info,warn,error中的任意一个 27 | path: "storage/logs/" 28 | 29 | svc: 30 | user_url: "http://localhost:8080" 31 | login_url: "https://scm.ddd.com" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.github 2 | *.exe 3 | /tmp 4 | /vendor 5 | /storage/logs/* 6 | /logs 7 | .env -------------------------------------------------------------------------------- /.windsurfrules: -------------------------------------------------------------------------------- 1 | - 使用中文 2 | - 你是一个golang开发工程师 3 | - 当让你生成/创建/新增错误常量、路由、参数、cron定时任务、中间件、事件、业务逻辑、事件监听器、控制器、异步任务、定时任务、ddl迁移、dml迁移、模型时,你必须完整的阅读对应的模板的所有内容 4 | 5 | ### 模板文件(template files) 6 | - .ai/tpl/errcode.tpl 错误常量模板文件 7 | - .ai/tpl/route.tpl 路由模板文件 8 | - .ai/tpl/typing.tpl 参数模板文件 9 | - .ai/tpl/cron.tpl cron定时任务模板文件 10 | - .ai/tpl/middleware.tpl 中间件模板文件 11 | - .ai/tpl/event.tpl 事件模板文件 12 | - .ai/tpl/logic.tpl 业务逻辑模板文件 13 | - .ai/tpl/listener.tpl 事件监听器模板文件 14 | - .ai/tpl/controller.tpl 控制器模板文件 15 | - .ai/tpl/task.tpl 异步任务模板文件 16 | - .ai/tpl/ddl.tpl ddl迁移模板文件 17 | - .ai/tpl/dml.tpl dml迁移、数据升级模板文件 18 | - .ai/tpl/model.tpl 模型模板文件 19 | 20 | ### 项目结构(project structure) 21 | - cmd/ api服务、cron、task的主入口目录 22 | - config/ 配置文件目录 23 | - const/ 常量目录 24 | - errcode/ 业务错误目录 25 | - controller/ 控制器目录 26 | - cron/ cron定时任务目录 27 | - event/ 事件目录 28 | - listener/ 事件监听器目录 29 | - init.go 事件绑定监听器 30 | - logic/ 业务逻辑目录 31 | - middleware/ 中间件目录 32 | - init.go 注册中间件 33 | - r.Before 注册前置中间件 34 | - r.After 注册后置中间件 35 | - migrations/ 数据库迁移目录 36 | - ddl/ ddl升级目录 37 | - dml/ 历史数据升级目录 38 | - model/ 模型目录 39 | - internal/ 内部功能目录,里面方法不建议修改 40 | - rest/ 第三方接口目录 41 | - router/ 路由目录 42 | - init.go 注册控制器路由 43 | - xxx.go 注册控制器每一个方法的路由 44 | - storage/ 存储静态文件、日志目录 45 | - task/ 异步队列任务目录 46 | - init.go 注册异步任务处理器 47 | - transformer/ 转换器目录,例如model转换成api返回的格式 48 | - typing/ 数据结构目录 49 | - query/ 传递给model层的数据结构 50 | - xxx.go 参数数据结构,一个文件一个控制器模块 51 | - req参数的binding标签用于参数验证,验证规则与`github.com/go-playground/validator`一致 52 | - util/ 工具目录 53 | - jsonx/ json工具 54 | - stringx/ 字符串工具 55 | - timex/ 时间工具 -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # go-gin 2 | 3 | 用gin框架配合golang方面比较优秀的库,搭建的一个项目结构,方便快速开发项目。出结果 4 | 用最少的依赖实现80%项目可以完成的需求 5 | 6 | # 功能特性 7 | - 使用主流轻量的路由框架gin,实现路由 8 | - 引入`github.com/go-playground/validator`实现常见的验证,最重要的是引入了中文的提示,以及可以自定义字段名字 9 | - 引入主流的`gorm`库作为数据库层的操作 10 | - 引入`github.com/redis/go-redis/v9`作为缓存层操作 11 | - 引入`github.com/google/uuid`生成traceid,traceid贯穿于各种日志,以及在响应中返回,并且支持自定义traceid的字段名字 12 | - 引入`github.com/labstack/gommon`实现调试模式下日志打印到console,并且不同的日志级别用不用的颜色进行区分 13 | - 引入`github.com/robfig/cron`实现定时任务,定时任务也引入了traceid 14 | - 使用轻量的日志库`github.com/rs/zerolog`进行记录日志 15 | - 引入`gopkg.in/yaml.v3`解析yaml配置文件到golang变量 16 | - 引入`github.com/go-resty/resty/v2`发起http请求,方便的请求第三方接口 17 | - 引入`github.com/hibiken/asynq`实现异步队列 18 | - 引入`github.com/hibiken/asynqmon` 队列使用情况的漂亮web ui界面 19 | - 引入`gorm.io/gorm`操作数据库 20 | # 亮点,很好的优雅度 21 | - 封装了db error,redis error, db层减少了很多if error,else error的判断 22 | - 封装了gin框架,减少了控制器层error相关if else的判断,使代码更美观,清晰,简洁 23 | - 封装了gin的参数绑定,提供了shouldbindhandle、shouldbindqueryhandle等方法,使controller代码更优雅 24 | - crontab、task、event、数据库迁移migration可以优雅的使用 25 | - 提供了状态相关的枚举结构 26 | - 日志记录traceid,方便排查问题 27 | ### 依赖库如下 28 | ```shell 29 | github.com/gin-gonic/gin v1.9.1 30 | github.com/go-playground/locales v0.14.1 31 | github.com/go-playground/universal-translator v0.18.1 32 | github.com/go-playground/validator/v10 v10.14.0 33 | github.com/go-resty/resty/v2 v2.13.1 34 | github.com/go-sql-driver/mysql v1.8.1 35 | github.com/golang-module/carbon/v2 v2.3.12 36 | github.com/google/uuid v1.6.0 37 | github.com/hibiken/asynq v0.24.1 38 | github.com/hibiken/asynqmon v0.7.2 39 | github.com/labstack/gommon v0.4.2 40 | github.com/redis/go-redis/v9 v9.7.0 41 | github.com/robfig/cron/v3 v3.0.1 42 | github.com/rs/zerolog v1.33.0 43 | golang.org/x/text v0.16.0 44 | gopkg.in/yaml.v3 v3.0.1 45 | gorm.io/driver/mysql v1.5.7 46 | gorm.io/gorm v1.25.10 47 | ``` 48 | 49 | ### 目录结构 50 | 51 | - cmd/ - web服务、cron的主入口目录 52 | - config/ -配置文件目录 53 | - const/ -常量目录 54 | - errcode - 错误结构 55 | - enum - 枚举常量结构 56 | - controller/ - 控制器目录 57 | - internal/ -内部功能目录,里面方法不建议修改 58 | - cron/ - 定时任务目录 59 | - middleware/ -中间件目录 60 | - model/ -数据表结构目录 61 | - logic/ -业务逻辑目录 62 | - typing/ 结构目录,用于定义请求参数、响应的数据结构 63 | - util/ 工具目录,提供常用的辅助函数,一般不包含业务逻辑和状态信息 64 | - event/ 事件目录 65 | - listener/ 事件监听器 66 | - rest/ 请求第三方服务的目录 67 | - task/ 任务队列目录 68 | - router/ 路由目录 69 | ### 功能代码 70 | - 控制器 71 | 72 | 在`controller`目录下面创建控制器,例如`user_controller.go` 73 | ```go 74 | type userController struct { 75 | } 76 | 77 | var UserController = &userController{ 78 | } 79 | 80 | func (c *userController) List(ctx *httpx.Context) (any, error) { 81 | var req types.ListReq 82 | l := logic.NewGetUsersLogic() 83 | return l.Handle(ctx, req) 84 | } 85 | ``` 86 | 然后在`controllers/init.go`文件定义路由即可 87 | ```go 88 | user_router := route.Group("/user") 89 | user_router.GET("/", UserController.Index) 90 | ``` 91 | 控制器直接返回logic层处理后的结果,不需要关心响应格式,减少了不必要的if,else判断,自己封装的gin框架底层会根据error自动判断渲染数据还是error数据 92 | ``` 93 | 封装响应的原因是定义了输出的响应结构,如下,永远返回包含code、data、message、trace_id四个字段的结构,使响应结果结构化 94 | ```shell 95 | { 96 | "code": 0, 97 | "data": { 98 | "data": "add user succcess ddddd=96" 99 | }, 100 | "message": "操作成功", 101 | "trace_id": "dc119c64-d4b9-4af1-9e02-d15fc4ba2e42" 102 | } 103 | ``` 104 | 如果响应结构字段名字不符合你的预期,可以进行自定义 105 | ```go 106 | func main() { 107 | // to do something 108 | httpx.DefaultSuccessCodeValue = 0 // 定义成功的code默认值,默认是0,你也可以改成200 109 | httpx.DefaultSuccessMessageValue = "成功" // 定义成功的message默认值,默认是'操作成功' 110 | httpx.CodeFieldName = "code" // 定义响应结构的code字段名,你也可以改成status 111 | httpx.MessageFieldName="msg"// 定义响应结构的消息字段名 112 | httpx.ResultFieldName = "data"// 定义响应结构的数据字段名 113 | traceid.TraceIdFieldName="request_id" // 定义响应以及日志中traceid的字段名字 114 | } 115 | ``` 116 | 响应结果类似如下 117 | ``` 118 | { 119 | "code": 10001, 120 | "data": null, 121 | "msg": "年龄为必填字段\n", 122 | "request_id": "8ddb97db-be44-4df0-8110-0d38a0cc4657" 123 | } 124 | ``` 125 | - 业务层 126 | 127 | 业务层采用命令模式,一个logic只负责处理一个业务的处理,例如`getusers_logic.go` 128 | ```go 129 | type GetUsersLogic struct { 130 | model *models.UserModel 131 | } 132 | 133 | func NewGetUsersLogic() *GetUsersLogic { 134 | return &GetUsersLogic{ 135 | model: models.NewUserModel(), 136 | } 137 | } 138 | 139 | func (l *GetUsersLogic) Handle(ctx context.Context, req types.ListReq) (resp *types.ListReply, err error) { 140 | var u []models.User 141 | if u, err = l.model.List(ctx); err != nil { 142 | return nil, err 143 | } 144 | 145 | redisx.Client().HSet(ctx, "name", "age", 43) 146 | return &types.ListReply{ 147 | Users: u, 148 | }, nil 149 | 150 | } 151 | 152 | ``` 153 | - 数据库 154 | 155 | 要使用数据库,为了记录traceid,以及防止乱调用,所以系统只定义了一种获取gorm连接的方式,必须先调用`WithContext(ctx)`才能获得gorm资源,如下 156 | ```go 157 | db.WithContext(ctx).Find(&u).Error() 158 | ``` 159 | Error()方法底层转成DBError,便于上层区分,以及响应判断 160 | - redis 161 | 162 | 系统的redis库用的是`go-redis`,没有进行过多的封装,获取redis连接后,使用方法上就跟`go-redis`一样了,调用`Client()`方法获取redis资源对象 163 | ```go 164 | redisx.Client().HSet(ctx, "name", "age", 43) 165 | ``` 166 | - 日志 167 | 168 | 系统提供了debug、info、warn、error四种级别的日志,接口如下 169 | ```go 170 | type Logger interface { 171 | Debug(keyword string, message any) 172 | Debugf(keyword string, format string, message ...any) 173 | 174 | Info(keyword string, message any) 175 | Infof(keyword string, format string, message ...any) 176 | 177 | Warn(keyword string, message any) 178 | Warnf(keyword string, format string, message ...any) 179 | 180 | Error(keyword string, message any) 181 | Errorf(keyword string, format string, message ...any) 182 | } 183 | ``` 184 | 可以通过env文件指定日志存储路径和要记录的日志级别,使用方式如下,第一个参数是用于为要记录的日志起一个有意义的关键字,便于grep日志 185 | ```go 186 | logx.WithContext(ctx).Warn("ShouldBind异常", err) 187 | logx.WithContext(ctx).Warnf("这是日志%s", "我叫张三") 188 | ``` 189 | 最终日志文件中记录的内容如下格式,包含`trace_id` 190 | ``` 191 | {"level":"WARN","keyword":"redis","data":"services/user_service.go:24 execute command:[hset name age 43], error=dial tcp 192.168.65.254:6379: connect: connection refused","time":"2024-06-22 23:24:10","trace_id":"5f8b1ee9-7daf-4269-806a-029ee7c3768f"} 192 | ``` 193 | 另外,常规日志文件的名字是`年-月-日.log`格式,如**2024-05-22.log**。值得注意的是warn、error级别日志会单独拿到`年-月-日-error.log`格式文件,如**2024-05-22-error.log**,这样一方面是便于很好的监控异常,另一方面可以很快的排查异常问题 194 | 195 | >此外,系统还提供记录请求access日志,会记录到env配置的路径下的access文件夹,文件以`年-月-日.log`格式命名,日志内容主要包含请求路径、get参数、请求Method、响应码、耗时、User-Agent几个重要参数,格式如下 196 | ``` 197 | {"level":"INFO","path":"/user/list","method":"GET","ip":"127.0.0.1","cost":"227.238215ms","status":200,"proto":"HTTP/1.1","user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36","time":"2024-06-22 23:28:20","trace_id":"f606d909-2f4c-4455-b4b9-5eea0684c49a"} 198 | ``` 199 | 200 | - 事件 201 | 事件定义在`event/`目录,事件的监听器定义在`event/listener/`目录 202 | 事件参考内容如下,使用`eventbus.NewEvent`方法注册事件 203 | ```golang 204 | package event 205 | 206 | import ( 207 | "go-gin/internal/eventbus" 208 | ) 209 | 210 | var SampleEventName = "event.sample" 211 | 212 | func NewSampleEvent(user string) *eventbus.Event { 213 | return eventbus.NewEvent(SampleEventName, user) 214 | } 215 | ``` 216 | 事件监听器`listener`参考内容如下,只需要实现`Handle`方法 217 | 218 | ```golang 219 | package listener 220 | import ( 221 | "context" 222 | "fmt" 223 | "go-gin/internal/eventbus" 224 | "go-gin/model" 225 | ) 226 | 227 | type DemoAListener struct { 228 | } 229 | 230 | func (l DemoAListener) Handle(ctx context.Context, e *eventbus.Event) error { 231 | user := e.Payload().(*model.User) 232 | fmt.Println(user.Name) 233 | return nil 234 | } 235 | ``` 236 | 在`event/init.go`文件进行事件与监听器的绑定,使用`eventbus.AddListener`方法`进行绑定,一个事件可以有多个监听器,如果前一个监听器返回error,后面的监听器不会执行 237 | ```golang 238 | eventbus.AddListener(SampleEventName, &listener.SampleAListener{}, &listener.SampleBListener{}) 239 | ``` 240 | 在控制器触发事件,触发方式 241 | ```golang 242 | eventbus.Fire(ctx, event.NewSampleEvent("hello 测试")) 243 | // 或者 244 | event.NewSampleEvent("333").Fire(ctx) 245 | ``` 246 | - 定时任务 247 | 248 | 定时任务的入口文件为`cmd/cron/main.go`,具体业务代码在`cron`目录编写。定时任务业务代码可以像api模式一样使用`log`、`db` 249 | 250 | 定义一个job首先要定义一个实现了`cronx.Job`的接口的结构,`cronx.Job`接口如下 251 | ```go 252 | type Job interface { 253 | Handle(ctx context.Context) error // 实现业务逻辑 254 | } 255 | ``` 256 | 例子如下 257 | ```go 258 | type SampleJob struct{} 259 | 260 | func (j *SampleJob) Handle(ctx context.Context) error { 261 | 262 | var u model.User 263 | db.WithContext(ctx).Find(&u) 264 | 265 | return nil 266 | } 267 | ``` 268 | 然后在`cron/init.go`文件定义cron的任务执行频率即可,如下定义`SampleJob`每3s执行一次 269 | ``` 270 | cronx.AddJob("@every 3s", &SampleJob{}) 271 | // cronx.Schedule(&SampleJob{}).EveryMinute() 272 | // cronx.Schedule(&SampleJob{}).EveryFiveMinutes() 273 | ``` 274 | 提供了`EveryFiveMinutes`、`EveryThreeMinutes`、`EveryTenMinutes`等多种优雅的时候方法 275 | 定时任务也支持func方式,只需要提供一个`cronx.JobFunc`类型的函数即可,也就是`func(context.Context) error`形式 276 | 例子如下: 277 | ```go 278 | func SampleFunc(ctx context.Context) error { 279 | fmt.Println("this is a sample function") 280 | return nil 281 | } 282 | ``` 283 | 我只需要像注册结构体方式一样,将func添加的定时任务管理器即可 284 | ```go 285 | cronx.AddFunc("@every 5s", SampleFunc) 286 | cronx.ScheduleFunc(SampleFunc).EveryMinute() 287 | ``` 288 | 定时任务其它执行频率的定义方式可以参考[https://github.com/robfig/cron](https://github.com/robfig/cron) 289 | 290 | - 验证器 291 | 292 | 验证器主要是对`gin`内置的binding进行的扩展 293 | - 支持中文化提示 294 | ```go 295 | type AddUserReq struct { 296 | Name string `form:"name" binding:"required"` 297 | Age int `form:"age" binding:"required"` 298 | Status bool `form:"status"` 299 | Ctime time.Time `form:"ctime"` 300 | } 301 | 302 | // controller 303 | var req typing.AddUserReq 304 | if err := ctx.ShouldBind(&req); err != nil { 305 | logx.WithContext(ctx).Warn("ShouldBind异常", err) 306 | httpx.Error(ctx, err) 307 | return 308 | } 309 | ``` 310 | 如上如果参数不包括name的时候,会提示如下,自动进行了中文化处理 311 | ``` 312 | {"code":10001,"data":null,"message":"Name为必填字段\n年龄为必填字段\n","trace_id":"695517e3-1b68-4845-839d-c0e58d8f3a43"} 313 | - 支持自定义提示语的字段名字 314 | 315 | 使用`label`标签定义字段名字 316 | 317 | ```go 318 | type AddUserReq struct { 319 | Name string `form:"name" binding:"required"` 320 | Age int `form:"age" binding:"required" label:"年龄"` 321 | Status bool `form:"status"` 322 | Ctime time.Time `form:"ctime"` 323 | } 324 | ``` 325 | 如上提示语不再提示`Age为必填字段`,而是提示`年龄为必填字段` 326 | 327 | - 支持非`gin`框架方式使用验证器 328 | 提供了`validators.Validate()`方法进行验证结构字段的值是否合理 329 | ```go 330 | var req = typing.AddUserReq{ 331 | Name: "测试", 332 | } 333 | if err := validators.Validate(&req); err != nil { 334 | httpx.Error(ctx, err) 335 | return 336 | } 337 | ``` 338 | **注意:**`validators.Validate`和`ctx.ShouldBind`验证失败返回的是`BizError`类型错误,错误码是`ErrCodeValidateFailed`,默认值是`10001`,你也可以通过`errorx.ErrCodeValidateFailed = xxx`在main入口修改默认值 339 | - 参数、响应结构 340 | 341 | 定义了可以规范化请求参数、响应结构的目录,使代码更容易维护,结构定义在`typing/`目录,一个模块一个文件名,如`user.go` 342 | 343 | 结构定义如下 344 | ```go 345 | package typing 346 | 347 | import ( 348 | "time" 349 | ) 350 | 351 | type AddUserReq struct { 352 | Name string `form:"name"` 353 | Age int `form:"age"` 354 | Status bool `form:"status"` 355 | Ctime time.Time `form:"ctime"` 356 | } 357 | 358 | type AddUserReply struct { 359 | Message string `json:"message"` 360 | } 361 | 362 | ``` 363 | 使用方式,在`controller`层使用 364 | ```go 365 | var req typing.AddUserReq 366 | if err := ctx.ShouldBind(&req); err != nil { 367 | logx.WithContext(ctx).Warn("ShouldBind异常", err) 368 | httpx.Error(ctx, err) 369 | return 370 | } 371 | 372 | ``` 373 | 其实就是使用了`gin`框架本身提供的shouldbind特性,将参数绑定到结构体,后面逻辑直接可以使用结构体里面的字段进行操作了,参数需要包括那些字段,通过结构体很容易看到,实现了参数的可维护性 374 | ```go 375 | resp := typing.AddUserReply{ 376 | Message: fmt.Sprintf("add user succcess %s=%d", user.Name, user.Id), 377 | } 378 | httpx.Ok(ctx, resp) 379 | ``` 380 | 响应结构体如上,结构体数据响应中转成json渲染到`data`域,这样实现相应的结构化和可维护性,响应结果如下 381 | ``` 382 | {"code":0,"data":{"message":"add user succcess ddddd=125"},"message":"成功","trace_id":"b1a9e4f8-7772-4c3a-bb3d-99a22d6a0ff6"} 383 | ``` 384 | 385 | 386 | - 常量 387 | 388 | 未来系统中可能会存在很多业务常量,这里预先建立了目录,当前内置了一些关于错误的预定义常量,这样在业务逻辑中直接使用即可,不需要到处写相同的错误,另外使错误相关更加集中,方便管理,也提高了可维护性 389 | ```go 390 | var ( 391 | ErrUserNotFound = errorx.New(2001, "用户不存在") 392 | ) 393 | ``` 394 | - 错误类型 395 | 系统内置了两种错误类型`BizError`和`ServerError` 396 | - `ServerError`主要是为了处理no method或者method not allowed以及其他服务上的错误,便于响应返回正确的http状态码和统一一致的响应结构,`errorx`包内置错误常量 397 | ```go 398 | ErrMethodNotAllowed = NewServerError(http.StatusMethodNotAllowed) 399 | ErrNoRoute = NewServerError(http.StatusNotFound) 400 | ErrInternalServerError = NewServerError(http.StatusInternalServerError) 401 | ``` 402 | - `BizError`是我们业务开发中使用更多的错误结构,就是业务中定义的异常错误类型,这种类型返回的http状态码都是200,响应结构的状态码、消息均来源于`BizError`变量中。`BizError`的变量定义方式如下 403 | ```go 404 | errorx.New(20001, "用户不存在") 405 | errorx.NewDefault("用户不存在") // code默认值为ErrCodeDefaultCommon的值,也就是10000 406 | ``` 407 | 注意,新增的业务错误码建议从20000开始,因为`internal`底层可能会定义10000-20000之内的业务错误码,例如校验失败的错误码是`ErrCodeValidateFailed`值为10001,通用错误`ErrCodeDefaultCommon`值为10000 408 | - `error`,error应该是其他错误的超类,如果非上述两种错误,我们统一用`error`捕获,并且返回响应http状态码200,code为默认值`ErrCodeDefaultCommon`,也就是10000 409 | ``` 410 | { 411 | "code": 10000, 412 | "data": null, 413 | "message": "用户不存在", 414 | "trace_id": "dc119c64-d4b9-4af1-9e02-d15fc4ba2e42" 415 | } 416 | ``` 417 | - 枚举常量 418 | - 枚举常量建议定义在`const/enum`目录 419 | - 枚举常量定义 420 | - 定义一个结构体 421 | ```golang 422 | // UserStatus 用户状态 423 | type UserStatus struct { 424 | etype.BaseEnum 425 | } 426 | ``` 427 | - 定义一个prefix常量,需要这一部的原因是多张常量类型的code码会重复,需要一种方式解析到code码正确的desc描述,所以使用这个常量将结构体注册到一个map里面,用于`json.Unmarshal`解码常量正确的描述desc字段,以及gorm保存正确的字段到数据库 428 | ```golang 429 | const PrefixUserStatus etype.PrefixType = "user_status" 430 | ``` 431 | - 要保存正确的值到数据库需要类型实现`sql.Scanner`接口,要解包枚举code正确的描述还要实现`json.Unmarshaler` 432 | ```golang 433 | // Scan 实现 sql.Scanner 接口 434 | func (s *OrderStatus) Scan(value interface{}) error { 435 | return s.BaseEnum.Scan(value, PrefixOrderStatus) 436 | } 437 | 438 | // UnmarshalJSON 实现 json.Unmarshaler 接口 439 | func (s *OrderStatus) UnmarshalJSON(data []byte) error { 440 | return s.BaseEnum.UnmarshalJSON(data, PrefixOrderStatus) 441 | } 442 | ``` 443 | - 定义枚举常量 444 | ```golang 445 | // 定义用户状态常量 446 | var ( 447 | USER_STATUS_NORMAL = NewUserStatus(1, "正常") 448 | USER_STATUS_DISABLED = NewUserStatus(2, "禁用") 449 | USER_STATUS_DELETED = NewUserStatus(3, "已删除") 450 | ) 451 | ``` 452 | - gorm使用枚举常量 453 | ```golang 454 | type User struct { 455 | Id int64 456 | Name string `gorm:"column:name" json:"name"` 457 | Status *enum.UserStatus `gorm:"column:status;default:null" json:"status"` 458 | } 459 | // 添加到数据库 460 | user := model.User{ 461 | Name: req.Name, 462 | Status: enum.STATUS_DELETED, 463 | } 464 | db.WithContext(ctx).Create(&user) 465 | 466 | // 实现了Scanner接口的话,会将status字段解析到结构体的枚举字段Status并自动填充枚举的描述 467 | var u model.User 468 | db.WithContext(ctx).Find(&u,1) 469 | ``` 470 | - json编码,baseEnum结构已经进行了实现,当json.Marshal的时候,会自动将枚举常量code值转成json 471 | ```golang 472 | type ListData struct { 473 | Id int `json:"id"` 474 | Name string `json:"name"` 475 | AgeTips string `json:"age_tips"` 476 | Age int `json:"age"` 477 | Status *enum.UserStatus `json:"status"` 478 | } 479 | // json.Marshal转成json后结果是 480 | [{"id":13,"name":"测试你好","age_tips":"未成年","age":2,"status":2}] 481 | ``` 482 | - 每种枚举常量可以定义自己的从code码转成枚举常量的方法,参考如下: 483 | ```golang 484 | // ParseUserStatus 解析用户状态 485 | func ParseUserStatus(code int) (*UserStatus, error) { 486 | base, err := etype.ParseBaseEnum(PrefixUserStatus, code) 487 | if err != nil { 488 | return nil, err 489 | } 490 | return &UserStatus{BaseEnum: base}, nil 491 | } 492 | ``` 493 | - 请求第三方接口 494 | 接入了`go-resty`库,并做了简单封装,便于开箱即用 495 | 496 | - 原生方式 497 | ``` 498 | resp, err := httpc.POST(ctx, "http://localhost:8080/api/list"). 499 | SetFormData(httpc.M{"username": "aaaa", "age": "55555"}). 500 | Send() 501 | ``` 502 | 如上,主要对go-resty进行了简单封装,封装成了`httpc`库,并提供了`POST`,`GET`常用两种请求方式 503 | - 服务方式 504 | 505 | 如果第三方接口交互较多,可以作为服务进行对接,首先在`main.go`文件配置第三方服务地址,例如 506 | ```go 507 | user.Init("http://localhost:8080") 508 | ``` 509 | 然后在`rest`目录定义服务相关文件主要包括 510 | - `init.go`启动文件 511 | - `response.go`接口返回格式以及解析响应结果 512 | - `svc.go` 定义服务接口、参数以及响应结构,进行明确要求,便于代码的可维护性 513 | - `svc.impl.go` 对svc.go中接口的实现 514 | 定义要上面几个文件之后,便可以在自己的业务文件中发起请求了 515 | ```go 516 | hash := md5.Sum([]byte("abcd")) 517 | pwd := hex.EncodeToString(hash[:]) 518 | resp, err := login.Svc.Login(ctx, &login.LoginReq{Username: "1", Pwd: pwd}) 519 | if err != nil { 520 | httpx.Error(ctx, err) 521 | return 522 | } 523 | ``` 524 | 525 | - 队列 526 | 527 | 队列使用的是比较热门的库`github.com/hibiken/asynq`,本项目稍微进行了一点点儿封装,简化使用,更加结构化,便于代码的维护,弱化了client和server端指定taskname 528 | - 队列server目录为`cmd/task` 529 | - 队列代码维护在`task/`目录 530 | - 将数据写入队列的方式,封装了3个方法 531 | ```go 532 | task.Dispatch(queue.NewSampleTask("测试3333"),3*time.Secord) // 使用task包下的Dispatch方法,并添加延迟时间3s后执行 533 | task.DispatchWithRetry(queue.NewSampleTask("测试3333"),)// 使用task包下的Dispatch方法,并添加延迟时间和失败后的重试次数 534 | task.DispatchNow(queue.NewSampleTask("测试3333")) // 使用task包下的Dispatch方法,立即执行 535 | tasqueuekx.NewSampleTask("测试3333").DispatchNow() // 使用task结构的DispatchNow方法 536 | 537 | task.NewOption().Queue(task.HIGH).TaskID("test").Dispatch(queue.NewSampleBTask("hello")) // 指定发送队列 538 | 539 | ``` 540 | - server端handler处理,首先需要将没一个task的handler维护到server端,在`task/init.go`文件进行添加 541 | ```go 542 | task.Handle(NewSampleTaskHandler()) // Handle是封装的一个方法 543 | ``` 544 | - util方法 545 | - `util.IsTrue` - 标量判断是否为true 546 | - `util.IsFalse` - 标量判断是否为false 547 | - `util.WhenFunc` - 标量判断为true时候执行方法 548 | - `utils.When(condition,trueValue,falseValue)` -- 标量condition为true,返回trueValue,否则返回falseValue 549 | - `jsonx.MarshalToString`和`jsonx.Encode` -- 转成json字符串 550 | - `jsonx.UnmarshalFromString`和`jsonx.Decode` -- json字符转转成对应的golang数据 551 | - 552 | ### 快速启动 553 | 554 | ```shell 555 | 1. git clone git@github.com:fanqingxuan/go-gin.git 556 | 2. cd go-gin && go mod tidy 557 | 3. go run cmd/api/main.go -f .env // api启动方式 558 | 4. go run cmd/cron/main.go -f .env // 定时任务启动方式 559 | 5. go run cmd/queue/main.go -f .env // 队列服务入口 560 | 6. go run cmd/migrate/main.go -f .env // 数据库迁移入口 561 | ``` 562 | -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "go-gin/config" 7 | "go-gin/event" 8 | "go-gin/internal/component/db" 9 | "go-gin/internal/component/logx" 10 | "go-gin/internal/component/redisx" 11 | "go-gin/internal/environment" 12 | "go-gin/internal/errorx" 13 | "go-gin/internal/httpx" 14 | "go-gin/internal/httpx/validators" 15 | "go-gin/internal/queue" 16 | _ "go-gin/internal/util" 17 | "go-gin/middleware" 18 | "go-gin/router" 19 | "go-gin/util" 20 | 21 | "github.com/gin-gonic/gin" 22 | _ "github.com/go-sql-driver/mysql" 23 | "github.com/hibiken/asynqmon" 24 | ) 25 | 26 | var configFile = flag.String("f", "./.env", "the config file") 27 | 28 | func main() { 29 | 30 | flag.Parse() 31 | 32 | config.Init(*configFile) 33 | config.InitGlobalVars() 34 | config.InitEnvironment() 35 | 36 | logx.InitConfig(config.GetLogConf()) 37 | logx.Init() 38 | 39 | db.InitConfig(config.GetDbConf()) 40 | db.Init() 41 | 42 | redisx.InitConfig(config.GetRedisConf()) 43 | redisx.Init() 44 | 45 | queue.Init(config.GetRedisConf()) 46 | defer queue.Close() 47 | 48 | event.Init() 49 | 50 | // 初始化第三方服务地址 51 | config.InitSvc() 52 | 53 | // 初始化http服务 54 | engine := initHttpServer() 55 | 56 | // 挂载监控 57 | mountMonitor(engine) 58 | 59 | // 启动http服务 60 | port := config.GetAppConf().Port 61 | fmt.Printf("Starting server at localhost%s...\n", port) 62 | if err := engine.Run(port); err != nil { 63 | fmt.Printf("Start server error,err=%v", err) 64 | } 65 | } 66 | 67 | // 初始化http服务 68 | func initHttpServer() *httpx.Engine { 69 | if environment.IsDebugMode() { 70 | httpx.SetDebugMode() 71 | } else { 72 | httpx.SetReleaseMode() 73 | } 74 | engine := httpx.Default() 75 | validators.Init() 76 | middleware.Init(engine) 77 | router.Init(engine) 78 | return engine 79 | } 80 | 81 | // 挂载监控 82 | func mountMonitor(engine *httpx.Engine) { 83 | // 挂载队列监控web ui 84 | mon := asynqmon.New(asynqmon.Options{ 85 | RootPath: "/monitor/queue", 86 | RedisConnOpt: queue.RedisClientOpt(config.GetRedisConf()), 87 | }) 88 | 89 | r := engine.Engine.Group("/monitor") 90 | r.Use(func(ctx *gin.Context) { 91 | clientIp := ctx.ClientIP() 92 | // 如果客户端ip不在白名单内,直接返回403 93 | if !util.InArray(clientIp, config.GetMonitorConf().WhiteIpList) { 94 | httpx.Error(httpx.NewContext(ctx), errorx.ErrorForbidden) 95 | ctx.Abort() 96 | return 97 | } 98 | ctx.Next() 99 | }) 100 | r.GET("/queue/*any", gin.WrapH(mon)) 101 | } 102 | -------------------------------------------------------------------------------- /cmd/cron/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "go-gin/config" 6 | "go-gin/cron" 7 | "go-gin/event" 8 | "go-gin/internal/component/db" 9 | "go-gin/internal/component/logx" 10 | "go-gin/internal/component/redisx" 11 | "go-gin/internal/cronx" 12 | ) 13 | 14 | var configFile = flag.String("f", "./.env", "the config file") 15 | 16 | func main() { 17 | 18 | flag.Parse() 19 | 20 | config.Init(*configFile) 21 | config.InitGlobalVars() 22 | config.InitEnvironment() 23 | 24 | logx.InitConfig(config.GetLogConf()) 25 | logx.Init() 26 | 27 | db.InitConfig(config.GetDbConf()) 28 | db.Init() 29 | 30 | redisx.InitConfig(config.GetRedisConf()) 31 | redisx.Init() 32 | 33 | event.Init() 34 | 35 | // 初始化第三方服务地址 36 | config.InitSvc() 37 | 38 | // 定时任务 39 | cronx.New() 40 | cron.Init() 41 | cronx.Run() 42 | 43 | } 44 | -------------------------------------------------------------------------------- /cmd/migrate/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | filex "go-gin/internal/file" 6 | "go-gin/internal/migration" 7 | 8 | "github.com/labstack/gommon/color" 9 | "gorm.io/driver/mysql" 10 | "gorm.io/gorm" 11 | "gorm.io/gorm/logger" 12 | 13 | // 导入所有迁移文件 14 | _ "go-gin/migration/ddl" 15 | _ "go-gin/migration/dml" 16 | ) 17 | 18 | var configFile = flag.String("f", "./.env", "the config file") 19 | 20 | type DBConfig struct { 21 | DSN string `yaml:"dsn"` 22 | } 23 | 24 | type Config struct { 25 | DB DBConfig `yaml:"db"` 26 | } 27 | 28 | func main() { 29 | flag.Parse() 30 | color.Enable() 31 | var c Config 32 | filex.MustLoad(*configFile, &c) 33 | // 数据库连接配置 34 | db, err := gorm.Open(mysql.New(mysql.Config{ 35 | DSN: c.DB.DSN, // DSN data source name 36 | DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持 37 | DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引 38 | DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列 39 | 40 | }), &gorm.Config{ 41 | Logger: logger.Default.LogMode(logger.Silent), 42 | DisableForeignKeyConstraintWhenMigrating: true, 43 | }) 44 | 45 | if err != nil { 46 | color.Printf(color.Red("Database connection failed: %v\n"), err) 47 | return 48 | } 49 | // 设置数据库连接 50 | migration.SetDB(db) 51 | 52 | // 执行迁移 53 | if err := migration.GetManager().Run(); err != nil { 54 | return 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cmd/queue/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "go-gin/config" 7 | "go-gin/internal/component/db" 8 | "go-gin/internal/component/logx" 9 | "go-gin/internal/component/redisx" 10 | "go-gin/internal/queue" 11 | "go-gin/task" 12 | ) 13 | 14 | var configFile = flag.String("f", "./.env", "the config file") 15 | 16 | func main() { 17 | 18 | flag.Parse() 19 | 20 | config.Init(*configFile) 21 | config.InitGlobalVars() 22 | config.InitEnvironment() 23 | 24 | logx.InitConfig(config.GetLogConf()) 25 | logx.Init() 26 | 27 | db.InitConfig(config.GetDbConf()) 28 | db.Init() 29 | 30 | redisx.InitConfig(config.GetRedisConf()) 31 | redisx.Init() 32 | queue.InitServer(config.GetRedisConf()) 33 | task.Init() 34 | if err := queue.Start(); err != nil { 35 | fmt.Printf("could not run server: %v", err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /config/app.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import "go-gin/internal/environment" 4 | 5 | type App struct { 6 | Name string `yaml:"name"` 7 | Port string `yaml:"port"` 8 | Mode environment.Mode `yaml:"mode"` 9 | TimeZone string `yaml:"timezone"` 10 | TimeFormat string `yaml:"timeformat"` 11 | } 12 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "go-gin/internal/component/db" 5 | "go-gin/internal/component/logx" 6 | "go-gin/internal/component/redisx" 7 | filex "go-gin/internal/file" 8 | "sync" 9 | ) 10 | 11 | type Config struct { 12 | App App `yaml:"app"` 13 | Redis redisx.Config `yaml:"redis"` 14 | DB db.Config `yaml:"db"` 15 | Log logx.Config `yaml:"log"` 16 | Svc SvcConfig `yaml:"svc"` 17 | Monitor MonitorConfig `yaml:"monitor"` 18 | } 19 | 20 | var instance *Config 21 | var once sync.Once 22 | 23 | func Init(filename string) { 24 | once.Do(func() { 25 | err := filex.MustLoad(filename, &instance) 26 | if err != nil { 27 | panic(err) 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /config/env.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "go-gin/internal/environment" 5 | 6 | "github.com/golang-module/carbon/v2" 7 | ) 8 | 9 | func InitEnvironment() { 10 | 11 | carbon.SetDefault(carbon.Default{ 12 | Layout: instance.App.TimeFormat, 13 | Timezone: instance.App.TimeZone, 14 | }) 15 | environment.SetEnvMode(instance.App.Mode) 16 | environment.SetTimeZone(instance.App.TimeZone) 17 | } 18 | -------------------------------------------------------------------------------- /config/monitor.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type MonitorConfig struct { 4 | WhiteIpList []string `yaml:"white_ip_list"` 5 | } 6 | -------------------------------------------------------------------------------- /config/svc.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "go-gin/rest/login" 5 | "go-gin/rest/mylogin" 6 | "go-gin/rest/user" 7 | ) 8 | 9 | type SvcConfig struct { 10 | UserSvcUrl string `yaml:"user_url"` 11 | LoginSvcUrl string `yaml:"login_url"` 12 | } 13 | 14 | func InitSvc() { 15 | svcConfig := instance.Svc 16 | // 初始化第三方请求服务 17 | user.Init(svcConfig.UserSvcUrl) 18 | login.Init(svcConfig.LoginSvcUrl) 19 | mylogin.Init(svcConfig.LoginSvcUrl) 20 | } 21 | -------------------------------------------------------------------------------- /config/util.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "go-gin/internal/component/db" 5 | "go-gin/internal/component/logx" 6 | "go-gin/internal/component/redisx" 7 | ) 8 | 9 | func GetAppConf() App { 10 | return instance.App 11 | } 12 | 13 | func GetRedisConf() redisx.Config { 14 | return instance.Redis 15 | } 16 | 17 | func GetLogConf() logx.Config { 18 | return instance.Log 19 | } 20 | 21 | func GetDbConf() db.Config { 22 | return instance.DB 23 | } 24 | 25 | func GetMonitorConf() MonitorConfig { 26 | return instance.Monitor 27 | } 28 | -------------------------------------------------------------------------------- /config/var.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "go-gin/internal/httpx" 5 | "go-gin/internal/traceid" 6 | ) 7 | 8 | func InitGlobalVars() { 9 | httpx.DefaultSuccessCodeValue = 200 10 | httpx.CodeFieldName = "status" 11 | httpx.ResultFieldName = "data" 12 | httpx.MessageFieldName = "msg" 13 | httpx.DefaultSuccessMessageValue = "成功" 14 | traceid.TraceIdFieldName = "requestId" 15 | } 16 | -------------------------------------------------------------------------------- /const/enum/order_status.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | import ( 4 | "go-gin/internal/etype" 5 | ) 6 | 7 | const PrefixOrderStatus etype.PrefixType = "order_status" // 订单状态前缀 8 | 9 | // 定义订单状态常量 10 | var ( 11 | ORDER_STATUS_PENDING = NewOrderStatus(1, "待支付") 12 | ORDER_STATUS_PAID = NewOrderStatus(2, "已支付") 13 | ORDER_STATUS_SHIPPING = NewOrderStatus(3, "配送中") 14 | ORDER_STATUS_COMPLETED = NewOrderStatus(4, "已完成") 15 | ORDER_STATUS_CANCELLED = NewOrderStatus(5, "已取消") 16 | ) 17 | 18 | // OrderStatus 订单状态 19 | type OrderStatus struct { 20 | etype.BaseEnum 21 | } 22 | 23 | // NewOrderStatus 创建订单状态 24 | func NewOrderStatus(code int, desc string) *OrderStatus { 25 | return &OrderStatus{ 26 | BaseEnum: etype.CreateBaseEnumAndSetMap(PrefixOrderStatus, code, desc), 27 | } 28 | } 29 | 30 | // ParseOrderStatus 解析订单状态 31 | func ParseOrderStatus(code int) (*OrderStatus, error) { 32 | base, err := etype.ParseBaseEnum(PrefixOrderStatus, code) 33 | if err != nil { 34 | return nil, err 35 | } 36 | return &OrderStatus{BaseEnum: base}, nil 37 | } 38 | 39 | // Scan 实现 sql.Scanner 接口 40 | func (s *OrderStatus) Scan(value interface{}) error { 41 | return s.BaseEnum.Scan(value, PrefixOrderStatus) 42 | } 43 | 44 | // UnmarshalJSON 实现 json.Unmarshaler 接口 45 | func (s *OrderStatus) UnmarshalJSON(data []byte) error { 46 | return s.BaseEnum.UnmarshalJSON(data, PrefixOrderStatus) 47 | } 48 | -------------------------------------------------------------------------------- /const/enum/user_status.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | import ( 4 | "go-gin/internal/etype" 5 | ) 6 | 7 | const PrefixUserStatus etype.PrefixType = "user_status" 8 | 9 | // 定义用户状态常量 10 | var ( 11 | USER_STATUS_NORMAL = NewUserStatus(1, "正常") 12 | USER_STATUS_DISABLED = NewUserStatus(2, "禁用") 13 | USER_STATUS_DELETED = NewUserStatus(3, "已删除") 14 | ) 15 | 16 | // UserStatus 用户状态 17 | type UserStatus struct { 18 | etype.BaseEnum 19 | } 20 | 21 | // NewUserStatus 创建用户状态 22 | func NewUserStatus(code int, desc string) *UserStatus { 23 | return &UserStatus{ 24 | BaseEnum: etype.CreateBaseEnumAndSetMap(PrefixUserStatus, code, desc), 25 | } 26 | } 27 | 28 | // ParseUserStatus 解析用户状态 29 | func ParseUserStatus(code int) (*UserStatus, error) { 30 | 31 | base, err := etype.ParseBaseEnum(PrefixUserStatus, code) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return &UserStatus{BaseEnum: base}, nil 37 | } 38 | 39 | // Scan 实现 sql.Scanner 接口 40 | func (s *UserStatus) Scan(value interface{}) error { 41 | return s.BaseEnum.Scan(value, PrefixUserStatus) 42 | } 43 | 44 | // UnmarshalJSON 实现 json.Unmarshaler 接口 45 | func (s *UserStatus) UnmarshalJSON(data []byte) error { 46 | return s.BaseEnum.UnmarshalJSON(data, PrefixUserStatus) 47 | 48 | } 49 | -------------------------------------------------------------------------------- /const/enum/usertype.go: -------------------------------------------------------------------------------- 1 | package enum 2 | 3 | import ( 4 | "fmt" 5 | "go-gin/internal/etype" 6 | ) 7 | 8 | // 用户类型 9 | type UserType etype.NumEnum 10 | 11 | var _ etype.INumEnum[UserType] = UserType(0) 12 | 13 | const ( 14 | // 正常添加 15 | UserTypeNormal UserType = 1 16 | // 从第三方导入 17 | UserTypeFromThird = 2 18 | // 供应商用户 19 | UserTypeSupplier = 3 20 | ) 21 | 22 | var userTypeMap = map[UserType]string{ 23 | UserTypeNormal: "正常添加", 24 | UserTypeFromThird: "从第三方导入", 25 | UserTypeSupplier: "供应商用户", 26 | } 27 | 28 | func (b UserType) String() string { 29 | return etype.ParseStringFromMap(b, userTypeMap) 30 | } 31 | 32 | func (b UserType) Equal(other UserType) bool { 33 | return etype.Equal(b, other) 34 | } 35 | 36 | func (b UserType) Format(f fmt.State, r rune) { 37 | etype.Format(f, b) 38 | } 39 | -------------------------------------------------------------------------------- /const/errcode/user.go: -------------------------------------------------------------------------------- 1 | package errcode 2 | 3 | import ( 4 | "go-gin/internal/errorx" 5 | ) 6 | 7 | var ( 8 | // 以下定义业务上的错误,注意1开头的是系统错误 9 | ErrUserNotFound = errorx.New(20001, "用户不存在") 10 | ErrUserNameOrPwdFaild = errorx.New(20002, "用户名或者密码错误") 11 | ErrUserMustLogin = errorx.New(20003, "请先登录") 12 | ErrUserNeedLoginAgain = errorx.New(20004, "token已过期,请重新登录") 13 | ) 14 | -------------------------------------------------------------------------------- /const/errcode/util.go: -------------------------------------------------------------------------------- 1 | package errcode 2 | 3 | import ( 4 | "errors" 5 | "go-gin/internal/errorx" 6 | ) 7 | 8 | func New(code int, msg string) errorx.BizError { 9 | return errorx.BizError{Code: code, Msg: msg} 10 | } 11 | 12 | func NewDefault(msg string) errorx.BizError { 13 | return NewError(errors.New(msg)) 14 | } 15 | 16 | func NewError(err error) errorx.BizError { 17 | return errorx.BizError{Code: errorx.ErrCodeBizDefault, Msg: err.Error()} 18 | } 19 | 20 | func IsRecordNotFound(err error) bool { 21 | return errorx.IsRecordNotFound(err) 22 | } 23 | 24 | func IsError(err error) bool { 25 | return errorx.IsError(err) 26 | } 27 | -------------------------------------------------------------------------------- /controller/api_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "go-gin/internal/httpc" 7 | "go-gin/internal/httpx" 8 | "go-gin/rest/login" 9 | "go-gin/rest/mylogin" 10 | "go-gin/rest/user" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | type apiController struct { 16 | } 17 | 18 | var ApiController = &apiController{} 19 | 20 | func (c *apiController) Index(ctx *httpx.Context) (any, error) { 21 | 22 | return httpc.POST(ctx, "http://localhost:8080/api/list"). 23 | SetFormData(httpc.M{"username": "aaaa", "age": "55555"}). 24 | Send() 25 | 26 | } 27 | 28 | func (c *apiController) IndexA(ctx *httpx.Context) (any, error) { 29 | 30 | return user.Svc.Hello(ctx, &user.HelloReq{UserId: "userId111"}) 31 | 32 | } 33 | 34 | func (c *apiController) IndexB(ctx *httpx.Context) (any, error) { 35 | 36 | hash := md5.Sum([]byte("BRUCEMUWU2023")) 37 | pwd := hex.EncodeToString(hash[:]) 38 | return login.Svc.Login(ctx, &login.LoginReq{Username: "1", Pwd: pwd}) 39 | } 40 | 41 | func (c *apiController) IndexC(ctx *httpx.Context) (any, error) { 42 | hash := md5.Sum([]byte("BRUCEMUWU2")) 43 | pwd := hex.EncodeToString(hash[:]) 44 | return mylogin.Svc.Login(ctx, &mylogin.LoginReq{Username: "1", Pwd: pwd}) 45 | } 46 | 47 | func (c *apiController) List(ctx *httpx.Context) (any, error) { 48 | return gin.H{ 49 | "userId": ctx.PostForm("userId"), 50 | "username": "张三", 51 | "age": 18, 52 | }, nil 53 | } 54 | -------------------------------------------------------------------------------- /controller/login_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "go-gin/internal/httpx" 5 | "go-gin/internal/token" 6 | "go-gin/logic" 7 | ) 8 | 9 | type loginController struct { 10 | } 11 | 12 | var LoginController = &loginController{} 13 | 14 | func (c *loginController) Login(ctx *httpx.Context) (any, error) { 15 | return httpx.ShouldBindHandle(ctx, logic.NewLoginLogic()) 16 | } 17 | 18 | func (c *loginController) LoginOut(ctx *httpx.Context) (any, error) { 19 | token.Flush(ctx, ctx.GetHeader("token")) 20 | return nil, nil 21 | } 22 | -------------------------------------------------------------------------------- /controller/user_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "go-gin/internal/httpx" 6 | "go-gin/logic" 7 | "go-gin/typing" 8 | 9 | "github.com/golang-module/carbon/v2" 10 | ) 11 | 12 | type userController struct { 13 | } 14 | 15 | var UserController = &userController{} 16 | 17 | type User struct { 18 | Name string `json:"name"` 19 | CreateTime carbon.Carbon `json:"create_time"` 20 | } 21 | 22 | func (c *userController) Index(ctx *httpx.Context) (any, error) { 23 | // event.Fire(ctx, events.NewSampleEvent("hello 测试")) 24 | // events.NewSampleEvent("333").Fire(ctx) 25 | // u := User{ 26 | // Name: "hello", 27 | // CreateTime: carbon.Parse("now").AddCentury(), 28 | // } 29 | // return u, nil 30 | fmt.Println("index") 31 | return httpx.ShouldBindHandle(ctx, logic.NewIndexLogic()) 32 | } 33 | 34 | func (c *userController) List(ctx *httpx.Context) (any, error) { 35 | var req typing.ListReq 36 | l := logic.NewGetUsersLogic() 37 | return l.Handle(ctx, req) 38 | } 39 | 40 | func (c *userController) AddUser(ctx *httpx.Context) (any, error) { 41 | return httpx.ShouldBindHandle(ctx, logic.NewAddUserLogic()) 42 | } 43 | 44 | func (c *userController) MultiUserAdd(ctx *httpx.Context) (any, error) { 45 | return httpx.ShouldBindHandle(ctx, logic.NewMultiAddUserLogic()) 46 | } 47 | -------------------------------------------------------------------------------- /cron/db_check.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "context" 5 | "go-gin/internal/component/db" 6 | ) 7 | 8 | type DBCheckJob struct{} 9 | 10 | func (j *DBCheckJob) Handle(ctx context.Context) error { 11 | if db.IsConnected() { 12 | return nil 13 | } 14 | return db.Connect() 15 | } 16 | -------------------------------------------------------------------------------- /cron/init.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import "go-gin/internal/cronx" 4 | 5 | func Init() { 6 | 7 | cronx.AddJob("@every 3s", &DBCheckJob{}) 8 | // cronx.AddJob("@every 3s", &SampleJob{}) 9 | cronx.Schedule(&SampleJob{}).EveryMinute() 10 | cronx.AddFunc("@every 5s", SampleFunc) 11 | // cronx.ScheduleFunc(SampleFunc).EveryMinute() 12 | } 13 | -------------------------------------------------------------------------------- /cron/sample.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | type SampleJob struct{} 8 | 9 | func (j *SampleJob) Handle(ctx context.Context) error { 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /cron/sample_func.go: -------------------------------------------------------------------------------- 1 | package cron 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | func SampleFunc(ctx context.Context) error { 9 | fmt.Println("this is a sample function") 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /event/demo_event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "go-gin/internal/eventbus" 5 | "go-gin/model" 6 | ) 7 | 8 | var DemoEventName = "event.demo" 9 | 10 | func NewDemoEvent(u *model.User) *eventbus.Event { 11 | return eventbus.NewEvent(DemoEventName, u) 12 | } 13 | -------------------------------------------------------------------------------- /event/init.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "go-gin/event/listener" 5 | "go-gin/internal/eventbus" 6 | ) 7 | 8 | func Init() { 9 | eventbus.AddListener(SampleEventName, &listener.SampleAListener{}, &listener.SampleBListener{}) 10 | eventbus.AddListener(DemoEventName, &listener.DemoAListener{}) 11 | } 12 | -------------------------------------------------------------------------------- /event/listener/demo_a_listener.go: -------------------------------------------------------------------------------- 1 | package listener 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go-gin/internal/eventbus" 7 | "go-gin/model" 8 | ) 9 | 10 | type DemoAListener struct { 11 | } 12 | 13 | func (l DemoAListener) Handle(ctx context.Context, e *eventbus.Event) error { 14 | user := e.Payload().(*model.User) 15 | fmt.Println(user.Name) 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /event/listener/sample_a_listener.go: -------------------------------------------------------------------------------- 1 | package listener 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go-gin/internal/eventbus" 7 | ) 8 | 9 | type SampleAListener struct { 10 | } 11 | 12 | func (l SampleAListener) Handle(ctx context.Context, e *eventbus.Event) error { 13 | fmt.Println("SampleAListener") 14 | fmt.Println(e.Payload().(string)) 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /event/listener/sample_b_listener.go: -------------------------------------------------------------------------------- 1 | package listener 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go-gin/internal/eventbus" 7 | ) 8 | 9 | type SampleBListener struct { 10 | } 11 | 12 | func (l *SampleBListener) Handle(ctx context.Context, e *eventbus.Event) error { 13 | fmt.Println("SampleBListener") 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /event/sample_event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "go-gin/internal/eventbus" 5 | ) 6 | 7 | var SampleEventName = "event.sample" 8 | 9 | func NewSampleEvent(user string) *eventbus.Event { 10 | return eventbus.NewEvent(SampleEventName, user) 11 | } 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go-gin 2 | 3 | go 1.21.4 4 | 5 | toolchain go1.21.5 6 | 7 | require ( 8 | github.com/gin-gonic/gin v1.9.1 9 | github.com/go-playground/locales v0.14.1 10 | github.com/go-playground/universal-translator v0.18.1 11 | github.com/go-playground/validator/v10 v10.14.0 12 | github.com/go-resty/resty/v2 v2.13.1 13 | github.com/go-sql-driver/mysql v1.8.1 14 | github.com/golang-module/carbon/v2 v2.3.12 15 | github.com/google/uuid v1.6.0 16 | github.com/hibiken/asynq v0.24.1 17 | github.com/hibiken/asynqmon v0.7.2 18 | github.com/labstack/gommon v0.4.2 19 | github.com/redis/go-redis/v9 v9.7.0 20 | github.com/robfig/cron/v3 v3.0.1 21 | github.com/rs/zerolog v1.33.0 22 | github.com/stretchr/testify v1.10.0 23 | golang.org/x/text v0.16.0 24 | gopkg.in/yaml.v3 v3.0.1 25 | gorm.io/driver/mysql v1.5.7 26 | gorm.io/gorm v1.25.10 27 | 28 | ) 29 | 30 | require ( 31 | filippo.io/edwards25519 v1.1.0 // indirect 32 | github.com/bytedance/sonic v1.9.1 // indirect 33 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 34 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 35 | github.com/davecgh/go-spew v1.1.1 // indirect 36 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 37 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 38 | github.com/gin-contrib/sse v0.1.0 // indirect 39 | github.com/goccy/go-json v0.10.2 // indirect 40 | github.com/golang/protobuf v1.5.4 // indirect 41 | github.com/google/go-cmp v0.6.0 // indirect 42 | github.com/gorilla/mux v1.8.0 // indirect 43 | github.com/jinzhu/inflection v1.0.0 // indirect 44 | github.com/jinzhu/now v1.1.5 // indirect 45 | github.com/json-iterator/go v1.1.12 // indirect 46 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 47 | github.com/leodido/go-urn v1.2.4 // indirect 48 | github.com/mattn/go-colorable v0.1.13 // indirect 49 | github.com/mattn/go-isatty v0.0.20 // indirect 50 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 51 | github.com/modern-go/reflect2 v1.0.2 // indirect 52 | github.com/pelletier/go-toml/v2 v2.1.1 // indirect 53 | github.com/pmezard/go-difflib v1.0.0 // indirect 54 | github.com/rogpeppe/go-internal v1.10.0 // indirect 55 | github.com/spf13/cast v1.7.0 // indirect 56 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 57 | github.com/ugorji/go/codec v1.2.11 // indirect 58 | golang.org/x/arch v0.3.0 // indirect 59 | golang.org/x/crypto v0.23.0 // indirect 60 | golang.org/x/net v0.25.0 // indirect 61 | golang.org/x/sys v0.26.0 // indirect 62 | golang.org/x/time v0.7.0 // indirect 63 | google.golang.org/protobuf v1.35.1 // indirect 64 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 65 | ) 66 | -------------------------------------------------------------------------------- /internal/component/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "go-gin/internal/errorx" 5 | 6 | "gorm.io/gorm" 7 | "gorm.io/gorm/clause" 8 | ) 9 | 10 | type DB struct { 11 | *gorm.DB 12 | } 13 | 14 | // 查询方法 15 | func (db *DB) Where(query any, args ...any) *DB { 16 | return &DB{db.DB.Where(query, args...)} 17 | } 18 | 19 | func (db *DB) Select(query any, args ...any) *DB { 20 | return &DB{db.DB.Select(query, args...)} 21 | } 22 | 23 | func (db *DB) First(dest any, conds ...any) *DB { 24 | return &DB{db.DB.First(dest, conds...)} 25 | } 26 | 27 | func (db *DB) Last(dest any, conds ...any) *DB { 28 | return &DB{db.DB.Last(dest, conds...)} 29 | } 30 | 31 | func (db *DB) Find(dest any, conds ...any) *DB { 32 | return &DB{db.DB.Find(dest, conds...)} 33 | } 34 | 35 | func (db *DB) Take(dest any, conds ...any) *DB { 36 | return &DB{db.DB.Take(dest, conds...)} 37 | } 38 | 39 | func (db *DB) Scan(dest any) *DB { 40 | return &DB{db.DB.Scan(dest)} 41 | } 42 | 43 | // 创建方法 44 | func (db *DB) Create(value any) *DB { 45 | return &DB{db.DB.Create(value)} 46 | } 47 | 48 | func (db *DB) CreateInBatches(value any, batchSize int) *DB { 49 | return &DB{db.DB.CreateInBatches(value, batchSize)} 50 | } 51 | 52 | // 更新方法 53 | func (db *DB) Save(value any) *DB { 54 | return &DB{db.DB.Save(value)} 55 | } 56 | 57 | func (db *DB) Updates(values any) *DB { 58 | return &DB{db.DB.Updates(values)} 59 | } 60 | 61 | func (db *DB) Update(column string, value any) *DB { 62 | return &DB{db.DB.Update(column, value)} 63 | } 64 | 65 | // 删除方法 66 | func (db *DB) Delete(value any, conds ...any) *DB { 67 | return &DB{db.DB.Delete(value, conds...)} 68 | } 69 | 70 | // 条件构造方法 71 | func (db *DB) Or(query any, args ...any) *DB { 72 | return &DB{db.DB.Or(query, args...)} 73 | } 74 | 75 | func (db *DB) Not(query any, args ...any) *DB { 76 | return &DB{db.DB.Not(query, args...)} 77 | } 78 | 79 | func (db *DB) Distinct(args ...any) *DB { 80 | return &DB{db.DB.Distinct(args...)} 81 | } 82 | 83 | func (db *DB) Omit(columns ...string) *DB { 84 | return &DB{db.DB.Omit(columns...)} 85 | } 86 | 87 | // 分页和排序 88 | func (db *DB) Limit(limit int) *DB { 89 | return &DB{db.DB.Limit(limit)} 90 | } 91 | 92 | func (db *DB) Offset(offset int) *DB { 93 | return &DB{db.DB.Offset(offset)} 94 | } 95 | 96 | func (db *DB) Order(value any) *DB { 97 | return &DB{db.DB.Order(value)} 98 | } 99 | 100 | func (db *DB) Group(name string) *DB { 101 | return &DB{db.DB.Group(name)} 102 | } 103 | 104 | func (db *DB) Having(query any, args ...any) *DB { 105 | return &DB{db.DB.Having(query, args...)} 106 | } 107 | 108 | // 关联查询 109 | func (db *DB) Joins(query string, args ...any) *DB { 110 | return &DB{db.DB.Joins(query, args...)} 111 | } 112 | 113 | func (db *DB) Preload(query string, args ...any) *DB { 114 | return &DB{db.DB.Preload(query, args...)} 115 | } 116 | 117 | // 事务相关 118 | func (db *DB) Begin() *DB { 119 | return &DB{db.DB.Begin()} 120 | } 121 | 122 | func (db *DB) Commit() *DB { 123 | return &DB{db.DB.Commit()} 124 | } 125 | 126 | func (db *DB) Rollback() *DB { 127 | return &DB{db.DB.Rollback()} 128 | } 129 | 130 | // 锁相关 131 | func (db *DB) Clauses(conds ...clause.Expression) *DB { 132 | return &DB{db.DB.Clauses(conds...)} 133 | } 134 | 135 | // 统计 136 | func (db *DB) Count(count *int64) *DB { 137 | return &DB{db.DB.Count(count)} 138 | } 139 | 140 | // 原始SQL 141 | func (db *DB) Raw(sql string, values ...any) *DB { 142 | return &DB{db.DB.Raw(sql, values...)} 143 | } 144 | 145 | func (db *DB) Exec(sql string, values ...any) *DB { 146 | return &DB{db.DB.Exec(sql, values...)} 147 | } 148 | 149 | // Scopes 150 | func (db *DB) Scopes(funcs ...func(*gorm.DB) *gorm.DB) *DB { 151 | return &DB{db.DB.Scopes(funcs...)} 152 | } 153 | 154 | // 错误处理 155 | func (db *DB) Error() error { 156 | return errorx.TryToDBError(db.DB.Error) 157 | } 158 | 159 | // Ping 160 | func (db *DB) Ping() error { 161 | var num int 162 | err := db.DB.Raw("select 1").Scan(&num).Error 163 | return errorx.TryToDBError(err) 164 | } 165 | -------------------------------------------------------------------------------- /internal/component/db/db_log.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "go-gin/internal/component/logx" 8 | "go-gin/internal/util" 9 | "strings" 10 | "time" 11 | 12 | "gorm.io/gorm/logger" 13 | ) 14 | 15 | var ( 16 | traceStr = "%s [%.3fms] [rows:%v] %s" 17 | traceWarnStr = "%s %s [%.3fms] [rows:%v] %s" 18 | traceErrStr = "%s %s [%.3fms] [rows:%v] %s" 19 | ) 20 | 21 | type DBLog struct { 22 | LogLevel logger.LogLevel 23 | } 24 | 25 | func ParseLevel(levelStr string) logger.LogLevel { 26 | level_str := strings.ToLower(levelStr) 27 | switch level_str { 28 | case "debug": 29 | return logger.Info 30 | case "info": 31 | return logger.Info 32 | case "warn": 33 | return logger.Warn 34 | case "error": 35 | return logger.Error 36 | } 37 | return logger.Info 38 | } 39 | 40 | // LogMode log mode 41 | func (l *DBLog) LogMode(level logger.LogLevel) logger.Interface { 42 | 43 | return &DBLog{ 44 | LogLevel: level, 45 | } 46 | } 47 | 48 | // Info print info 49 | func (l *DBLog) Info(ctx context.Context, msg string, data ...any) { 50 | } 51 | 52 | // Warn print warn messages 53 | func (l *DBLog) Warn(ctx context.Context, msg string, data ...any) { 54 | } 55 | 56 | // Error print error messages 57 | func (l *DBLog) Error(ctx context.Context, msg string, data ...any) { 58 | } 59 | 60 | // Trace print sql message 61 | // 62 | //nolint:cyclop 63 | func (l *DBLog) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) { 64 | if l.LogLevel <= logger.Silent { 65 | return 66 | } 67 | elapsed := time.Since(begin) 68 | slowThreshold := 10 * time.Second 69 | switch { 70 | case err != nil && l.LogLevel >= logger.Error && !errors.Is(err, logger.ErrRecordNotFound): 71 | sql, rows := fc() 72 | if rows == -1 { 73 | logx.WithContext(ctx).Errorf("sql", traceErrStr, util.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, "-", sql) 74 | } else { 75 | logx.WithContext(ctx).Errorf("sql", traceErrStr, util.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, rows, sql) 76 | } 77 | case elapsed > slowThreshold && l.LogLevel >= logger.Warn: 78 | sql, rows := fc() 79 | slowLog := fmt.Sprintf("SLOW SQL >= %v", slowThreshold) 80 | if rows == -1 { 81 | logx.WithContext(ctx).Warnf("sql", traceWarnStr, util.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, '-', sql) 82 | } else { 83 | logx.WithContext(ctx).Warnf("sql", traceWarnStr, util.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, rows, sql) 84 | 85 | } 86 | case l.LogLevel == logger.Info: 87 | sql, rows := fc() 88 | if rows == -1 { 89 | logx.WithContext(ctx).Debugf("sql", traceStr, util.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, "-", sql) 90 | } else { 91 | logx.WithContext(ctx).Debugf("sql", traceStr, util.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, rows, sql) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /internal/component/db/init.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "context" 5 | 6 | "gorm.io/driver/mysql" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | var ( 11 | instance *gorm.DB 12 | conf Config 13 | ) 14 | 15 | type Config struct { 16 | DSN string `yaml:"dsn"` 17 | MaxOpenConns int `yaml:"max-open-conn"` 18 | MaxIdleConns int `yaml:"max-idle-conn"` 19 | LogLevel string `yaml:"log-level"` 20 | } 21 | 22 | func InitConfig(c Config) { 23 | conf = c 24 | } 25 | 26 | func Init() { 27 | err := Connect() 28 | if err != nil { 29 | panic(err) 30 | } 31 | } 32 | 33 | func IsConnected() bool { 34 | return instance != nil 35 | } 36 | 37 | func Connect() (err error) { 38 | instance, err = gorm.Open(mysql.New(mysql.Config{ 39 | DSN: conf.DSN, // DSN data source name 40 | DefaultStringSize: 256, // string 类型字段的默认长度 41 | DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持 42 | DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引 43 | DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列 44 | SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置 45 | }), &gorm.Config{ 46 | Logger: &DBLog{ 47 | LogLevel: ParseLevel(conf.LogLevel), 48 | }, 49 | }) 50 | if err != nil { 51 | instance = nil 52 | return err 53 | } 54 | 55 | sqlDB, err := instance.DB() 56 | if err != nil { 57 | instance = nil 58 | return err 59 | } 60 | 61 | if err = sqlDB.Ping(); err != nil { 62 | instance = nil 63 | return err 64 | } 65 | // SetMaxIdleConns sets the maximum number of connections in the idle connection pool. 66 | sqlDB.SetMaxIdleConns(conf.MaxIdleConns) 67 | 68 | // SetMaxOpenConns sets the maximum number of open connections to the database. 69 | sqlDB.SetMaxOpenConns(conf.MaxOpenConns) 70 | return 71 | } 72 | 73 | func WithContext(ctx context.Context) *DB { 74 | return &DB{instance.WithContext(ctx)} 75 | } 76 | -------------------------------------------------------------------------------- /internal/component/logx/console_writer.go: -------------------------------------------------------------------------------- 1 | package logx 2 | 3 | import ( 4 | "github.com/labstack/gommon/color" 5 | "github.com/rs/zerolog" 6 | ) 7 | 8 | type ConsoleLevelWriter struct { 9 | } 10 | 11 | var _ zerolog.LevelWriter = (*ConsoleLevelWriter)(nil) 12 | 13 | func (w *ConsoleLevelWriter) Write(p []byte) (n int, err error) { 14 | color.Print(color.White(string(p))) 15 | return len(p), nil 16 | } 17 | 18 | func (w ConsoleLevelWriter) WriteLevel(l zerolog.Level, p []byte) (n int, err error) { 19 | s := string(p) 20 | switch l { 21 | case zerolog.WarnLevel: 22 | s = color.Magenta(s) 23 | case zerolog.ErrorLevel: 24 | s = color.Red(s) 25 | case zerolog.FatalLevel: 26 | s = color.Bold(color.Red(s)) 27 | default: 28 | s = color.White(s) 29 | } 30 | color.Print(s) 31 | return len(p), nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/component/logx/file_writer.go: -------------------------------------------------------------------------------- 1 | package logx 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/rs/zerolog" 9 | ) 10 | 11 | type FileLevelWriter struct { 12 | Dirname string 13 | FilePattern string 14 | } 15 | 16 | var _ zerolog.LevelWriter = (*FileLevelWriter)(nil) 17 | 18 | func (w *FileLevelWriter) Write(p []byte) (n int, err error) { 19 | w.output("", p) 20 | return len(p), nil 21 | } 22 | 23 | func (w FileLevelWriter) WriteLevel(l zerolog.Level, p []byte) (n int, err error) { 24 | // s := string(p) 25 | suffix := "" 26 | switch l { 27 | case zerolog.WarnLevel, zerolog.ErrorLevel, zerolog.FatalLevel: 28 | suffix = "-error" 29 | } 30 | w.output(suffix, p) 31 | return len(p), nil 32 | } 33 | 34 | func (w FileLevelWriter) output(suffix string, p []byte) { 35 | pattern := time.Now().Format(w.FilePattern) 36 | dir := w.Dirname 37 | _, err := os.Stat(dir) 38 | if os.IsNotExist(err) { 39 | err := os.MkdirAll(dir, 0744) 40 | if err != nil { 41 | panic(fmt.Errorf("can't make directories for new logfile: %s", err)) 42 | } 43 | } 44 | 45 | filename := dir + pattern + suffix + ".log" 46 | logFile, err := os.OpenFile(filename, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 47 | if err != nil { 48 | panic(fmt.Sprintf("can not open or create file %s,error=%s", filename, err.Error())) 49 | } 50 | defer logFile.Close() 51 | logFile.Write(p) 52 | } 53 | -------------------------------------------------------------------------------- /internal/component/logx/init.go: -------------------------------------------------------------------------------- 1 | package logx 2 | 3 | import ( 4 | "go-gin/internal/environment" 5 | "io" 6 | "strings" 7 | "time" 8 | 9 | "github.com/labstack/gommon/color" 10 | "github.com/rs/zerolog" 11 | "github.com/rs/zerolog/log" 12 | ) 13 | 14 | var ( 15 | AccessLoggerInstance zerolog.Logger 16 | DBLoggerInstance zerolog.Logger 17 | RestyLoggerInstance zerolog.Logger 18 | CronLoggerInstance zerolog.Logger 19 | QueueLoggerInstance zerolog.Logger 20 | ) 21 | 22 | type Config struct { 23 | Level string `yaml:"level"` 24 | Path string `yaml:"path"` 25 | } 26 | 27 | var conf Config 28 | 29 | func InitConfig(c Config) { 30 | if !strings.HasSuffix(c.Path, "/") { 31 | c.Path = c.Path + "/" 32 | } 33 | conf = c 34 | } 35 | 36 | func Init() { 37 | if environment.IsDebugMode() { 38 | color.Enable() 39 | } 40 | level, err := zerolog.ParseLevel(conf.Level) 41 | if err != nil { 42 | level = zerolog.InfoLevel 43 | } 44 | zerolog.TimeFieldFormat = time.DateTime 45 | zerolog.LevelFieldMarshalFunc = func(l zerolog.Level) string { 46 | return strings.ToUpper(l.String()) 47 | } 48 | 49 | log.Logger = initDefaultInstance(level) 50 | AccessLoggerInstance = initLoggerInstance("access") 51 | RestyLoggerInstance = initLoggerInstance("httpc") 52 | CronLoggerInstance = initLoggerInstance("access_cron") 53 | QueueLoggerInstance = initLoggerInstance("access_queue") 54 | } 55 | 56 | func initDefaultInstance(l zerolog.Level) zerolog.Logger { 57 | fileWriter := zerolog.SyncWriter(&FileLevelWriter{ 58 | Dirname: conf.Path, 59 | FilePattern: time.DateOnly, 60 | }) 61 | 62 | writers := []io.Writer{fileWriter} 63 | if environment.IsDebugMode() { 64 | writers = append(writers, &ConsoleLevelWriter{}) 65 | } 66 | multi := zerolog.MultiLevelWriter(writers...) 67 | log.Output(multi).Level(l).With().Logger().Hook(TracingHook{}) 68 | return zerolog.Nop() 69 | } 70 | 71 | func initLoggerInstance(path string) zerolog.Logger { 72 | queueFileWriter := zerolog.SyncWriter(&FileLevelWriter{ 73 | Dirname: conf.Path + strings.Trim(path, "/") + "/", 74 | FilePattern: time.DateOnly, 75 | }) 76 | writers := []io.Writer{queueFileWriter} 77 | if environment.IsDebugMode() { 78 | writers = append(writers, &ConsoleLevelWriter{}) 79 | } 80 | return zerolog.Nop().Output(zerolog.MultiLevelWriter(writers...)).Level(zerolog.InfoLevel).With().Timestamp().Logger().Hook(TracingHook{}) 81 | } 82 | -------------------------------------------------------------------------------- /internal/component/logx/log.go: -------------------------------------------------------------------------------- 1 | package logx 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/rs/zerolog" 8 | "github.com/rs/zerolog/log" 9 | ) 10 | 11 | type default_log struct { 12 | ctx context.Context 13 | } 14 | 15 | var _ Logger = (*default_log)(nil) 16 | 17 | func WithContext(ctx context.Context) Logger { 18 | return &default_log{ctx} 19 | } 20 | 21 | func (l *default_log) Debug(keyword string, data any) { 22 | l.print(zerolog.DebugLevel, keyword, data) 23 | } 24 | 25 | func (l *default_log) Debugf(keyword string, format string, data ...any) { 26 | l.Debug(keyword, fmt.Sprintf(format, data...)) 27 | } 28 | 29 | func (l *default_log) Info(keyword string, data any) { 30 | l.print(zerolog.InfoLevel, keyword, data) 31 | } 32 | 33 | func (l *default_log) Infof(keyword string, format string, data ...any) { 34 | l.Info(keyword, fmt.Sprintf(format, data...)) 35 | } 36 | 37 | func (l *default_log) Warn(keyword string, data any) { 38 | l.print(zerolog.WarnLevel, keyword, data) 39 | } 40 | 41 | func (l *default_log) Warnf(keyword string, format string, data ...any) { 42 | l.Warn(keyword, fmt.Sprintf(format, data...)) 43 | } 44 | 45 | func (l *default_log) Error(keyword string, data any) { 46 | l.print(zerolog.ErrorLevel, keyword, data) 47 | } 48 | 49 | func (l *default_log) Errorf(keyword string, format string, data ...any) { 50 | l.Error(keyword, fmt.Sprintf(format, data...)) 51 | } 52 | 53 | func (l *default_log) print(level zerolog.Level, keyword string, data any) { 54 | log.WithLevel(level).Ctx(l.ctx).Str("keyword", keyword).Any("data", data).Send() 55 | } 56 | -------------------------------------------------------------------------------- /internal/component/logx/logger.go: -------------------------------------------------------------------------------- 1 | package logx 2 | 3 | type Logger interface { 4 | Debug(keyword string, message any) 5 | Debugf(keyword string, format string, message ...any) 6 | 7 | Info(keyword string, message any) 8 | Infof(keyword string, format string, message ...any) 9 | 10 | Warn(keyword string, message any) 11 | Warnf(keyword string, format string, message ...any) 12 | 13 | Error(keyword string, message any) 14 | Errorf(keyword string, format string, message ...any) 15 | } 16 | -------------------------------------------------------------------------------- /internal/component/logx/tracing_hook.go: -------------------------------------------------------------------------------- 1 | package logx 2 | 3 | import ( 4 | "context" 5 | "go-gin/internal/traceid" 6 | 7 | "github.com/rs/zerolog" 8 | ) 9 | 10 | type TracingHook struct{} 11 | 12 | func (h TracingHook) Run(e *zerolog.Event, level zerolog.Level, msg string) { 13 | 14 | traceId := getTraceIdFromContext(e.GetCtx()) 15 | if traceId != "" { 16 | e.Str(traceid.TraceIdFieldName, getTraceIdFromContext(e.GetCtx())) 17 | } 18 | } 19 | 20 | func getTraceIdFromContext(ctx context.Context) string { 21 | if trace_id, ok := ctx.Value(traceid.TraceIdFieldName).(string); ok { 22 | return trace_id 23 | } 24 | return "" 25 | } 26 | -------------------------------------------------------------------------------- /internal/component/redisx/error_hook.go: -------------------------------------------------------------------------------- 1 | package redisx 2 | 3 | import ( 4 | "context" 5 | "go-gin/internal/errorx" 6 | "net" 7 | 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | type ErrHook struct{} 12 | 13 | func (ErrHook) DialHook(next redis.DialHook) redis.DialHook { 14 | return func(ctx context.Context, network, addr string) (net.Conn, error) { 15 | conn, err := next(ctx, network, addr) 16 | return conn, errorx.TryToRedisError(err) 17 | } 18 | } 19 | func (ErrHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { 20 | return func(ctx context.Context, cmd redis.Cmder) error { 21 | return errorx.TryToRedisError(next(ctx, cmd)) 22 | } 23 | } 24 | func (ErrHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { 25 | return func(ctx context.Context, cmds []redis.Cmder) error { 26 | return errorx.TryToRedisError(next(ctx, cmds)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /internal/component/redisx/init.go: -------------------------------------------------------------------------------- 1 | package redisx 2 | 3 | import ( 4 | "context" 5 | "go-gin/internal/component/logx" 6 | 7 | "github.com/redis/go-redis/v9" 8 | ) 9 | 10 | var ( 11 | instance *redis.Client 12 | conf Config 13 | ) 14 | 15 | type Config struct { 16 | Addr string `yaml:"addr"` 17 | Username string `yaml:"username"` 18 | Password string `yaml:"password"` // no password set 19 | DB int `yaml:"db"` // use default DB 20 | } 21 | 22 | func InitConfig(c Config) { 23 | conf = c 24 | } 25 | 26 | func Init() { 27 | options := &redis.Options{ 28 | Addr: conf.Addr, 29 | Username: conf.Username, 30 | Password: conf.Password, 31 | DB: conf.DB, 32 | } 33 | rdb := redis.NewClient(options) 34 | rdb.AddHook(&LogHook{}) 35 | rdb.AddHook(&ErrHook{}) 36 | err := rdb.Ping(context.Background()).Err() 37 | if err != nil { 38 | logx.WithContext(context.Background()).Error("redis", err) 39 | } 40 | instance = rdb 41 | } 42 | 43 | func Client() *redis.Client { 44 | return instance 45 | } 46 | -------------------------------------------------------------------------------- /internal/component/redisx/log_hook.go: -------------------------------------------------------------------------------- 1 | package redisx 2 | 3 | import ( 4 | "context" 5 | "go-gin/internal/component/logx" 6 | "go-gin/internal/util" 7 | "net" 8 | 9 | "github.com/redis/go-redis/v9" 10 | ) 11 | 12 | type LogHook struct{} 13 | 14 | func (LogHook) DialHook(next redis.DialHook) redis.DialHook { 15 | return func(ctx context.Context, network, addr string) (net.Conn, error) { 16 | return next(ctx, network, addr) 17 | 18 | } 19 | } 20 | func (LogHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook { 21 | return func(ctx context.Context, cmd redis.Cmder) error { 22 | err := next(ctx, cmd) 23 | if err != nil { 24 | logx.WithContext(ctx).Warnf("redis", "%s execute command:%+v, error=%s", util.FileWithLineNum(), cmd.Args(), err) 25 | return err 26 | } 27 | return nil 28 | } 29 | } 30 | func (LogHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook { 31 | return func(ctx context.Context, cmds []redis.Cmder) error { 32 | return next(ctx, cmds) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/cronx/builder.go: -------------------------------------------------------------------------------- 1 | package cronx 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | // 星期常量定义 10 | const ( 11 | Sunday = 0 12 | Monday = 1 13 | Tuesday = 2 14 | Wednesday = 3 15 | Thursday = 4 16 | Friday = 5 17 | Saturday = 6 18 | ) 19 | 20 | // JobBuilder 用于构建带有执行周期的任务 21 | type JobBuilder struct { 22 | job Job 23 | expression string // cron 表达式 24 | } 25 | 26 | // 添加构造函数 27 | func NewJobBuilder(job Job) *JobBuilder { 28 | return &JobBuilder{ 29 | job: job, 30 | expression: "* * * * *", 31 | } 32 | } 33 | 34 | // 基础时间调度方法 35 | func (jb *JobBuilder) spliceIntoPosition(position int, value string) *JobBuilder { 36 | segments := strings.Split(jb.expression, " ") 37 | if len(segments) != 5 { 38 | segments = []string{"*", "*", "*", "*", "*"} 39 | } 40 | segments[position-1] = value 41 | jb.expression = strings.Join(segments, " ") 42 | return jb 43 | } 44 | 45 | // handleJob 用于执行任务 46 | func (jb *JobBuilder) handleJob() { 47 | AddJob(jb.expression, jb.job) 48 | } 49 | 50 | // Cron 设置自定义cron表达式 51 | func (jb *JobBuilder) Cron(expression string) { 52 | jb.expression = expression 53 | jb.handleJob() 54 | } 55 | 56 | // 分钟级别的调度 57 | func (jb *JobBuilder) EveryMinute() { 58 | jb.spliceIntoPosition(1, "*"). 59 | handleJob() 60 | } 61 | 62 | func (jb *JobBuilder) EveryTwoMinutes() { 63 | jb.spliceIntoPosition(1, "*/2"). 64 | handleJob() 65 | } 66 | 67 | func (jb *JobBuilder) EveryThreeMinutes() { 68 | jb.spliceIntoPosition(1, "*/3"). 69 | handleJob() 70 | } 71 | 72 | func (jb *JobBuilder) EveryFourMinutes() { 73 | jb.spliceIntoPosition(1, "*/4"). 74 | handleJob() 75 | } 76 | 77 | func (jb *JobBuilder) EveryFiveMinutes() { 78 | jb.spliceIntoPosition(1, "*/5"). 79 | handleJob() 80 | } 81 | 82 | func (jb *JobBuilder) EveryTenMinutes() { 83 | jb.spliceIntoPosition(1, "*/10"). 84 | handleJob() 85 | } 86 | 87 | func (jb *JobBuilder) EveryFifteenMinutes() { 88 | jb.spliceIntoPosition(1, "*/15"). 89 | handleJob() 90 | } 91 | 92 | func (jb *JobBuilder) EveryThirtyMinutes() { 93 | jb.spliceIntoPosition(1, "0,30"). 94 | handleJob() 95 | } 96 | 97 | // 小时级别的调度 98 | func (jb *JobBuilder) Hourly() { 99 | jb.spliceIntoPosition(1, "0"). 100 | spliceIntoPosition(2, "*"). 101 | handleJob() 102 | } 103 | 104 | func (jb *JobBuilder) HourlyAt(minute int) { 105 | jb.spliceIntoPosition(1, strconv.Itoa(minute)). 106 | handleJob() 107 | } 108 | 109 | func (jb *JobBuilder) EveryTwoHours() { 110 | jb.spliceIntoPosition(2, "*/2"). 111 | handleJob() 112 | } 113 | 114 | func (jb *JobBuilder) EveryThreeHours() { 115 | jb.spliceIntoPosition(2, "*/3"). 116 | handleJob() 117 | } 118 | 119 | func (jb *JobBuilder) EveryFourHours() { 120 | jb.spliceIntoPosition(2, "*/4"). 121 | handleJob() 122 | } 123 | 124 | func (jb *JobBuilder) EverySixHours() { 125 | jb.spliceIntoPosition(2, "*/6"). 126 | handleJob() 127 | } 128 | 129 | // 天级别的调度 130 | func (jb *JobBuilder) Daily() { 131 | jb.spliceIntoPosition(1, "0"). 132 | spliceIntoPosition(2, "0"). 133 | handleJob() 134 | } 135 | 136 | func (jb *JobBuilder) DailyAt(time string) { 137 | segments := strings.Split(time, ":") 138 | if len(segments) != 2 { 139 | panic("Time should be in format HH:mm") 140 | } 141 | 142 | // 添加时间验证 143 | hour, err := strconv.Atoi(segments[0]) 144 | if err != nil || hour < 0 || hour > 23 { 145 | panic("Hour must be between 0 and 23") 146 | } 147 | 148 | minute, err := strconv.Atoi(segments[1]) 149 | if err != nil || minute < 0 || minute > 59 { 150 | panic("Minute must be between 0 and 59") 151 | } 152 | 153 | jb.spliceIntoPosition(1, segments[1]). 154 | spliceIntoPosition(2, segments[0]). 155 | handleJob() 156 | } 157 | 158 | // 星期几的调度 159 | func (jb *JobBuilder) Weekdays() { 160 | jb.Days(fmt.Sprintf("%d-%d", Monday, Friday)) 161 | } 162 | 163 | func (jb *JobBuilder) Weekends() { 164 | jb.Days(fmt.Sprintf("%d,%d")) 165 | } 166 | 167 | func (jb *JobBuilder) Mondays() { 168 | jb.Days(strconv.Itoa(Monday)) 169 | } 170 | 171 | func (jb *JobBuilder) Tuesdays() { 172 | jb.Days(strconv.Itoa(Tuesday)) 173 | } 174 | 175 | func (jb *JobBuilder) Wednesdays() { 176 | jb.Days(strconv.Itoa(Wednesday)) 177 | } 178 | 179 | func (jb *JobBuilder) Thursdays() { 180 | jb.Days(strconv.Itoa(Thursday)) 181 | } 182 | 183 | func (jb *JobBuilder) Fridays() { 184 | jb.Days(strconv.Itoa(Friday)) 185 | } 186 | 187 | func (jb *JobBuilder) Saturdays() { 188 | jb.Days(strconv.Itoa(Saturday)) 189 | } 190 | 191 | func (jb *JobBuilder) Sundays() { 192 | jb.Days(strconv.Itoa(Sunday)) 193 | } 194 | 195 | func (jb *JobBuilder) Days(days string) { 196 | jb.spliceIntoPosition(5, days). 197 | handleJob() 198 | } 199 | 200 | // 月份相关的调度 201 | func (jb *JobBuilder) Monthly() { 202 | jb.spliceIntoPosition(1, "0"). 203 | spliceIntoPosition(2, "0"). 204 | spliceIntoPosition(3, "1"). 205 | handleJob() 206 | } 207 | 208 | func (jb *JobBuilder) MonthlyOn(dayOfMonth int, time string) { 209 | jb.DailyAt(time) 210 | jb.spliceIntoPosition(3, strconv.Itoa(dayOfMonth)). 211 | handleJob() 212 | } 213 | 214 | // 每周相关的调度 215 | func (jb *JobBuilder) Weekly() { 216 | jb.spliceIntoPosition(1, "0"). 217 | spliceIntoPosition(2, "0"). 218 | spliceIntoPosition(5, "0"). 219 | handleJob() 220 | } 221 | 222 | func (jb *JobBuilder) WeeklyOn(dayOfWeek int, time string) { 223 | jb.DailyAt(time) 224 | jb.spliceIntoPosition(5, strconv.Itoa(dayOfWeek)). 225 | handleJob() 226 | } 227 | 228 | // 每月两次 229 | func (jb *JobBuilder) TwiceMonthly(first, second int, time string) { 230 | jb.DailyAt(time) 231 | jb.spliceIntoPosition(3, fmt.Sprintf("%d,%d", first, second)). 232 | handleJob() 233 | } 234 | 235 | // 每天两次 236 | func (jb *JobBuilder) TwiceDaily(first, second int) { 237 | jb.spliceIntoPosition(2, fmt.Sprintf("%d,%d")). 238 | handleJob() 239 | } 240 | 241 | func (jb *JobBuilder) TwiceDailyAt(first, second, minute int) { 242 | jb.spliceIntoPosition(1, strconv.Itoa(minute)). 243 | spliceIntoPosition(2, fmt.Sprintf("%d,%d", first, second)). 244 | handleJob() 245 | } 246 | 247 | // 秒级调度 248 | func (jb *JobBuilder) EverySecond() { 249 | jb.Cron("@every 1s") 250 | } 251 | 252 | func (jb *JobBuilder) EveryTwoSeconds() { 253 | jb.Cron("@every 2s") 254 | } 255 | 256 | func (jb *JobBuilder) EveryFiveSeconds() { 257 | jb.Cron("@every 5s") 258 | } 259 | 260 | func (jb *JobBuilder) EveryTenSeconds() { 261 | jb.Cron("@every 10s") 262 | } 263 | 264 | func (jb *JobBuilder) EveryFifteenSeconds() { 265 | jb.Cron("@every 15s") 266 | } 267 | 268 | func (jb *JobBuilder) EveryThirtySeconds() { 269 | jb.Cron("@every 30s") 270 | } 271 | 272 | // 季度和年度调度 273 | func (jb *JobBuilder) Quarterly() { 274 | jb.spliceIntoPosition(1, "0"). 275 | spliceIntoPosition(2, "0"). 276 | spliceIntoPosition(3, "1"). 277 | spliceIntoPosition(4, "1,4,7,10"). 278 | handleJob() 279 | } 280 | 281 | func (jb *JobBuilder) QuarterlyOn(dayOfQuarter int, time string) { 282 | jb.DailyAt(time) 283 | jb.spliceIntoPosition(3, strconv.Itoa(dayOfQuarter)). 284 | spliceIntoPosition(4, "1,4,7,10"). 285 | handleJob() 286 | } 287 | 288 | func (jb *JobBuilder) Yearly() { 289 | jb.spliceIntoPosition(1, "0"). 290 | spliceIntoPosition(2, "0"). 291 | spliceIntoPosition(3, "1"). 292 | spliceIntoPosition(4, "1"). 293 | handleJob() 294 | } 295 | 296 | func (jb *JobBuilder) YearlyOn(month int, dayOfMonth int, time string) { 297 | jb.DailyAt(time) 298 | jb.spliceIntoPosition(3, strconv.Itoa(dayOfMonth)). 299 | spliceIntoPosition(4, strconv.Itoa(month)). 300 | handleJob() 301 | } 302 | 303 | // 月末调度 304 | func (jb *JobBuilder) LastDayOfMonth(time string) { 305 | segments := strings.Split(time, ":") 306 | if len(segments) != 2 { 307 | panic("Time should be in format HH:mm") 308 | } 309 | 310 | // 添加时间验证 311 | hour, err := strconv.Atoi(segments[0]) 312 | if err != nil || hour < 0 || hour > 23 { 313 | panic("Hour must be between 0 and 23") 314 | } 315 | 316 | minute, err := strconv.Atoi(segments[1]) 317 | if err != nil || minute < 0 || minute > 59 { 318 | panic("Minute must be between 0 and 59") 319 | } 320 | 321 | jb.spliceIntoPosition(1, segments[1]). // 分钟 322 | spliceIntoPosition(2, segments[0]). // 小时 323 | spliceIntoPosition(3, "L"). // 日期(月末) 324 | spliceIntoPosition(4, "*"). // 月份 325 | spliceIntoPosition(5, "*"). // 星期 326 | handleJob() 327 | } 328 | 329 | // 奇数小时调度 330 | func (jb *JobBuilder) EveryOddHour(minute int) { 331 | jb.spliceIntoPosition(1, strconv.Itoa(minute)). 332 | spliceIntoPosition(2, "1-23/2"). 333 | handleJob() 334 | } 335 | 336 | 337 | -------------------------------------------------------------------------------- /internal/cronx/init.go: -------------------------------------------------------------------------------- 1 | package cronx 2 | 3 | import ( 4 | "context" 5 | "go-gin/internal/component/logx" 6 | "go-gin/internal/traceid" 7 | "reflect" 8 | "runtime" 9 | "strings" 10 | "time" 11 | 12 | "github.com/robfig/cron/v3" 13 | ) 14 | 15 | var c *cron.Cron 16 | 17 | func New() { 18 | c = cron.New() 19 | } 20 | 21 | func Run() { 22 | c.Start() 23 | // 程序结束时停止 cron 定时器(可选) 24 | defer c.Stop() 25 | // 主程序可以保持运行状态,等待 cron 任务执行 26 | select {} 27 | } 28 | 29 | // Schedule 创建定时任务 30 | func Schedule(job Job) *JobBuilder { 31 | if c == nil { 32 | panic("please call cronx.New() first") 33 | } 34 | return NewJobBuilder(job) 35 | } 36 | 37 | func AddJob(spec string, cmd Job) { 38 | jobName := getStructName(cmd) 39 | _, err := c.AddFunc(spec, func() { 40 | ctx := context.WithValue(context.Background(), traceid.TraceIdFieldName, traceid.New()) 41 | logx.CronLoggerInstance.Info().Ctx(ctx).Str("cron", jobName).Str("spec", spec).Str("keywords", "开始执行").Send() 42 | 43 | start := time.Now() 44 | 45 | err := cmd.Handle(ctx) 46 | 47 | TimeStamp := time.Now() 48 | Cost := TimeStamp.Sub(start) 49 | if Cost > time.Minute { 50 | Cost = Cost.Truncate(time.Second) 51 | } 52 | if err != nil { 53 | logx.CronLoggerInstance.Error().Ctx(ctx).Str("cron", jobName).Str("spec", spec).Str("keywords", "执行结束").Str("cost", Cost.String()).Str("err", err.Error()).Send() 54 | } else { 55 | logx.CronLoggerInstance.Info().Ctx(ctx).Str("cron", jobName).Str("spec", spec).Str("keywords", "执行结束").Str("cost", Cost.String()).Send() 56 | } 57 | }) 58 | if err != nil { 59 | logx.CronLoggerInstance.Info().Ctx(context.Background()).Str("cron", jobName).Str("spec", spec).Str("keywords", "添加失败").Send() 60 | } 61 | } 62 | 63 | // ScheduleFunc 创建函数类型的定时任务 64 | func ScheduleFunc(fn JobFunc) *JobBuilder { 65 | if c == nil { 66 | panic("please call cronx.New() first") 67 | } 68 | return NewJobBuilder(fn) 69 | } 70 | 71 | func AddFunc(spec string, cmd JobFunc) { 72 | AddJob(spec, cmd) 73 | } 74 | 75 | // 添加一个工具函数来获取结构体名称 76 | func getStructName(v interface{}) string { 77 | t := reflect.TypeOf(v) 78 | 79 | // 处理函数类型 80 | if t.Kind() == reflect.Func { 81 | // 获取函数的完整路径名 82 | fullName := runtime.FuncForPC(reflect.ValueOf(v).Pointer()).Name() 83 | // 提取最后一个点号后的函数名 84 | if lastDot := strings.LastIndex(fullName, "."); lastDot >= 0 { 85 | return fullName[lastDot+1:] 86 | } 87 | return fullName 88 | } 89 | 90 | // 处理指针类型 91 | if t.Kind() == reflect.Ptr { 92 | return t.Elem().Name() 93 | } 94 | 95 | // 处理普通类型 96 | return t.Name() 97 | } 98 | -------------------------------------------------------------------------------- /internal/cronx/var.go: -------------------------------------------------------------------------------- 1 | package cronx 2 | 3 | import "context" 4 | 5 | type Job interface { 6 | Handle(ctx context.Context) error 7 | } 8 | 9 | type JobFunc func(context.Context) error 10 | 11 | var _ Job = JobFunc(nil) 12 | 13 | func (f JobFunc) Handle(ctx context.Context) error { 14 | return f(ctx) 15 | } 16 | -------------------------------------------------------------------------------- /internal/environment/init.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | type Mode string 4 | 5 | const ( 6 | // DebugMode indicates gin mode is debug. 7 | DebugMode = "debug" 8 | // ReleaseMode indicates gin mode is release. 9 | ReleaseMode = "release" 10 | // TestMode indicates gin mode is test. 11 | TestMode = "test" 12 | ) 13 | 14 | var EnvMode Mode = ReleaseMode 15 | 16 | func SetEnvMode(s Mode) { 17 | EnvMode = s 18 | } 19 | 20 | func IsDebugMode() bool { 21 | return EnvMode == DebugMode 22 | } 23 | 24 | func GetEnvMode() Mode { 25 | return EnvMode 26 | } 27 | -------------------------------------------------------------------------------- /internal/environment/timezone.go: -------------------------------------------------------------------------------- 1 | package environment 2 | 3 | import "os" 4 | 5 | func SetTimeZone(val string) { 6 | err := os.Setenv("TZ", val) 7 | if err != nil { 8 | panic("设置环境变量失败:" + err.Error()) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /internal/errorx/biz_error.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | type BizError struct { 4 | Code int 5 | Msg string 6 | } 7 | 8 | func New(code int, msg string) BizError { 9 | return BizError{Code: code, Msg: msg} 10 | } 11 | 12 | func (e BizError) Error() string { 13 | return e.Msg 14 | } 15 | -------------------------------------------------------------------------------- /internal/errorx/db_error.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "errors" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type DBError struct { 10 | Msg string 11 | } 12 | 13 | func (e DBError) Error() string { 14 | return e.Msg 15 | } 16 | 17 | func TryToDBError(err error) error { 18 | if err == nil { 19 | return nil 20 | } 21 | if errors.Is(err, gorm.ErrRecordNotFound) { 22 | return err 23 | } 24 | return DBError{Msg: err.Error()} 25 | } 26 | -------------------------------------------------------------------------------- /internal/errorx/redis_error.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | type RedisError struct { 4 | Msg string 5 | } 6 | 7 | func TryToRedisError(err error) error { 8 | if err == nil { 9 | return nil 10 | } 11 | return RedisError{Msg: err.Error()} 12 | } 13 | 14 | func (e RedisError) Error() string { 15 | return e.Msg 16 | } 17 | -------------------------------------------------------------------------------- /internal/errorx/server_error.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type ServerError struct { 8 | Code int 9 | Msg string 10 | } 11 | 12 | func NewServerError(code int) ServerError { 13 | 14 | msg, ok := StatusText(code) 15 | if !ok { 16 | msg = "未知错误" 17 | } 18 | return ServerError{Code: code, Msg: msg} 19 | } 20 | 21 | func (c ServerError) Error() string { 22 | return c.Msg 23 | } 24 | 25 | // 定义一个映射表,将 HTTP 状态码的英文描述映射成中文描述 26 | var statusTextCN = map[int]string{ 27 | http.StatusContinue: "继续", 28 | http.StatusSwitchingProtocols: "切换协议", 29 | http.StatusProcessing: "处理中", 30 | http.StatusEarlyHints: "早期提示", 31 | 32 | http.StatusOK: "成功", 33 | http.StatusCreated: "已创建", 34 | http.StatusAccepted: "已接受", 35 | http.StatusNonAuthoritativeInfo: "非权威信息", 36 | http.StatusNoContent: "无内容", 37 | http.StatusResetContent: "重置内容", 38 | http.StatusPartialContent: "部分内容", 39 | http.StatusMultiStatus: "多状态", 40 | http.StatusAlreadyReported: "已报告", 41 | http.StatusIMUsed: "IM 已用", 42 | 43 | http.StatusMultipleChoices: "多种选择", 44 | http.StatusMovedPermanently: "永久移动", 45 | http.StatusFound: "已找到", 46 | http.StatusSeeOther: "另请参见", 47 | http.StatusNotModified: "未修改", 48 | http.StatusUseProxy: "使用代理", 49 | http.StatusTemporaryRedirect: "临时重定向", 50 | http.StatusPermanentRedirect: "永久重定向", 51 | 52 | http.StatusBadRequest: "错误的请求", 53 | http.StatusUnauthorized: "未经授权", 54 | http.StatusPaymentRequired: "需要付款", 55 | http.StatusForbidden: "禁止访问", 56 | http.StatusNotFound: "页面不存在", 57 | http.StatusMethodNotAllowed: "方法不允许", 58 | http.StatusNotAcceptable: "不可接受", 59 | http.StatusProxyAuthRequired: "需要代理授权", 60 | http.StatusRequestTimeout: "请求超时", 61 | http.StatusConflict: "冲突", 62 | http.StatusGone: "已经不存在", 63 | http.StatusLengthRequired: "需要 Content-Length", 64 | http.StatusPreconditionFailed: "前置条件失败", 65 | http.StatusRequestEntityTooLarge: "请求实体过大", 66 | http.StatusRequestURITooLong: "请求 URI 过长", 67 | http.StatusUnsupportedMediaType: "不支持的媒体类型", 68 | http.StatusRequestedRangeNotSatisfiable: "请求的范围无法满足", 69 | http.StatusExpectationFailed: "预期失败", 70 | http.StatusTeapot: "茶壶", 71 | http.StatusMisdirectedRequest: "错误的请求方向", 72 | http.StatusUnprocessableEntity: "无法处理的实体", 73 | http.StatusLocked: "已锁定", 74 | http.StatusFailedDependency: "依赖失败", 75 | http.StatusTooEarly: "请求过早", 76 | http.StatusUpgradeRequired: "需要升级", 77 | http.StatusPreconditionRequired: "需要预加载", 78 | http.StatusTooManyRequests: "请求过多", 79 | http.StatusRequestHeaderFieldsTooLarge: "请求头过大", 80 | http.StatusUnavailableForLegalReasons: "因法律原因不可用", 81 | 82 | http.StatusInternalServerError: "服务器内部错误", 83 | http.StatusNotImplemented: "未实现", 84 | http.StatusBadGateway: "错误的网关", 85 | http.StatusServiceUnavailable: "服务不可用", 86 | http.StatusGatewayTimeout: "网关超时", 87 | http.StatusHTTPVersionNotSupported: "HTTP 版本不支持", 88 | http.StatusVariantAlsoNegotiates: "变种也谈判", 89 | http.StatusInsufficientStorage: "存储空间不足", 90 | http.StatusLoopDetected: "检测到循环", 91 | http.StatusNotExtended: "未扩展", 92 | http.StatusNetworkAuthenticationRequired: "需要网络认证", 93 | } 94 | 95 | func StatusText(code int) (string, bool) { 96 | text, ok := statusTextCN[code] 97 | if ok { 98 | return text, true 99 | } 100 | return "", false 101 | } 102 | -------------------------------------------------------------------------------- /internal/errorx/util.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/redis/go-redis/v9" 7 | "gorm.io/gorm" 8 | ) 9 | 10 | func IsRecordNotFound(err error) bool { 11 | if err == nil { 12 | return false 13 | } 14 | return errors.Is(err, gorm.ErrRecordNotFound) || errors.Is(err, redis.Nil) 15 | } 16 | 17 | func IsError(err error) bool { 18 | if err == nil { 19 | return false 20 | } 21 | if errors.Is(err, gorm.ErrRecordNotFound) || errors.Is(err, redis.Nil) { 22 | return false 23 | } 24 | return true 25 | } 26 | -------------------------------------------------------------------------------- /internal/errorx/var.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import "net/http" 4 | 5 | const ( 6 | ErrCodeDefault = 10000 // 默认通用错误码 7 | ErrCodeBizDefault = 10001 // 业务默认错误 8 | ErrCodeValidateFailed = 10002 // 验证失败 9 | ErrCodeDBOperateFailed = 10003 // 数据库操作失败 10 | ErrCodeRedisOperateFailed = 10004 // redis操作失败 11 | ) 12 | 13 | var ( 14 | ErrThirdAPIConnectFailed = New(11001, "第三方接口连接失败") 15 | ErrThirdAPIContentNoContentFailed = New(11002, "第三方接口返回内容为空") 16 | ErrThirdAPIContentParseFailed = New(11003, "第三方接口响应内容解析失败") 17 | ErrThirdAPICallFormatFailed = New(11004, "第三方接口返回格式不正确") 18 | ErrThirdAPIDataParseFailed = New(11005, "第三方接口解析data失败") 19 | ErrThirdAPIBusinessFailed = New(11006, "第三方接口业务上的错误") 20 | ) 21 | 22 | // http错误 23 | var ( 24 | ErrorBadRequest = NewServerError(http.StatusBadRequest) 25 | ErrorUnauthorized = NewServerError(http.StatusUnauthorized) 26 | ErrorForbidden = NewServerError(http.StatusForbidden) 27 | ErrMethodNotAllowed = NewServerError(http.StatusMethodNotAllowed) 28 | ErrNoRoute = NewServerError(http.StatusNotFound) 29 | ErrInternalServerError = NewServerError(http.StatusInternalServerError) 30 | ) 31 | -------------------------------------------------------------------------------- /internal/etype/enum.go: -------------------------------------------------------------------------------- 1 | package etype 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | "encoding/json" 7 | "fmt" 8 | ) 9 | 10 | // IEnum 定义枚举接口 11 | type IEnum interface { 12 | Code() int 13 | Desc() string 14 | String() string 15 | // 通过 sql.Scanner 和 sql.Valuer 接口实现数据库的存储和读取 16 | // 这两个接口是 go-sql-driver/mysql 包提供的 17 | sql.Scanner 18 | driver.Valuer 19 | json.Marshaler 20 | json.Unmarshaler 21 | } 22 | 23 | // BaseEnum 基础枚举结构体 24 | type BaseEnum struct { 25 | code int 26 | desc string 27 | } 28 | 29 | // NewBaseEnum 创建基础枚举 30 | 31 | func NewBaseEnum(code int, desc string) *BaseEnum { 32 | base := &BaseEnum{ 33 | code: code, 34 | desc: desc, 35 | } 36 | return base 37 | } 38 | 39 | // Code 获取状态码 40 | func (e *BaseEnum) Code() int { 41 | return e.code 42 | } 43 | 44 | // Desc 获取描述 45 | func (e *BaseEnum) Desc() string { 46 | return e.desc 47 | } 48 | 49 | // String 实现 Stringer 接口 50 | func (e *BaseEnum) String() string { 51 | return fmt.Sprintf("%s(%d)", e.desc, e.code) 52 | } 53 | 54 | // Equal 比较两个枚举是否相等 55 | func (e *BaseEnum) Equal(enum IEnum) bool { 56 | if e == nil && enum == nil { 57 | return true 58 | } 59 | if e == nil || enum == nil { 60 | return false 61 | } 62 | return e.code == enum.Code() && e.desc == enum.Desc() 63 | } 64 | 65 | // Value 实现 driver.Valuer 接口 66 | func (e *BaseEnum) Value() (driver.Value, error) { 67 | if e == nil { 68 | return nil, nil 69 | } 70 | return int64(e.code), nil 71 | } 72 | 73 | // MarshalJSON 实现 json.Marshaler 接口 74 | func (e *BaseEnum) MarshalJSON() ([]byte, error) { 75 | if e == nil { 76 | return []byte("null"), nil 77 | } 78 | return json.Marshal(e.code) 79 | } 80 | 81 | // Scan 实现 sql.Scanner 接口 82 | func (s *BaseEnum) Scan(value any, prefix PrefixType) error { 83 | if value == nil { 84 | s = nil 85 | return nil 86 | } 87 | 88 | var code int 89 | switch v := value.(type) { 90 | case int64: 91 | code = int(v) 92 | case int: 93 | code = v 94 | default: 95 | return fmt.Errorf("不支持的类型转换: %T", value) 96 | } 97 | m := GetAll(prefix) 98 | if base, ok := m[code]; ok { 99 | s.code = code 100 | s.desc = base.Desc() 101 | return nil 102 | } 103 | return fmt.Errorf("未知的code码: %d", code) 104 | } 105 | 106 | // UnmarshalJSON 实现 json.Unmarshaler 接口 107 | func (s *BaseEnum) UnmarshalJSON(data []byte, prefix PrefixType) error { 108 | if len(data) == 0 || string(data) == "null" { 109 | s = nil 110 | return nil 111 | } 112 | 113 | var code int 114 | if err := json.Unmarshal(data, &code); err != nil { 115 | return err 116 | } 117 | m := GetAll(prefix) 118 | if base, ok := m[code]; ok { 119 | s.code = code 120 | s.desc = base.Desc() 121 | return nil 122 | } 123 | return fmt.Errorf("未知的code码: %d", code) 124 | } 125 | -------------------------------------------------------------------------------- /internal/etype/enum_map.go: -------------------------------------------------------------------------------- 1 | package etype 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // PrefixType 前缀类型 8 | type PrefixType string 9 | type ValueType map[int]*BaseEnum 10 | 11 | // 包级别的二维map变量 12 | var ( 13 | enumMap = make(map[PrefixType]ValueType) 14 | enumMapMutex sync.RWMutex 15 | ) 16 | 17 | // Set 设置枚举值 18 | func Set(prefix PrefixType, enum *BaseEnum) { 19 | 20 | enumMapMutex.Lock() 21 | defer enumMapMutex.Unlock() 22 | 23 | if _, ok := enumMap[prefix]; !ok { 24 | enumMap[prefix] = make(ValueType) 25 | } 26 | enumMap[prefix][enum.code] = enum 27 | } 28 | 29 | // Get 获取枚举值 30 | func Get(prefix PrefixType, code int) (*BaseEnum, bool) { 31 | 32 | enumMapMutex.RLock() 33 | defer enumMapMutex.RUnlock() 34 | 35 | if prefixMap, ok := enumMap[prefix]; ok { 36 | value, exists := prefixMap[code] 37 | return value, exists 38 | } 39 | return &BaseEnum{}, false 40 | } 41 | 42 | // GetAll 获取指定前缀的所有值 43 | func GetAll(prefix PrefixType) ValueType { 44 | enumMapMutex.RLock() 45 | defer enumMapMutex.RUnlock() 46 | 47 | if prefixMap, ok := enumMap[prefix]; ok { 48 | result := make(ValueType, len(prefixMap)) 49 | for k, v := range prefixMap { 50 | result[k] = v 51 | } 52 | return result 53 | } 54 | return make(ValueType) 55 | } 56 | -------------------------------------------------------------------------------- /internal/etype/num_enum.go: -------------------------------------------------------------------------------- 1 | package etype 2 | 3 | import ( 4 | "fmt" 5 | "go-gin/internal/g" 6 | ) 7 | 8 | type NumEnum int 9 | 10 | type INumEnum[T g.IntScalar] interface { 11 | Equal(a T) bool 12 | fmt.Stringer 13 | fmt.Formatter 14 | } 15 | 16 | func Equal[T g.IntScalar](a, b T) bool { 17 | return a == b 18 | } 19 | 20 | // Format 实现 fmt.Formatter 接口 21 | func Format[T g.IntScalar](f fmt.State, n T) { 22 | // 对于所有的默认打印请求(如fmt.Println调用),使用text(xx)格式 23 | fmt.Fprintf(f, "%d", int(n)) 24 | } 25 | 26 | func ParseStringFromMap[K g.IntScalar](n K, m map[K]string) string { 27 | if str, ok := m[n]; ok { 28 | return str 29 | } 30 | return "unknown" 31 | } 32 | -------------------------------------------------------------------------------- /internal/etype/util.go: -------------------------------------------------------------------------------- 1 | package etype 2 | 3 | import "fmt" 4 | 5 | func CreateBaseEnumAndSetMap(prefix PrefixType, code int, desc string) BaseEnum { 6 | baseenum := NewBaseEnum(code, desc) 7 | Set(prefix, baseenum) 8 | return *baseenum 9 | } 10 | 11 | // ParseBaseEnum 通用的枚举解析方法 12 | func ParseBaseEnum(prefix PrefixType, code int) (BaseEnum, error) { 13 | if base, ok := Get(prefix, code); ok { 14 | return CreateBaseEnumAndSetMap(prefix, code, base.Desc()), nil 15 | } 16 | return BaseEnum{}, fmt.Errorf("未知的enum码: %d", code) 17 | } 18 | -------------------------------------------------------------------------------- /internal/eventbus/event.go: -------------------------------------------------------------------------------- 1 | package eventbus 2 | 3 | import "context" 4 | 5 | type Event struct { 6 | name string 7 | payload any 8 | } 9 | 10 | func NewEvent(name string, payload any) *Event { 11 | return &Event{ 12 | name: name, 13 | payload: payload, 14 | } 15 | } 16 | 17 | func (e *Event) Name() string { 18 | return e.name 19 | } 20 | 21 | func (e *Event) Payload() any { 22 | return e.payload 23 | } 24 | 25 | func (e *Event) Fire(ctx context.Context) { 26 | Fire(ctx, e) 27 | } 28 | 29 | func (e *Event) FireIf(ctx context.Context, condition bool) { 30 | FireIf(ctx, condition, e) 31 | } 32 | 33 | // FireAsync 异步执行事件监听 34 | func (e *Event) FireAsync(ctx context.Context) { 35 | FireAsync(ctx, e) 36 | } 37 | 38 | // FireAsyncIf 异步执行事件监听,如果第一个参数是true则运行事件 39 | func (e *Event) FireAsyncIf(ctx context.Context, condition bool) { 40 | FireAsyncIf(ctx, condition, e) 41 | } 42 | -------------------------------------------------------------------------------- /internal/eventbus/listener.go: -------------------------------------------------------------------------------- 1 | package eventbus 2 | 3 | import "context" 4 | 5 | type Listener interface { 6 | Handle(context.Context, *Event) error 7 | } 8 | -------------------------------------------------------------------------------- /internal/eventbus/manager.go: -------------------------------------------------------------------------------- 1 | package eventbus 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | var mappings map[string][]Listener 9 | 10 | var once sync.Once 11 | 12 | // AddListener 为事件添加监听器 13 | func AddListener(eventname string, listener ...Listener) { 14 | name := eventName(eventname) 15 | once.Do(func() { 16 | mappings = make(map[string][]Listener, 1024) 17 | }) 18 | 19 | mappings[name] = append(mappings[name], listener...) 20 | } 21 | 22 | // Fire 同步执行事件监听,如果前一个返回error则停止执行 23 | func Fire(ctx context.Context, event *Event) { 24 | name := eventName(event.Name()) 25 | listeners := mappings[name] 26 | for _, listener := range listeners { 27 | if err := listener.Handle(ctx, event); err != nil { 28 | break 29 | } 30 | } 31 | } 32 | 33 | // FireIf 同步执行事件监听,如果第一个参数是true则运行事件 34 | func FireIf(ctx context.Context, condition bool, event *Event) { 35 | if condition { 36 | Fire(ctx, event) 37 | } 38 | } 39 | 40 | // FireAsync 异步执行事件监听 41 | func FireAsync(ctx context.Context, event *Event) { 42 | name := eventName(event.Name()) 43 | listeners := mappings[name] 44 | for _, listener := range listeners { 45 | go listener.Handle(ctx, event) 46 | } 47 | } 48 | 49 | // FireAsyncIf 异步执行事件监听,如果第一个参数是true则运行事件 50 | func FireAsyncIf(ctx context.Context, condition bool, event *Event) { 51 | if condition { 52 | FireAsync(ctx, event) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /internal/eventbus/util.go: -------------------------------------------------------------------------------- 1 | package eventbus 2 | 3 | import "strings" 4 | 5 | func eventName(s string) string { 6 | name := strings.TrimSpace(s) 7 | if name == "" { 8 | panic("event: the event name cannot be empty") 9 | } 10 | return name 11 | } 12 | -------------------------------------------------------------------------------- /internal/file/file.go: -------------------------------------------------------------------------------- 1 | package filex 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "gopkg.in/yaml.v3" 8 | ) 9 | 10 | func MustLoad(filename string, v any) error { 11 | content, err := os.ReadFile(filename) 12 | 13 | if err != nil { 14 | return fmt.Errorf("read file error," + err.Error()) 15 | } 16 | err = yaml.Unmarshal(content, v) 17 | 18 | if err != nil { 19 | return fmt.Errorf("parse config content error:" + err.Error()) 20 | } 21 | return nil 22 | } 23 | -------------------------------------------------------------------------------- /internal/g/g.go: -------------------------------------------------------------------------------- 1 | package g 2 | 3 | type ( 4 | M = map[string]any 5 | Map = map[string]any // Map is alias of frequently-used map type map[string]any. 6 | MapAnyAny = map[any]any // MapAnyAny is alias of frequently-used map type map[any]any. 7 | MapAnyStr = map[any]string // MapAnyStr is alias of frequently-used map type map[any]string. 8 | MapAnyInt = map[any]int // MapAnyInt is alias of frequently-used map type map[any]int. 9 | MapStrAny = map[string]any // MapStrAny is alias of frequently-used map type map[string]any. 10 | MapStrStr = map[string]string // MapStrStr is alias of frequently-used map type map[string]string. 11 | MapStrInt = map[string]int // MapStrInt is alias of frequently-used map type map[string]int. 12 | MapIntAny = map[int]any // MapIntAny is alias of frequently-used map type map[int]any. 13 | MapIntStr = map[int]string // MapIntStr is alias of frequently-used map type map[int]string. 14 | MapIntInt = map[int]int // MapIntInt is alias of frequently-used map type map[int]int. 15 | MapAnyBool = map[any]bool // MapAnyBool is alias of frequently-used map type map[any]bool. 16 | MapStrBool = map[string]bool // MapStrBool is alias of frequently-used map type map[string]bool. 17 | MapIntBool = map[int]bool // MapIntBool is alias of frequently-used map type map[int]bool. 18 | ) 19 | 20 | type ( 21 | List = []Map // List is alias of frequently-used slice type []Map. 22 | ListAnyAny = []MapAnyAny // ListAnyAny is alias of frequently-used slice type []MapAnyAny. 23 | ListAnyStr = []MapAnyStr // ListAnyStr is alias of frequently-used slice type []MapAnyStr. 24 | ListAnyInt = []MapAnyInt // ListAnyInt is alias of frequently-used slice type []MapAnyInt. 25 | ListStrAny = []MapStrAny // ListStrAny is alias of frequently-used slice type []MapStrAny. 26 | ListStrStr = []MapStrStr // ListStrStr is alias of frequently-used slice type []MapStrStr. 27 | ListStrInt = []MapStrInt // ListStrInt is alias of frequently-used slice type []MapStrInt. 28 | ListIntAny = []MapIntAny // ListIntAny is alias of frequently-used slice type []MapIntAny. 29 | ListIntStr = []MapIntStr // ListIntStr is alias of frequently-used slice type []MapIntStr. 30 | ListIntInt = []MapIntInt // ListIntInt is alias of frequently-used slice type []MapIntInt. 31 | ListAnyBool = []MapAnyBool // ListAnyBool is alias of frequently-used slice type []MapAnyBool. 32 | ListStrBool = []MapStrBool // ListStrBool is alias of frequently-used slice type []MapStrBool. 33 | ListIntBool = []MapIntBool // ListIntBool is alias of frequently-used slice type []MapIntBool. 34 | ) 35 | 36 | type ( 37 | Slice = []any // Slice is alias of frequently-used slice type []any. 38 | SliceAny = []any // SliceAny is alias of frequently-used slice type []any. 39 | SliceStr = []string // SliceStr is alias of frequently-used slice type []string. 40 | SliceInt = []int // SliceInt is alias of frequently-used slice type []int. 41 | ) 42 | 43 | type ( 44 | Array = []any // Array is alias of frequently-used slice type []any. 45 | ArrayAny = []any // ArrayAny is alias of frequently-used slice type []any. 46 | ArrayStr = []string // ArrayStr is alias of frequently-used slice type []string. 47 | ArrayInt = []int // ArrayInt is alias of frequently-used slice type []int. 48 | ) 49 | 50 | // Scalar 接口用于限制参数为标量类型或基于标量的自定义类型 51 | type Scalar interface { 52 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | 53 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | 54 | ~float32 | ~float64 | ~bool | ~string // 允许基于标量类型的自定义类型 55 | } 56 | 57 | type IntScalar interface { 58 | ~int | ~int8 | ~int16 | ~int32 | ~int64 | 59 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 60 | } 61 | -------------------------------------------------------------------------------- /internal/httpc/client.go: -------------------------------------------------------------------------------- 1 | package httpc 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-resty/resty/v2" 7 | ) 8 | 9 | type Client struct { 10 | base *resty.Client 11 | } 12 | 13 | func (c *Client) SetBaseURL(url string) *Client { 14 | c.base.SetBaseURL(url) 15 | return c 16 | } 17 | 18 | func (c *Client) SetTimeout(timeout time.Duration) *Client { 19 | c.base.SetTimeout(timeout) 20 | return c 21 | } 22 | 23 | func (c *Client) NewRequest() *Request { 24 | return &Request{ 25 | base: c.base.NewRequest(), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/httpc/hook.go: -------------------------------------------------------------------------------- 1 | package httpc 2 | 3 | import ( 4 | "context" 5 | "go-gin/internal/component/logx" 6 | "net/http" 7 | 8 | "github.com/go-resty/resty/v2" 9 | ) 10 | 11 | func LogBeforeRequest(c *resty.Client, r *resty.Request) error { 12 | logx.RestyLoggerInstance.Info().Ctx(r.Context()). 13 | Str("keywords", "request"). 14 | Str("url", c.BaseURL+r.URL). 15 | Str("method", r.Method). 16 | Any("header", r.Header). 17 | Str("query", r.QueryParam.Encode()). 18 | Any("post", r.FormData.Encode()). 19 | Any("body", r.Body). 20 | Send() 21 | return nil 22 | } 23 | 24 | func LogErrorHook(r *resty.Request, err error) { 25 | if responseErr, ok := err.(*resty.ResponseError); ok { 26 | LogResponse(r.Context(), responseErr.Response) 27 | } 28 | logx.RestyLoggerInstance.Info().Ctx(r.Context()). 29 | Str("keywords", "error hook"). 30 | Str("msg", err.Error()). 31 | Send() 32 | } 33 | 34 | func LogSuccessHook(c *resty.Client, r *resty.Response) { 35 | LogResponse(r.Request.Context(), r) 36 | } 37 | 38 | func LogResponse(ctx context.Context, r *resty.Response) { 39 | e := logx.RestyLoggerInstance. 40 | Info(). 41 | Ctx(ctx). 42 | Str("keywords", "response"). 43 | Str("body", r.String()) 44 | 45 | if r.StatusCode() != http.StatusOK { 46 | e = e.Int("status", r.StatusCode()) 47 | } 48 | if r.Error() != nil { 49 | e = e.Any("error", r.Error()) 50 | } 51 | e.Send() 52 | } 53 | -------------------------------------------------------------------------------- /internal/httpc/httpc.go: -------------------------------------------------------------------------------- 1 | package httpc 2 | 3 | import ( 4 | "context" 5 | "go-gin/internal/errorx" 6 | 7 | "github.com/go-resty/resty/v2" 8 | ) 9 | 10 | type Request struct { 11 | base *resty.Request 12 | } 13 | 14 | func (r *Request) SetHeader(header, value string) *Request { 15 | r.base.SetHeader(header, value) 16 | return r 17 | } 18 | 19 | func (r *Request) SetHeaders(headers map[string]string) *Request { 20 | r.base.SetHeaders(headers) 21 | return r 22 | } 23 | 24 | func (r *Request) SetHeaderMultiValues(headers map[string][]string) *Request { 25 | r.base.SetHeaderMultiValues(headers) 26 | return r 27 | } 28 | 29 | func (r *Request) SetQueryString(query string) *Request { 30 | r.base.SetQueryString(query) 31 | return r 32 | } 33 | 34 | func (r *Request) SetQueryParam(param, value string) *Request { 35 | r.base.SetQueryParam(param, value) 36 | return r 37 | } 38 | 39 | func (r *Request) SetQueryParams(params map[string]string) *Request { 40 | r.base.SetQueryParams(params) 41 | return r 42 | } 43 | 44 | func (r *Request) SetFormData(data map[string]string) *Request { 45 | r.base.SetFormData(data) 46 | return r 47 | } 48 | 49 | func (r *Request) AddFormData(key string, data []string) *Request { 50 | for _, v := range data { 51 | r.base.FormData.Add(key, v) 52 | } 53 | return r 54 | } 55 | 56 | func (r *Request) SetBody(body any) *Request { 57 | r.base.SetBody(body) 58 | return r 59 | } 60 | 61 | func (r *Request) SetResult(res any) *Request { 62 | r.base.SetResult(res) 63 | return r 64 | } 65 | 66 | func (r *Request) SetContext(ctx context.Context) *Request { 67 | r.base.SetContext(ctx) 68 | return r 69 | } 70 | 71 | func (r *Request) GET(url string) *Request { 72 | r.base.Method = resty.MethodGet 73 | r.base.URL = url 74 | return r 75 | } 76 | 77 | func (r *Request) POST(url string) *Request { 78 | r.base.Method = resty.MethodPost 79 | r.base.URL = url 80 | return r 81 | } 82 | 83 | func (r *Request) Send() (*resty.Response, error) { 84 | return r.base.Send() 85 | } 86 | 87 | func (r *Request) Exec() error { 88 | resp, err := r.base.Send() 89 | if err != nil { 90 | return errorx.ErrThirdAPIConnectFailed 91 | } 92 | if resp.String() == "" { 93 | return errorx.ErrThirdAPIContentNoContentFailed 94 | } 95 | if ret, ok := r.base.Result.(IBaseResponse); ok { 96 | if err := ret.Parse([]byte(resp.String())); err != nil { 97 | return errorx.ErrThirdAPIContentParseFailed 98 | } 99 | 100 | if !ret.Valid() { 101 | return errorx.ErrThirdAPICallFormatFailed 102 | } 103 | if !ret.IsSuccess() { 104 | return errorx.ErrThirdAPIBusinessFailed 105 | } 106 | switch res := r.base.Result.(type) { 107 | case IRepsonseNonStardard: 108 | if err := res.ParseData([]byte(resp.String())); err != nil { 109 | return errorx.ErrThirdAPIDataParseFailed 110 | } 111 | case IResponse: 112 | if err := res.ParseData(); err != nil { 113 | return errorx.ErrThirdAPIDataParseFailed 114 | } 115 | } 116 | } 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /internal/httpc/svc.go: -------------------------------------------------------------------------------- 1 | package httpc 2 | 3 | type IBaseResponse interface { 4 | Parse([]byte) error 5 | Valid() bool 6 | IsSuccess() bool 7 | Msg() string 8 | } 9 | 10 | type IResponse interface { 11 | IBaseResponse 12 | ParseData() error 13 | } 14 | 15 | type IRepsonseNonStardard interface { 16 | IBaseResponse 17 | ParseData([]byte) error 18 | } 19 | 20 | type BaseSvc struct { 21 | client *Client 22 | } 23 | 24 | func NewBaseSvc(url string) *BaseSvc { 25 | return &BaseSvc{ 26 | client: NewClient().SetBaseURL(url), 27 | } 28 | } 29 | 30 | func (b *BaseSvc) Client() *Client { 31 | return b.client 32 | } 33 | -------------------------------------------------------------------------------- /internal/httpc/util.go: -------------------------------------------------------------------------------- 1 | package httpc 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/go-resty/resty/v2" 8 | ) 9 | 10 | type M map[string]string 11 | 12 | func NewClient() *Client { 13 | client := &Client{ 14 | base: resty.New(), 15 | } 16 | client.SetTimeout(3 * time.Minute) 17 | 18 | client.base.OnBeforeRequest(LogBeforeRequest) 19 | client.base.OnError(LogErrorHook) 20 | client.base.OnSuccess(LogSuccessHook) 21 | client.base.OnPanic(LogErrorHook) 22 | client.base.OnInvalid(LogErrorHook) 23 | return client 24 | } 25 | 26 | func GET(ctx context.Context, url string) *Request { 27 | return NewClient(). 28 | NewRequest(). 29 | GET(url). 30 | SetContext(ctx) 31 | } 32 | 33 | func POST(ctx context.Context, url string) *Request { 34 | return NewClient(). 35 | NewRequest(). 36 | POST(url). 37 | SetContext(ctx) 38 | } 39 | -------------------------------------------------------------------------------- /internal/httpx/context.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Manu Martinez-Almeida. All rights reserved. 2 | // Use of this source code is governed by a MIT style 3 | // license that can be found in the LICENSE file. 4 | 5 | package httpx 6 | 7 | import ( 8 | "context" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | // Context 自定义Context,扩展gin.Context 14 | type Context struct { 15 | *gin.Context // 继承gin.Context 16 | } 17 | 18 | var _ context.Context = &Context{} 19 | 20 | // NewContext 创建自定义Context 21 | func NewContext(c *gin.Context) *Context { 22 | return &Context{ 23 | c, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/httpx/db_check.go: -------------------------------------------------------------------------------- 1 | package httpx 2 | 3 | import ( 4 | "go-gin/internal/component/db" 5 | "go-gin/internal/component/logx" 6 | "go-gin/internal/errorx" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func dbCheck() gin.HandlerFunc { 12 | return func(ctx *gin.Context) { 13 | if !db.IsConnected() { 14 | err := db.Connect() 15 | if err != nil { 16 | logx.WithContext(ctx).Error("connect db", err.Error()) 17 | Error(NewContext(ctx), errorx.ErrInternalServerError) 18 | ctx.Abort() 19 | } 20 | } 21 | ctx.Next() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/httpx/debug.go: -------------------------------------------------------------------------------- 1 | package httpx 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | "reflect" 7 | "runtime" 8 | "strings" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | func init() { 14 | gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) { 15 | } 16 | } 17 | 18 | func debugPrintRoute(httpMethod, absolutePath string, nuMiddlewares int, handlers ...HandlerFunc) { 19 | if gin.IsDebugging() { 20 | nuHandlers := len(handlers) 21 | if nuHandlers == 0 { 22 | return 23 | } 24 | handlerName := nameOfFunction(handlers[len(handlers)-1]) 25 | debugPrint("%-6s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuMiddlewares+nuHandlers) 26 | 27 | } 28 | } 29 | 30 | func combineHandlers(handlers ...HandlerFunc) []HandlerFunc { 31 | finalSize := len(handlers) 32 | mergedHandlers := make([]HandlerFunc, finalSize) 33 | copy(mergedHandlers, handlers) 34 | return mergedHandlers 35 | } 36 | 37 | func calculateAbsolutePath(basePath, relativePath string) string { 38 | return joinPaths(basePath, relativePath) 39 | } 40 | 41 | func joinPaths(absolutePath, relativePath string) string { 42 | if relativePath == "" { 43 | return absolutePath 44 | } 45 | 46 | finalPath := path.Join(absolutePath, relativePath) 47 | if lastChar(relativePath) == '/' && lastChar(finalPath) != '/' { 48 | return finalPath + "/" 49 | } 50 | return finalPath 51 | } 52 | 53 | func lastChar(str string) uint8 { 54 | if str == "" { 55 | panic("The length of the string can't be 0") 56 | } 57 | return str[len(str)-1] 58 | } 59 | 60 | func nameOfFunction(f any) string { 61 | fmt.Println(runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name()) 62 | return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() 63 | } 64 | 65 | func debugPrint(format string, values ...any) { 66 | if gin.IsDebugging() { 67 | if !strings.HasSuffix(format, "\n") { 68 | format += "\n" 69 | } 70 | fmt.Fprintf(gin.DefaultWriter, "[GIN-debug] "+format, values...) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/httpx/engine.go: -------------------------------------------------------------------------------- 1 | package httpx 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // Engine 包装 gin.Engine 10 | type Engine struct { 11 | *gin.Engine 12 | RouterGroup // 继承 RouterGroup 13 | } 14 | 15 | // New 返回一个新的 Engine 实例 16 | func New() *Engine { 17 | engine := &Engine{ 18 | Engine: gin.New(), 19 | } 20 | engine.RouterGroup = *NewRouterGroup(&engine.Engine.RouterGroup) 21 | engine.HandleMethodNotAllowed = true 22 | return engine 23 | } 24 | 25 | // Default 返回一个带有默认中间件的 Engine 实例 26 | func Default() *Engine { 27 | engine := New() 28 | engine.Use(recoverLog(), TraceId(), RequestLog(), dbCheck()) 29 | return engine 30 | } 31 | 32 | // NoRoute 添加 404 处理器 33 | func (engine *Engine) NoRoute(handlers ...HandlerFunc) { 34 | engine.Engine.NoRoute(wrapHandlers(handlers)...) 35 | } 36 | 37 | // NoMethod 添加 405 处理器 38 | func (engine *Engine) NoMethod(handlers ...HandlerFunc) { 39 | engine.Engine.NoMethod(wrapHandlers(handlers)...) 40 | } 41 | 42 | // Group 创建一个新的路由组 43 | func (engine *Engine) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { 44 | return engine.RouterGroup.Group(relativePath, handlers...) 45 | } 46 | 47 | // Use 添加全局中间件 48 | func (engine *Engine) Use(middleware ...gin.HandlerFunc) IRoutes { 49 | engine.RouterGroup.Use(middleware...) 50 | return engine 51 | } 52 | 53 | // Before 添加前置中间件 54 | func (engine *Engine) Before(middleware ...HandlerFunc) IRoutes { 55 | engine.RouterGroup.Before(middleware...) 56 | return engine 57 | } 58 | 59 | // After 添加后置中间件 60 | func (engine *Engine) After(middleware ...HandlerFunc) IRoutes { 61 | engine.RouterGroup.After(middleware...) 62 | return engine 63 | } 64 | 65 | // Routes 返回已注册的路由 66 | func (engine *Engine) Routes() (routes gin.RoutesInfo) { 67 | return engine.Engine.Routes() 68 | } 69 | 70 | // Run 启动 HTTP 服务器 71 | func (engine *Engine) Run(addr ...string) (err error) { 72 | return engine.Engine.Run(addr...) 73 | } 74 | 75 | // RunTLS 启动 HTTPS 服务器 76 | func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) { 77 | return engine.Engine.RunTLS(addr, certFile, keyFile) 78 | } 79 | 80 | // ServeHTTP 实现 http.Handler 接口 81 | func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { 82 | engine.Engine.ServeHTTP(w, req) 83 | } 84 | -------------------------------------------------------------------------------- /internal/httpx/handler.go: -------------------------------------------------------------------------------- 1 | package httpx 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "go-gin/internal/component/logx" 7 | "io" 8 | 9 | "github.com/gin-gonic/gin/binding" 10 | ) 11 | 12 | // LogicHandler 处理请求的逻辑接口 13 | type LogicHandler[Req any, Resp any] interface { 14 | Handle(ctx context.Context, req Req) (Resp, error) 15 | } 16 | 17 | // ShouldBindHandle 处理请求 18 | func ShouldBindHandle[Req any, Resp any](c *Context, logicHandler LogicHandler[Req, Resp]) (Resp, error) { 19 | b := binding.Default(c.Request.Method, c.ContentType()) 20 | return ShouldBindWithHandle(c, logicHandler, b) 21 | } 22 | 23 | // ShouldBindJSONHandle 处理请求 24 | func ShouldBindJSONHandle[Req any, Resp any](c *Context, logicHandler LogicHandler[Req, Resp]) (Resp, error) { 25 | return ShouldBindWithHandle(c, logicHandler, binding.JSON) 26 | } 27 | 28 | // ShouldBindQueryHandle 处理请求 29 | func ShouldBindQueryHandle[Req any, Resp any](ctx *Context, logicHandler LogicHandler[Req, Resp]) (Resp, error) { 30 | return ShouldBindWithHandle(ctx, logicHandler, binding.Query) 31 | } 32 | 33 | // ShouldBindHeaderHandle 处理请求 34 | func ShouldBindHeaderHandle[Req any, Resp any](ctx *Context, logicHandler LogicHandler[Req, Resp]) (Resp, error) { 35 | return ShouldBindWithHandle(ctx, logicHandler, binding.Header) 36 | } 37 | 38 | // ShouldBindUriHandle 处理请求 39 | func ShouldBindUriHandle[Req any, Resp any](ctx *Context, logicHandler LogicHandler[Req, Resp]) (Resp, error) { 40 | var req Req 41 | var resp Resp 42 | if err := ctx.ShouldBindUri(&req); err != nil { 43 | if errors.Is(err, io.EOF) { 44 | logx.WithContext(ctx).Warn("ShouldBindUri异常", "io.EOF错误") 45 | return resp, err 46 | } 47 | logx.WithContext(ctx).Warn("ShouldBindUri异常", err) 48 | return resp, err 49 | } 50 | return logicHandler.Handle(ctx, req) 51 | } 52 | 53 | // ShouldBindWithHandle 处理请求 54 | func ShouldBindWithHandle[Req any, Resp any](ctx *Context, logicHandler LogicHandler[Req, Resp], b binding.Binding) (Resp, error) { 55 | var req Req 56 | var resp Resp 57 | if err := ctx.ShouldBindWith(&req, b); err != nil { 58 | if errors.Is(err, io.EOF) { 59 | logx.WithContext(ctx).Warn("ShouldBind异常", "io.EOF错误") 60 | return resp, err 61 | } 62 | logx.WithContext(ctx).Warn("ShouldBind异常", err.Error()) 63 | return resp, err 64 | } 65 | return logicHandler.Handle(ctx, req) 66 | } 67 | -------------------------------------------------------------------------------- /internal/httpx/method.go: -------------------------------------------------------------------------------- 1 | package httpx 2 | 3 | type HttpMethod string 4 | 5 | const ( 6 | MethodGet HttpMethod = "GET" 7 | MethodHead HttpMethod = "HEAD" 8 | MethodPost HttpMethod = "POST" 9 | MethodPut HttpMethod = "PUT" 10 | MethodPatch HttpMethod = "PATCH" // RFC 5789 11 | MethodDelete HttpMethod = "DELETE" 12 | MethodOptions HttpMethod = "OPTIONS" 13 | ) 14 | -------------------------------------------------------------------------------- /internal/httpx/mode.go: -------------------------------------------------------------------------------- 1 | package httpx 2 | 3 | import "github.com/gin-gonic/gin" 4 | 5 | func SetDebugMode() { 6 | gin.SetMode(gin.DebugMode) 7 | } 8 | 9 | func SetReleaseMode() { 10 | gin.SetMode(gin.ReleaseMode) 11 | } 12 | -------------------------------------------------------------------------------- /internal/httpx/recover_log.go: -------------------------------------------------------------------------------- 1 | package httpx 2 | 3 | import ( 4 | "fmt" 5 | "go-gin/internal/component/logx" 6 | "go-gin/internal/errorx" 7 | "go-gin/internal/util" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | func recoverLog() gin.HandlerFunc { 13 | return func(ctx *gin.Context) { 14 | 15 | defer func() { 16 | if r := recover(); r != nil { 17 | m := map[string]any{ 18 | "error": fmt.Sprintf("%v", r), 19 | "file": util.FileWithLineNum(), 20 | } 21 | logx.WithContext(ctx).Error("panic", m) 22 | fmt.Println(m) 23 | Error(NewContext(ctx), errorx.ErrInternalServerError) 24 | ctx.Abort() 25 | } 26 | }() 27 | 28 | ctx.Next() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/httpx/request_log.go: -------------------------------------------------------------------------------- 1 | package httpx 2 | 3 | import ( 4 | "go-gin/internal/component/logx" 5 | "net/url" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func RequestLog() gin.HandlerFunc { 12 | 13 | return func(c *gin.Context) { 14 | // Start timer 15 | start := time.Now() 16 | path := c.Request.URL.Path 17 | rawQuery := c.Request.URL.RawQuery 18 | raw, _ := url.QueryUnescape(rawQuery) 19 | // Process request 20 | c.Next() 21 | 22 | if raw != "" { 23 | path = path + "?" + raw 24 | } 25 | TimeStamp := time.Now() 26 | Cost := TimeStamp.Sub(start) 27 | if Cost > time.Minute { 28 | Cost = Cost.Truncate(time.Second) 29 | } 30 | 31 | logx.AccessLoggerInstance.Info().Ctx(c). 32 | Str("path", path). 33 | Str("method", c.Request.Method). 34 | Str("ip", c.ClientIP()). 35 | Str("cost", Cost.String()). 36 | Int("status", c.Writer.Status()). 37 | Str("proto", c.Request.Proto). 38 | Str("user_agent", c.Request.UserAgent()). 39 | Send() 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/httpx/response.go: -------------------------------------------------------------------------------- 1 | package httpx 2 | 3 | import ( 4 | "context" 5 | "go-gin/internal/environment" 6 | "go-gin/internal/errorx" 7 | "go-gin/internal/traceid" 8 | "net/http" 9 | ) 10 | 11 | var ( 12 | CodeFieldName = "code" 13 | ResultFieldName = "data" 14 | MessageFieldName = "message" 15 | ) 16 | 17 | var DefaultSuccessCodeValue = http.StatusOK 18 | var DefaultSuccessMessageValue = "操作成功" 19 | 20 | type Result struct { 21 | Code int 22 | Message string 23 | Data any 24 | TraceId string 25 | } 26 | 27 | func Ok(ctx *Context, data any) { 28 | result := Result{ 29 | Code: DefaultSuccessCodeValue, 30 | Data: data, 31 | Message: DefaultSuccessMessageValue, 32 | } 33 | ctx.JSON(http.StatusOK, transform(ctx, result)) 34 | } 35 | 36 | func Error(ctx *Context, err error) { 37 | var httpStatus int 38 | var code int 39 | var message string 40 | 41 | switch e := err.(type) { 42 | case errorx.ServerError: 43 | code = e.Code 44 | httpStatus = e.Code 45 | message = e.Msg 46 | case errorx.BizError: 47 | message = e.Msg 48 | httpStatus = http.StatusOK 49 | code = e.Code 50 | case errorx.RedisError: 51 | if environment.IsDebugMode() { 52 | message = e.Error() 53 | } else { 54 | message = "服务器内部错误" 55 | } 56 | httpStatus = http.StatusInternalServerError 57 | code = errorx.ErrCodeRedisOperateFailed 58 | case errorx.DBError: 59 | if environment.IsDebugMode() { 60 | message = e.Error() 61 | } else { 62 | message = "服务器内部错误" 63 | } 64 | httpStatus = http.StatusInternalServerError 65 | code = errorx.ErrCodeDBOperateFailed 66 | case error: 67 | httpStatus = http.StatusOK 68 | code = errorx.ErrCodeDefault 69 | message = err.Error() 70 | } 71 | result := Result{ 72 | Code: code, 73 | Message: message, 74 | } 75 | ctx.JSON(httpStatus, transform(ctx, result)) 76 | } 77 | 78 | func Handle(ctx *Context, data any, err error) { 79 | if err != nil { 80 | Error(ctx, err) 81 | } else { 82 | Ok(ctx, data) 83 | } 84 | } 85 | 86 | func transform(ctx context.Context, result Result) map[string]any { 87 | s, _ := ctx.Value(traceid.TraceIdFieldName).(string) 88 | 89 | return map[string]any{ 90 | CodeFieldName: result.Code, 91 | MessageFieldName: result.Message, 92 | ResultFieldName: result.Data, 93 | traceid.TraceIdFieldName: s, 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /internal/httpx/routergroup.go: -------------------------------------------------------------------------------- 1 | package httpx 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | type HandlerFunc func(*Context) (any, error) 8 | 9 | // IRouter 定义路由接口 10 | type IRouter interface { 11 | IRoutes 12 | Group(string, ...HandlerFunc) *RouterGroup 13 | } 14 | 15 | // IRoutes 定义路由方法接口 16 | type IRoutes interface { 17 | Use(...gin.HandlerFunc) IRoutes 18 | Before(...HandlerFunc) IRoutes 19 | After(...HandlerFunc) IRoutes 20 | Handle(HttpMethod, string, ...HandlerFunc) IRoutes 21 | Any(string, ...HandlerFunc) IRoutes 22 | GET(string, ...HandlerFunc) IRoutes 23 | POST(string, ...HandlerFunc) IRoutes 24 | DELETE(string, ...HandlerFunc) IRoutes 25 | PATCH(string, ...HandlerFunc) IRoutes 26 | PUT(string, ...HandlerFunc) IRoutes 27 | OPTIONS(string, ...HandlerFunc) IRoutes 28 | HEAD(string, ...HandlerFunc) IRoutes 29 | Match([]HttpMethod, string, ...HandlerFunc) IRoutes 30 | } 31 | 32 | // RouterGroup 包装 gin.RouterGroup 33 | type RouterGroup struct { 34 | *gin.RouterGroup 35 | } 36 | 37 | // NewRouterGroup 创建包装后的路由组 38 | func NewRouterGroup(group *gin.RouterGroup) *RouterGroup { 39 | return &RouterGroup{ 40 | group, 41 | } 42 | } 43 | 44 | // Group 创建新的路由组 45 | func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { 46 | return &RouterGroup{ 47 | group.RouterGroup.Group(relativePath, wrapHandlers(handlers)...), 48 | } 49 | } 50 | 51 | // Use 添加中间件 52 | func (group *RouterGroup) Use(middleware ...gin.HandlerFunc) IRoutes { 53 | group.RouterGroup.Use(middleware...) 54 | return group 55 | } 56 | 57 | // Before 添加前置中间件 58 | func (group *RouterGroup) Before(middleware ...HandlerFunc) IRoutes { 59 | wrapped := make([]gin.HandlerFunc, len(middleware)) 60 | for i, h := range middleware { 61 | wrapped[i] = func(m HandlerFunc) gin.HandlerFunc { 62 | return func(c *gin.Context) { 63 | ctx := NewContext(c) 64 | _, err := m(ctx) 65 | if err != nil { 66 | Handle(ctx, nil, err) 67 | c.Abort() 68 | return 69 | } 70 | c.Next() 71 | } 72 | }(h) 73 | } 74 | group.RouterGroup.Use(wrapped...) 75 | return group 76 | } 77 | 78 | // After 添加后置中间件 79 | func (group *RouterGroup) After(middleware ...HandlerFunc) IRoutes { 80 | wrapped := make([]gin.HandlerFunc, len(middleware)) 81 | for i, h := range middleware { 82 | wrapped[i] = func(m HandlerFunc) gin.HandlerFunc { 83 | return func(c *gin.Context) { 84 | c.Next() 85 | m(NewContext(c)) 86 | } 87 | }(h) 88 | } 89 | group.RouterGroup.Use(wrapped...) 90 | return group 91 | } 92 | 93 | // Handle 注册请求处理函数 94 | func (group *RouterGroup) Handle(httpMethod HttpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { 95 | group.RouterGroup.Handle(string(httpMethod), relativePath, wrapHandlers(handlers)...) 96 | debugPrintRoute(string(httpMethod), calculateAbsolutePath(group.BasePath(), relativePath), len(group.Handlers), combineHandlers(handlers...)...) 97 | return group 98 | } 99 | 100 | // GET 处理 GET 请求 101 | func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { 102 | return group.Handle(MethodGet, relativePath, handlers...) 103 | } 104 | 105 | // POST 处理 POST 请求 106 | func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes { 107 | return group.Handle(MethodPost, relativePath, handlers...) 108 | } 109 | 110 | // DELETE 处理 DELETE 请求 111 | func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes { 112 | return group.Handle(MethodDelete, relativePath, handlers...) 113 | } 114 | 115 | // PATCH 处理 PATCH 请求 116 | func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) IRoutes { 117 | return group.Handle(MethodPatch, relativePath, handlers...) 118 | } 119 | 120 | // PUT 处理 PUT 请求 121 | func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) IRoutes { 122 | return group.Handle(MethodPut, relativePath, handlers...) 123 | } 124 | 125 | // OPTIONS 处理 OPTIONS 请求 126 | func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) IRoutes { 127 | return group.Handle(MethodOptions, relativePath, handlers...) 128 | } 129 | 130 | // HEAD 处理 HEAD 请求 131 | func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes { 132 | return group.Handle(MethodHead, relativePath, handlers...) 133 | } 134 | 135 | // Match 注册多个请求方法的处理函数 136 | func (group *RouterGroup) Match(methods []HttpMethod, relativePath string, handlers ...HandlerFunc) IRoutes { 137 | for _, method := range methods { 138 | group.Handle(method, relativePath, handlers...) 139 | } 140 | return group 141 | } 142 | 143 | // Any 处理任意 HTTP 方法 144 | func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes { 145 | group.GET(relativePath, handlers...) 146 | group.POST(relativePath, handlers...) 147 | group.PUT(relativePath, handlers...) 148 | group.PATCH(relativePath, handlers...) 149 | group.HEAD(relativePath, handlers...) 150 | group.OPTIONS(relativePath, handlers...) 151 | group.DELETE(relativePath, handlers...) 152 | return group 153 | } 154 | 155 | // wrap 包装处理函数 156 | func wrapHandlers(handler []HandlerFunc) []gin.HandlerFunc { 157 | wrapped := make([]gin.HandlerFunc, len(handler)) 158 | for i, h := range handler { 159 | wrapped[i] = func(h HandlerFunc) gin.HandlerFunc { 160 | return func(c *gin.Context) { 161 | ctx := NewContext(c) 162 | resp, err := h(ctx) 163 | Handle(ctx, resp, err) 164 | } 165 | }(h) 166 | } 167 | return wrapped 168 | } 169 | -------------------------------------------------------------------------------- /internal/httpx/traceId.go: -------------------------------------------------------------------------------- 1 | package httpx 2 | 3 | import ( 4 | "go-gin/internal/traceid" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | func TraceId() gin.HandlerFunc { 10 | return func(ctx *gin.Context) { 11 | ctx.Set(traceid.TraceIdFieldName, traceid.New()) 12 | ctx.Next() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /internal/httpx/validators/default_validator.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | // Copyright 2017 Manu Martinez-Almeida. All rights reserved. 4 | // Use of this source code is governed by a MIT style 5 | // license that can be found in the LICENSE file. 6 | 7 | import ( 8 | "go-gin/internal/errorx" 9 | "reflect" 10 | "strings" 11 | "sync" 12 | 13 | "github.com/gin-gonic/gin/binding" 14 | "github.com/go-playground/locales/zh" 15 | ut "github.com/go-playground/universal-translator" 16 | "github.com/go-playground/validator/v10" 17 | zhTrans "github.com/go-playground/validator/v10/translations/zh" 18 | "golang.org/x/text/language" 19 | ) 20 | 21 | type defaultValidator struct { 22 | once sync.Once 23 | validate *validator.Validate 24 | trans ut.Translator 25 | } 26 | 27 | var _ binding.StructValidator = (*defaultValidator)(nil) 28 | 29 | // ValidateStruct receives any kind of type, but only performed struct or pointer to struct type. 30 | func (v *defaultValidator) ValidateStruct(obj any) error { 31 | if obj == nil { 32 | return nil 33 | } 34 | 35 | value := reflect.ValueOf(obj) 36 | switch value.Kind() { 37 | case reflect.Ptr: 38 | return v.ValidateStruct(value.Elem().Interface()) 39 | case reflect.Struct: 40 | return v.validateStruct(obj) 41 | case reflect.Slice, reflect.Array: 42 | count := value.Len() 43 | validateRet := make(binding.SliceValidationError, 0) 44 | for i := 0; i < count; i++ { 45 | if err := v.ValidateStruct(value.Index(i).Interface()); err != nil { 46 | validateRet = append(validateRet, err) 47 | } 48 | } 49 | if len(validateRet) == 0 { 50 | return nil 51 | } 52 | return validateRet 53 | default: 54 | return nil 55 | } 56 | } 57 | 58 | // validateStruct receives struct type 59 | func (v *defaultValidator) validateStruct(obj any) error { 60 | v.lazyinit() 61 | err := v.validate.Struct(obj) 62 | if err == nil { 63 | return err 64 | } 65 | errs, ok := err.(validator.ValidationErrors) 66 | if !ok { 67 | return err 68 | } 69 | // returns a map with key = namespace & value = translated error 70 | // NOTICE: 2 errors are returned and you'll see something surprising 71 | // translations are i18n aware!!!! 72 | // eg. '10 characters' vs '1 character' 73 | return v.formatValidationErrors(errs) 74 | } 75 | 76 | // Engine returns the underlying validator engine which powers the default 77 | // Validator instance. This is useful if you want to register custom validations 78 | // or struct level validations. See validator GoDoc for more info - 79 | // https://pkg.go.dev/github.com/go-playground/validator/v10 80 | func (v *defaultValidator) Engine() any { 81 | v.lazyinit() 82 | return v.validate 83 | } 84 | 85 | func (v *defaultValidator) lazyinit() { 86 | v.once.Do(func() { 87 | v.validate = validator.New() 88 | v.validate.RegisterTagNameFunc(func(fld reflect.StructField) string { 89 | name := strings.SplitN(fld.Tag.Get("label"), ",", 2)[0] 90 | // skip if tag key says it should be ignored 91 | if name == "-" { 92 | return "" 93 | } 94 | return name 95 | }) 96 | v.trans = getZhTranslator() 97 | _ = zhTrans.RegisterDefaultTranslations(v.validate, v.trans) 98 | v.validate.SetTagName("binding") 99 | }) 100 | } 101 | 102 | // formatValidationErrors 格式化验证器返回的错误消息 103 | func (v *defaultValidator) formatValidationErrors(errs validator.ValidationErrors) error { 104 | var errorMessage string 105 | for _, e := range errs { 106 | errorMessage += e.Translate(v.trans) + "\n" 107 | } 108 | 109 | return errorx.New(errorx.ErrCodeValidateFailed, errorMessage) 110 | } 111 | 112 | // getZhTranslator 获取中文翻译器 113 | func getZhTranslator() ut.Translator { 114 | zh := zh.New() 115 | uni := ut.New(zh, zh) 116 | trans, _ := uni.GetTranslator(language.Chinese.String()) 117 | return trans 118 | } 119 | -------------------------------------------------------------------------------- /internal/httpx/validators/init.go: -------------------------------------------------------------------------------- 1 | package validators 2 | 3 | import ( 4 | "github.com/gin-gonic/gin/binding" 5 | ) 6 | 7 | func Init() { 8 | binding.Validator = &defaultValidator{} 9 | } 10 | 11 | func Validate(obj any) error { 12 | if err := binding.Validator.ValidateStruct(obj); err != nil { 13 | return err 14 | } 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /internal/migration/global.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "sync" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | var ( 10 | manager *Manager 11 | once sync.Once 12 | ) 13 | 14 | // GetManager 获取全局迁移管理器 15 | func GetManager() *Manager { 16 | once.Do(func() { 17 | manager = &Manager{ 18 | ddlMigrations: make(map[string]DDLMigration), 19 | dmlMigrations: make(map[string]DMLMigration), 20 | } 21 | }) 22 | return manager 23 | } 24 | 25 | // SetDB 设置数据库连接 26 | func SetDB(db *gorm.DB) { 27 | GetManager().db = db 28 | } 29 | -------------------------------------------------------------------------------- /internal/migration/interface.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | // DDLMigration DDL迁移接口 8 | type DDLMigration interface { 9 | Up(migrator *DDLMigrator) error 10 | } 11 | 12 | // DMLMigration DML迁移接口 13 | type DMLMigration interface { 14 | Handle(db *gorm.DB) error 15 | Desc() string 16 | } 17 | -------------------------------------------------------------------------------- /internal/migration/manager.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | "sort" 8 | "time" 9 | 10 | "github.com/labstack/gommon/color" 11 | "gorm.io/gorm" 12 | ) 13 | 14 | // Manager 迁移管理器 15 | type Manager struct { 16 | db *gorm.DB 17 | ddlMigrations map[string]DDLMigration 18 | dmlMigrations map[string]DMLMigration 19 | } 20 | 21 | // NewManager 创建迁移管理器 22 | func NewManager(db *gorm.DB) *Manager { 23 | return &Manager{ 24 | db: db, 25 | ddlMigrations: make(map[string]DDLMigration), 26 | dmlMigrations: make(map[string]DMLMigration), 27 | } 28 | } 29 | 30 | // 添加一个新的验证函数 31 | func validateMigrationName(name string) error { 32 | if len(name) < 14 { 33 | err := fmt.Errorf("migration name '%s' is too short, must end with YYYYMMDDHHMMSS format.", name) 34 | color.Printf(color.Red("Migration name validation failed: %v\n"), err) 35 | return err 36 | } 37 | 38 | timestamp := name[len(name)-14:] 39 | // 验证时间戳格式 40 | _, err := time.Parse("20060102150405", timestamp) 41 | if err != nil { 42 | err = fmt.Errorf("migration name '%s' must end with YYYYMMDDHHMMSS format.", name) 43 | color.Printf(color.Red("Migration name validation failed: %v\n"), err) 44 | return err 45 | } 46 | return nil 47 | } 48 | 49 | // RegisterDDL 注册DDL迁移 50 | func RegisterDDL(migration DDLMigration) error { 51 | name := reflect.TypeOf(migration).Elem().Name() 52 | if err := validateMigrationName(name); err != nil { 53 | os.Exit(0) // 使用 0 作为退出码,避免显示 exit status 1 54 | } 55 | GetManager().ddlMigrations[name] = migration 56 | return nil 57 | } 58 | 59 | // RegisterDML 注册DML迁移 60 | func RegisterDML(migration DMLMigration) error { 61 | name := reflect.TypeOf(migration).Elem().Name() 62 | if err := validateMigrationName(name); err != nil { 63 | os.Exit(0) // 使用 0 作为退出码,避免显示 exit status 1 64 | } 65 | GetManager().dmlMigrations[name] = migration 66 | return nil 67 | } 68 | 69 | // initMigrationTable 初始化迁移表 70 | func (m *Manager) initMigrationTable() error { 71 | if err := m.db.AutoMigrate(&Migration{}); err != nil { 72 | fmt.Printf(color.Red("failed to create migrations table: %v\n"), err) 73 | return fmt.Errorf("failed to create migrations table: %v\n", err) 74 | } 75 | return nil 76 | } 77 | 78 | // getExecutedMigrations 获取已执行的迁移 79 | func (m *Manager) getExecutedMigrations() (map[string]bool, error) { 80 | var executedMigrations []Migration 81 | if err := m.db.Find(&executedMigrations).Error; err != nil { 82 | fmt.Printf(color.Red("failed to get executed migrations: %v\n"), err) 83 | return nil, fmt.Errorf("failed to get executed migrations: %v\n", err) 84 | } 85 | 86 | executedMap := make(map[string]bool) 87 | for _, migration := range executedMigrations { 88 | executedMap[migration.Desc] = true 89 | } 90 | return executedMap, nil 91 | } 92 | 93 | // getCurrentBatch 获取当前批次号 94 | func (m *Manager) getCurrentBatch() (int, error) { 95 | var currentBatch int 96 | if err := m.db.Model(&Migration{}).Select("COALESCE(MAX(batch), 0)").Scan(¤tBatch).Error; err != nil { 97 | fmt.Printf(color.Red("failed to get current batch: %v\n"), err) 98 | return 0, fmt.Errorf("failed to get current batch: %v", err) 99 | } 100 | return currentBatch + 1, nil 101 | } 102 | 103 | // getSortedMigrationNames 获取排序后的迁移名称 104 | func (m *Manager) getSortedMigrationNames() (ddlNames []string, dmlNames []string) { 105 | // 提取 DDL 迁移名称 106 | for name := range m.ddlMigrations { 107 | ddlNames = append(ddlNames, name) 108 | } 109 | // 按照时间戳后缀排序(格式:YYYYMMDDHHMMSS) 110 | sort.Slice(ddlNames, func(i, j int) bool { 111 | // 获取最后14位作为时间戳进行比较 112 | iTime := ddlNames[i][len(ddlNames[i])-14:] 113 | jTime := ddlNames[j][len(ddlNames[j])-14:] 114 | return iTime < jTime 115 | }) 116 | 117 | // 提取 DML 迁移名称 118 | for name := range m.dmlMigrations { 119 | dmlNames = append(dmlNames, name) 120 | } 121 | // 按照时间戳后缀排序(格式:YYYYMMDDHHMMSS) 122 | sort.Slice(dmlNames, func(i, j int) bool { 123 | // 获取最后14位作为时间戳进行比较 124 | iTime := dmlNames[i][len(dmlNames[i])-14:] 125 | jTime := dmlNames[j][len(dmlNames[j])-14:] 126 | return iTime < jTime 127 | }) 128 | return 129 | } 130 | 131 | // recordMigration 记录迁移 132 | func (m *Manager) recordMigration(name string, batch int) error { 133 | record := Migration{ 134 | Desc: name, 135 | Batch: batch, 136 | CreatedAt: time.Now(), 137 | } 138 | if err := m.db.Create(&record).Error; err != nil { 139 | color.Printf(color.Red("Failed to save to migration table: %s, error: %v\n"), name, err) 140 | return err 141 | } 142 | return nil 143 | } 144 | 145 | // executeDDLMigrations 执行DDL迁移 146 | func (m *Manager) executeDDLMigrations(names []string, executedMap map[string]bool, batch int) error { 147 | ddlmigrator := NewDDLMigrator(m.db) 148 | for _, name := range names { 149 | if executedMap[name] { 150 | continue 151 | } 152 | 153 | migration := m.ddlMigrations[name] 154 | // 输出开始迁移 155 | color.Printf(color.White("Migrating: %s\n"), name) 156 | 157 | start := time.Now() 158 | if err := migration.Up(ddlmigrator); err != nil { 159 | color.Printf(color.Red("Failed: %s (%v)\n"), name, err) 160 | return err 161 | } 162 | 163 | if err := m.recordMigration(name, batch); err != nil { 164 | color.Printf(color.Red("Failed to save to migration table: %s, error: %v\n"), name, err) 165 | return err 166 | } 167 | 168 | // 计算执行时间并输出成功信息 169 | duration := time.Since(start) 170 | color.Printf(color.Green("Migrated: %s (%d seconds)\n"), name, int(duration.Seconds())) 171 | } 172 | return nil 173 | } 174 | 175 | // executeDMLMigrations 执行DML迁移 176 | func (m *Manager) executeDMLMigrations(names []string, executedMap map[string]bool, batch int) error { 177 | for _, name := range names { 178 | if executedMap[name] { 179 | continue 180 | } 181 | 182 | migration := m.dmlMigrations[name] 183 | // 输出开始迁移 184 | color.Printf(color.White("Migrating: %s %s\n"), name, migration.Desc()) 185 | 186 | start := time.Now() 187 | if err := migration.Handle(m.db); err != nil { 188 | color.Printf(color.Red("Failed: %s (%v)\n"), name, err) 189 | return err 190 | } 191 | 192 | if err := m.recordMigration(name, batch); err != nil { 193 | color.Printf(color.Red("Failed to save to migration table: %s, error: %v\n"), name, err) 194 | return err 195 | } 196 | 197 | // 计算执行时间并输出成功信息 198 | duration := time.Since(start) 199 | color.Printf(color.Green("Migrated: %s %s (%d seconds)\n"), name, migration.Desc(), int(duration.Seconds())) 200 | } 201 | return nil 202 | } 203 | 204 | // hasPendingMigrations 检查是否有待执行的迁移 205 | func (m *Manager) hasPendingMigrations(ddlNames []string, dmlNames []string, executedMap map[string]bool) bool { 206 | // 检查DDL迁移 207 | for _, name := range ddlNames { 208 | if !executedMap[name] { 209 | return true 210 | } 211 | } 212 | // 检查DML迁移 213 | for _, name := range dmlNames { 214 | if !executedMap[name] { 215 | return true 216 | } 217 | } 218 | return false 219 | } 220 | 221 | // Run 执行迁移 222 | func (m *Manager) Run() error { 223 | // 初始化迁移表 224 | if err := m.initMigrationTable(); err != nil { 225 | color.Printf(color.Red("Failed to initialize migration table: %v\n"), err) 226 | return err 227 | } 228 | 229 | // 获取已执行的迁移 230 | executedMap, err := m.getExecutedMigrations() 231 | if err != nil { 232 | color.Printf(color.Red("Failed to get executed migrations: %v\n"), err) 233 | return err 234 | } 235 | 236 | // 获取当前批次号 237 | currentBatch, err := m.getCurrentBatch() 238 | if err != nil { 239 | color.Printf(color.Red("Failed to get current batch: %v\n"), err) 240 | return err 241 | } 242 | 243 | // 获取排序后的迁移名称 244 | ddlNames, dmlNames := m.getSortedMigrationNames() 245 | 246 | // 检查是否有需要执行的迁移 247 | if !m.hasPendingMigrations(ddlNames, dmlNames, executedMap) { 248 | color.Println(color.Green("Nothing to migrate.\n")) 249 | return nil 250 | } 251 | 252 | // 执行DDL迁移 253 | if err := m.executeDDLMigrations(ddlNames, executedMap, currentBatch); err != nil { 254 | return err 255 | } 256 | 257 | // 执行DML迁移 258 | if err := m.executeDMLMigrations(dmlNames, executedMap, currentBatch); err != nil { 259 | return err 260 | } 261 | 262 | return nil 263 | } 264 | -------------------------------------------------------------------------------- /internal/migration/migrator.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | // DDLMigrator DDL迁移器 8 | type DDLMigrator struct { 9 | db *gorm.DB 10 | } 11 | 12 | // NewDDLMigrator 创建DDL迁移器 13 | func NewDDLMigrator(db *gorm.DB) *DDLMigrator { 14 | return &DDLMigrator{db: db} 15 | } 16 | 17 | // DropTable 删除表 18 | func (m *DDLMigrator) DropTable(tablename string) error { 19 | return m.db.Migrator().DropTable(tablename) 20 | } 21 | 22 | // HasTable 检查表是否存在 23 | func (m *DDLMigrator) HasTable(tablename string) bool { 24 | return m.db.Migrator().HasTable(tablename) 25 | } 26 | 27 | // HasColumn 检查列是否存在 28 | func (m *DDLMigrator) HasColumn(tablename, columnname string) bool { 29 | return m.db.Migrator().HasColumn(tablename, columnname) 30 | } 31 | 32 | // CreateIndex 创建索引 33 | func (m *DDLMigrator) CreateIndex(tablename, indexname string) error { 34 | return m.db.Migrator().CreateIndex(tablename, indexname) 35 | } 36 | 37 | // DropIndex 删除索引 38 | func (m *DDLMigrator) DropIndex(tablename, indexname string) error { 39 | return m.db.Migrator().DropIndex(tablename, indexname) 40 | } 41 | 42 | // HasIndex 检查索引是否存在 43 | func (m *DDLMigrator) HasIndex(tablename, indexname string) bool { 44 | return m.db.Migrator().HasIndex(tablename, indexname) 45 | } 46 | 47 | // Exec 执行SQL 48 | func (m *DDLMigrator) Exec(sql string) error { 49 | return m.db.Exec(sql).Error 50 | } 51 | 52 | // Raw 执行SQL 53 | func (m *DDLMigrator) Raw(sql string) error { 54 | return m.db.Raw(sql).Error 55 | } 56 | 57 | // DMLMigrator DML迁移器 58 | type DMLMigrator struct { 59 | db *gorm.DB 60 | } 61 | 62 | // NewDMLMigrator 创建DML迁移器 63 | func NewDMLMigrator(db *gorm.DB) *DMLMigrator { 64 | return &DMLMigrator{db: db} 65 | } 66 | 67 | // DB 获取数据库连接 68 | func (m *DMLMigrator) DB() *gorm.DB { 69 | return m.db 70 | } 71 | -------------------------------------------------------------------------------- /internal/migration/model.go: -------------------------------------------------------------------------------- 1 | package migration 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Migration 迁移记录模型 8 | type Migration struct { 9 | ID uint `gorm:"primaryKey"` 10 | Desc string `gorm:"column:desc;type:varchar(255);not null"` 11 | Batch int `gorm:"type:int;not null"` 12 | CreatedAt time.Time `gorm:"column:created_at;type:datetime;not null"` 13 | } 14 | 15 | // TableName 指定表名 16 | func (Migration) TableName() string { 17 | return "migrations" 18 | } 19 | -------------------------------------------------------------------------------- /internal/queue/client.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "go-gin/internal/component/redisx" 5 | 6 | "github.com/hibiken/asynq" 7 | ) 8 | 9 | var ( 10 | client *asynq.Client 11 | ) 12 | 13 | func Init(c redisx.Config) { 14 | client = asynq.NewClient(RedisClientOpt(c)) 15 | } 16 | 17 | func Close() { 18 | defer client.Close() 19 | } 20 | -------------------------------------------------------------------------------- /internal/queue/config.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "go-gin/internal/component/redisx" 5 | 6 | "github.com/hibiken/asynq" 7 | ) 8 | 9 | func RedisClientOpt(c redisx.Config) asynq.RedisClientOpt { 10 | return asynq.RedisClientOpt{ 11 | Addr: c.Addr, 12 | Username: c.Username, 13 | Password: c.Password, 14 | DB: c.DB, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /internal/queue/option.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/hibiken/asynq" 7 | ) 8 | 9 | type Option struct { 10 | opts []asynq.Option 11 | } 12 | 13 | func NewOption() *Option { 14 | return &Option{} 15 | } 16 | 17 | func (o *Option) MaxRetry(n int) *Option { 18 | o.add(asynq.MaxRetry(n)) 19 | return o 20 | } 21 | 22 | func (o *Option) Queue(name string) *Option { 23 | o.add(asynq.Queue(name)) 24 | return o 25 | } 26 | 27 | func (o *Option) HighQueue() *Option { 28 | o.add(asynq.Queue(HIGH)) 29 | return o 30 | } 31 | 32 | func (o *Option) LowQueue() *Option { 33 | o.add(asynq.Queue(LOW)) 34 | return o 35 | } 36 | 37 | func (o *Option) TaskID(id string) *Option { 38 | o.add(asynq.TaskID(id)) 39 | return o 40 | } 41 | 42 | func (o *Option) Timeout(d time.Duration) *Option { 43 | o.add(asynq.Timeout(d)) 44 | return o 45 | } 46 | 47 | func (o *Option) Deadline(t time.Time) *Option { 48 | o.add(asynq.Deadline(t)) 49 | return o 50 | } 51 | 52 | func (o *Option) Unique(ttl time.Duration) *Option { 53 | o.add(asynq.Unique(ttl)) 54 | return o 55 | } 56 | 57 | func (o *Option) ProcessAt(t time.Time) *Option { 58 | o.add(asynq.ProcessAt(t)) 59 | return o 60 | } 61 | 62 | func (o *Option) ProcessIn(d time.Duration) *Option { 63 | o.add(asynq.ProcessIn(d)) 64 | return o 65 | } 66 | 67 | func (o *Option) Retention(d time.Duration) *Option { 68 | o.add(asynq.Retention(d)) 69 | return o 70 | } 71 | 72 | func (o *Option) Group(name string) *Option { 73 | o.add(asynq.Group(name)) 74 | return o 75 | } 76 | 77 | func (o *Option) Dispatch(t *Task) error { 78 | return do(t, o.opts...) 79 | } 80 | 81 | func (o *Option) DispatchIf(b bool, t *Task) error { 82 | if !b { 83 | return nil 84 | } 85 | return do(t, o.opts...) 86 | } 87 | 88 | func (o *Option) add(opt asynq.Option) { 89 | o.opts = append(o.opts, opt) 90 | } 91 | -------------------------------------------------------------------------------- /internal/queue/server.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "go-gin/internal/component/redisx" 5 | 6 | "github.com/hibiken/asynq" 7 | ) 8 | 9 | var ( 10 | srv *asynq.Server 11 | mux *asynq.ServeMux 12 | ) 13 | 14 | var ( 15 | HIGH = "critical" 16 | NORMAL = "default" 17 | LOW = "low" 18 | ) 19 | 20 | func InitServer(c redisx.Config) { 21 | srv = asynq.NewServer( 22 | RedisClientOpt(c), 23 | asynq.Config{ 24 | // Specify how many concurrent workers to use 25 | Concurrency: 10, 26 | StrictPriority: true, 27 | // Optionally specify multiple queues with different priority. 28 | Queues: map[string]int{ 29 | HIGH: 6, 30 | NORMAL: 3, 31 | LOW: 1, 32 | }, 33 | // See the godoc for other configuration options 34 | }, 35 | ) 36 | mux = asynq.NewServeMux() 37 | } 38 | 39 | func Start() error { 40 | return srv.Run(mux) 41 | } 42 | -------------------------------------------------------------------------------- /internal/queue/task.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import "time" 4 | 5 | type Task struct { 6 | taskName string 7 | payload any 8 | } 9 | 10 | func (t *Task) DispatchNow() error { 11 | return DispatchNow(t) 12 | } 13 | 14 | func (t *Task) DispatchIf(b bool) error { 15 | if !b { 16 | return nil 17 | } 18 | return DispatchNow(t) 19 | } 20 | 21 | func (t *Task) Dispatch(d time.Duration) error { 22 | return Dispatch(t, d) 23 | } 24 | 25 | func NewTask(taskName string, payload any) *Task { 26 | return &Task{ 27 | taskName: taskName, 28 | payload: payload, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/queue/task_handler.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go-gin/internal/component/logx" 7 | "go-gin/internal/traceid" 8 | "time" 9 | 10 | "github.com/hibiken/asynq" 11 | ) 12 | 13 | type TaskHandler struct { 14 | taskName string 15 | handler func(context.Context, []byte) error 16 | } 17 | 18 | func NewTaskHandler(taskName string, handler func(context.Context, []byte) error) *TaskHandler { 19 | return &TaskHandler{ 20 | taskName: taskName, 21 | handler: handler, 22 | } 23 | } 24 | 25 | func AddHandler(h *TaskHandler) { 26 | mux.HandleFunc(h.taskName, func(ctx context.Context, t *asynq.Task) error { 27 | new_ctx := context.WithValue(ctx, traceid.TraceIdFieldName, traceid.New()) 28 | fmt.Println("---" + string(t.Payload())) 29 | logx.QueueLoggerInstance.Info().Ctx(new_ctx).Str("task", t.Type()).Str("keywords", "开始执行").Any("payload", string(t.Payload())).Send() 30 | 31 | start := time.Now() 32 | err := h.handler(new_ctx, t.Payload()) 33 | 34 | TimeStamp := time.Now() 35 | Cost := TimeStamp.Sub(start) 36 | if Cost > time.Minute { 37 | Cost = Cost.Truncate(time.Second) 38 | } 39 | if err != nil { 40 | logx.QueueLoggerInstance.Error().Ctx(new_ctx).Str("task", t.Type()).Str("keywords", "执行结束").Str("cost", Cost.String()).Str("err", err.Error()).Send() 41 | 42 | } else { 43 | logx.QueueLoggerInstance.Info().Ctx(new_ctx).Str("task", t.Type()).Str("keywords", "执行结束").Str("cost", Cost.String()).Send() 44 | 45 | } 46 | return err 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /internal/queue/util.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "time" 7 | 8 | "github.com/hibiken/asynq" 9 | ) 10 | 11 | func DispatchNow(t *Task) error { 12 | return do(t) 13 | } 14 | 15 | func DispatchIf(b bool, t *Task) error { 16 | if !b { 17 | return nil 18 | } 19 | return DispatchNow(t) 20 | } 21 | 22 | func Dispatch(t *Task, d time.Duration) error { 23 | return do(t, asynq.ProcessIn(d)) 24 | } 25 | 26 | func DispatchWithRetry(t *Task, d time.Duration, n int) error { 27 | return do(t, asynq.ProcessIn(d), asynq.MaxRetry(n)) 28 | } 29 | 30 | func do(t *Task, opts ...asynq.Option) error { 31 | p, err := json.Marshal(t.payload) 32 | if err != nil { 33 | return err 34 | } 35 | str := strings.Trim(string(p), "\"") // 结果: hello 36 | 37 | _, err = client.Enqueue(asynq.NewTask(t.taskName, []byte(str)), opts...) 38 | if err != nil { 39 | return err 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/token/token.go: -------------------------------------------------------------------------------- 1 | package token 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go-gin/internal/component/redisx" 7 | "strings" 8 | "time" 9 | 10 | "github.com/google/uuid" 11 | ) 12 | 13 | const ( 14 | TOKEN_PREFIX = "token:" 15 | EXPIRE_TIME = 7 * 24 * time.Hour 16 | ) 17 | 18 | func TokenId() string { 19 | tokenId := uuid.New().String() 20 | return strings.ToLower(strings.ReplaceAll(tokenId, "-", "")) 21 | } 22 | 23 | func Set(ctx context.Context, key string, field string, value string) error { 24 | if err := redisx.Client().HSet(ctx, transformKey(key), field, value).Err(); err != nil { 25 | return err 26 | } 27 | if err := redisx.Client().Expire(ctx, transformKey(key), EXPIRE_TIME).Err(); err != nil { 28 | return err 29 | } 30 | return nil 31 | } 32 | 33 | func Get(ctx context.Context, key string, field string) (string, error) { 34 | cmd := redisx.Client().HGet(ctx, transformKey(key), field) 35 | if err := cmd.Err(); err != nil { 36 | return "", err 37 | } 38 | return cmd.Val(), nil 39 | } 40 | 41 | func Has(ctx context.Context, key string) (bool, error) { 42 | cmd := redisx.Client().Exists(ctx, transformKey(key)) 43 | if err := cmd.Err(); err != nil { 44 | return false, err 45 | } 46 | return cmd.Val() != 0, nil 47 | } 48 | 49 | func HasField(ctx context.Context, key string, field string) (bool, error) { 50 | cmd := redisx.Client().HExists(ctx, transformKey(key), field) 51 | if err := cmd.Err(); err != nil { 52 | return false, err 53 | } 54 | return cmd.Val(), nil 55 | } 56 | 57 | func Delete(ctx context.Context, key string, field string) error { 58 | if err := redisx.Client().HDel(ctx, transformKey(key), field).Err(); err != nil { 59 | return err 60 | } 61 | return nil 62 | } 63 | 64 | func Flush(ctx context.Context, key string) error { 65 | if err := redisx.Client().Del(ctx, transformKey(key)).Err(); err != nil { 66 | return err 67 | } 68 | return nil 69 | } 70 | 71 | func GetAll(ctx context.Context, key string) (map[string]string, error) { 72 | cmd := redisx.Client().HGetAll(ctx, transformKey(key)) 73 | if err := cmd.Err(); err != nil { 74 | return nil, err 75 | } 76 | return cmd.Val(), nil 77 | } 78 | 79 | func transformKey(key string) string { 80 | return fmt.Sprintf("%s:%s", TOKEN_PREFIX, key) 81 | } 82 | -------------------------------------------------------------------------------- /internal/traceid/traceid.go: -------------------------------------------------------------------------------- 1 | package traceid 2 | 3 | import "github.com/google/uuid" 4 | 5 | var TraceIdFieldName = "trace_id" 6 | 7 | func New() string { 8 | return uuid.New().String() 9 | } 10 | -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "path/filepath" 5 | "runtime" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | var ProjectDir string 11 | 12 | func init() { 13 | _, file, _, _ := runtime.Caller(0) 14 | // compatible solution to get gorm source directory with various operating systems 15 | dir := filepath.Dir(filepath.Dir(filepath.Dir(file))) 16 | ProjectDir = filepath.ToSlash(dir) + "/" 17 | } 18 | 19 | // FileWithLineNum return the file name and line number of the current file 20 | func FileWithLineNum() string { 21 | pcs := [13]uintptr{} 22 | // the third caller usually from gorm internal 23 | len := runtime.Callers(3, pcs[:]) 24 | frames := runtime.CallersFrames(pcs[:len]) 25 | for i := 0; i < len; i++ { 26 | // second return value is "more", not "ok" 27 | frame, _ := frames.Next() 28 | 29 | if (strings.HasPrefix(frame.File, ProjectDir) && !strings.HasSuffix(frame.File, "_test.go")) && !strings.HasSuffix(frame.File, ".gen.go") { 30 | return string(strconv.AppendInt(append([]byte(strings.ReplaceAll(frame.File, ProjectDir, "")), ':'), int64(frame.Line), 10)) 31 | } 32 | } 33 | 34 | return "" 35 | } 36 | -------------------------------------------------------------------------------- /logic/adduser_logic.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go-gin/const/enum" 7 | "go-gin/model" 8 | "go-gin/typing" 9 | ) 10 | 11 | type AddUserLogic struct { 12 | model *model.UserModel 13 | } 14 | 15 | func NewAddUserLogic() *AddUserLogic { 16 | return &AddUserLogic{ 17 | model: model.NewUserModel(), 18 | } 19 | } 20 | 21 | func (l *AddUserLogic) Handle(ctx context.Context, req typing.AddUserReq) (resp *typing.AddUserResp, err error) { 22 | if err != nil { 23 | return nil, err 24 | } 25 | s := enum.UserType(24) 26 | user := model.User{ 27 | Name: req.Name, 28 | // Status: enum.STATUS_DELETED, 29 | UserType: s, 30 | } 31 | fmt.Println(user) 32 | if err = l.model.Add(ctx, &user); err != nil { 33 | return 34 | } 35 | resp = &typing.AddUserResp{ 36 | Message: fmt.Sprintf("message:%d", user.Id), 37 | } 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /logic/getusers_logic.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go-gin/const/enum" 7 | "go-gin/const/errcode" 8 | "go-gin/internal/component/redisx" 9 | "go-gin/model" 10 | "go-gin/transformer" 11 | "go-gin/typing" 12 | "go-gin/util/jsonx" 13 | ) 14 | 15 | type GetUsersLogic struct { 16 | model *model.UserModel 17 | } 18 | 19 | func NewGetUsersLogic() *GetUsersLogic { 20 | return &GetUsersLogic{ 21 | model: model.NewUserModel(), 22 | } 23 | } 24 | 25 | func (l *GetUsersLogic) Handle(ctx context.Context, req typing.ListReq) (resp *typing.ListResp, err error) { 26 | var u []model.User 27 | if u, err = l.model.List(ctx); errcode.IsError(err) { 28 | return nil, err 29 | } 30 | 31 | for _, v := range u { 32 | fmt.Println(v.UserType, v.UserType == enum.UserTypeSupplier, v.UserType.String(), v.UserType.Equal(enum.UserTypeSupplier), v.UserType == enum.UserTypeSupplier) 33 | } 34 | 35 | jsonStr, err := jsonx.Encode(u) 36 | fmt.Println(jsonStr, err) 37 | 38 | var my []model.User 39 | err = jsonx.Decode(jsonStr, &my) 40 | fmt.Println(err) 41 | fmt.Printf("%+v\n", my) 42 | 43 | redisx.Client().HSet(ctx, "name", "age", 43) 44 | 45 | return &typing.ListResp{ 46 | Data: transformer.ConvertUserToListData(u), 47 | }, nil 48 | 49 | } 50 | -------------------------------------------------------------------------------- /logic/index_logic.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import "context" 4 | 5 | type IndexLogic struct { 6 | } 7 | 8 | func NewIndexLogic() *IndexLogic { 9 | return &IndexLogic{} 10 | } 11 | 12 | func (l *IndexLogic) Handle(ctx context.Context, req any) (resp any, err error) { 13 | return "user/index", nil 14 | } 15 | -------------------------------------------------------------------------------- /logic/login_logic.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "go-gin/const/errcode" 6 | "go-gin/internal/token" 7 | "go-gin/model" 8 | "go-gin/typing" 9 | ) 10 | 11 | type LoginLogic struct { 12 | model *model.UserModel 13 | } 14 | 15 | func NewLoginLogic() *LoginLogic { 16 | return &LoginLogic{ 17 | model: model.NewUserModel(), 18 | } 19 | } 20 | 21 | func (l *LoginLogic) Handle(ctx context.Context, req typing.LoginReq) (resp *typing.LoginResp, err error) { 22 | if user, err := l.model.GetByUsername(ctx, req.Username); err != nil { 23 | if errcode.IsRecordNotFound(err) { 24 | return nil, errcode.ErrUserNotFound 25 | } else { 26 | return nil, err 27 | } 28 | } else { 29 | t := token.TokenId() 30 | if err := token.Set(ctx, t, "name", user.Name); err != nil { 31 | return nil, err 32 | } 33 | return &typing.LoginResp{ 34 | Token: t, 35 | User: *user, 36 | }, nil 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /logic/multiadduser_logic.go: -------------------------------------------------------------------------------- 1 | package logic 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go-gin/model" 7 | "go-gin/typing" 8 | ) 9 | 10 | type MultiAddUserLogic struct { 11 | model *model.UserModel 12 | } 13 | 14 | func NewMultiAddUserLogic() *MultiAddUserLogic { 15 | return &MultiAddUserLogic{ 16 | model: model.NewUserModel(), 17 | } 18 | } 19 | 20 | func (l *MultiAddUserLogic) Handle(ctx context.Context, req typing.MultiUserAddReq) (resp *typing.MultiUserAddResp, err error) { 21 | 22 | users := make([]model.User, len(req.Users)) 23 | for i, user := range req.Users { 24 | users[i] = model.User{ 25 | Name: user.Name, 26 | } 27 | } 28 | fmt.Println(users) 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /middleware/after_sample_a.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "go-gin/internal/httpx" 6 | ) 7 | 8 | func AfterSampleA() httpx.HandlerFunc { 9 | return func(ctx *httpx.Context) (any, error) { 10 | fmt.Println("AfterSampleA") 11 | return nil, nil 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /middleware/after_sample_b.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "go-gin/internal/httpx" 6 | ) 7 | 8 | func AfterSampleB() httpx.HandlerFunc { 9 | return func(ctx *httpx.Context) (any, error) { 10 | fmt.Println("AfterSampleB") 11 | return nil, nil 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /middleware/before_sample_a.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "go-gin/internal/httpx" 6 | ) 7 | 8 | func BeforeSampleA() httpx.HandlerFunc { 9 | return func(ctx *httpx.Context) (any, error) { 10 | fmt.Println("BeforeSampleA") 11 | return nil, nil 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /middleware/before_sample_b.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "go-gin/internal/httpx" 6 | ) 7 | 8 | func BeforeSampleB() httpx.HandlerFunc { 9 | return func(ctx *httpx.Context) (any, error) { 10 | fmt.Println("BeforeSampleB") 11 | return nil, nil 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /middleware/init.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import "go-gin/internal/httpx" 4 | 5 | func Init(r *httpx.Engine) { 6 | 7 | r.Before(BeforeSampleA(), BeforeSampleB()) 8 | r.After(AfterSampleB()) 9 | // r.Before(TokenCheck()) 10 | 11 | } 12 | -------------------------------------------------------------------------------- /middleware/token_check.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "go-gin/const/errcode" 5 | "go-gin/internal/httpx" 6 | "go-gin/internal/token" 7 | ) 8 | 9 | type TokenHeader struct { 10 | Token string `header:"token" binding:"required"` 11 | } 12 | 13 | func TokenCheck() httpx.HandlerFunc { 14 | return func(ctx *httpx.Context) (any, error) { 15 | var req TokenHeader 16 | if err := ctx.ShouldBindHeader(&req); err != nil { 17 | return nil, errcode.ErrUserMustLogin 18 | } 19 | if has, err := token.Has(ctx, req.Token); err != nil { 20 | return nil, errcode.NewDefault("获取token错误") 21 | } else if !has { 22 | return nil, errcode.ErrUserNeedLoginAgain 23 | } 24 | return nil, nil 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /migration/ddl/create_users_20240101120000.go: -------------------------------------------------------------------------------- 1 | package ddl 2 | 3 | import ( 4 | "go-gin/internal/migration" 5 | ) 6 | 7 | func init() { 8 | migration.RegisterDDL(&CreateUsers20240101120000{}) 9 | } 10 | 11 | // CreateUsers20240101120000 创建用户表迁移 12 | type CreateUsers20240101120000 struct{} 13 | 14 | // Up 执行迁移 15 | func (m *CreateUsers20240101120000) Up(migrator *migration.DDLMigrator) error { 16 | return migrator.Exec(` 17 | CREATE TABLE users ( 18 | id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY, 19 | username VARCHAR(255) DEFAULT '' COMMENT '用户名', 20 | age INT DEFAULT 0 COMMENT '年龄', 21 | create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' 22 | ) 23 | `) 24 | } 25 | -------------------------------------------------------------------------------- /migration/ddl/init.go: -------------------------------------------------------------------------------- 1 | package ddl 2 | -------------------------------------------------------------------------------- /migration/dml/deploy_20240101_120000.go: -------------------------------------------------------------------------------- 1 | package dml 2 | 3 | import ( 4 | "go-gin/internal/migration" 5 | 6 | "gorm.io/gorm" 7 | ) 8 | 9 | func init() { 10 | migration.RegisterDML(&Deploy20240101120000{}) 11 | } 12 | 13 | // Deploy20240101120000001 初始化管理员用户 14 | type Deploy20240101120000 struct{} 15 | 16 | // Handle 执行迁移 17 | func (m *Deploy20240101120000) Handle(db *gorm.DB) error { 18 | return db.Exec(` 19 | INSERT INTO users (username, age) 20 | VALUES ('admin', 18) 21 | `).Error 22 | } 23 | 24 | // Desc 获取迁移描述 25 | func (m *Deploy20240101120000) Desc() string { 26 | return "用户表写入一个默认管理员用户" 27 | } 28 | -------------------------------------------------------------------------------- /migration/dml/init.go: -------------------------------------------------------------------------------- 1 | package dml 2 | -------------------------------------------------------------------------------- /model/user_model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "context" 5 | "go-gin/const/enum" 6 | "go-gin/internal/component/db" 7 | "time" 8 | ) 9 | 10 | type User struct { 11 | Id int64 12 | Name string `gorm:"column:name" json:"name"` 13 | Age *int `gorm:"column:age;default:null" json:"age"` 14 | CreateTime time.Time `gorm:"column:create_time" json:"create_time"` 15 | Status *enum.UserStatus `gorm:"column:status;default:null" json:"status"` 16 | UserType enum.UserType `gorm:"column:user_type" json:"user_type"` 17 | } 18 | 19 | func (u *User) TableName() string { 20 | return `user` 21 | } 22 | 23 | type UserModel struct { 24 | } 25 | 26 | func NewUserModel() *UserModel { 27 | return &UserModel{} 28 | } 29 | 30 | func (m *UserModel) List(ctx context.Context) ([]User, error) { 31 | var u []User 32 | return u, db.WithContext(ctx).Find(&u).Error() 33 | } 34 | 35 | func (m *UserModel) Add(ctx context.Context, user *User) error { 36 | return db.WithContext(ctx).Select("Name", "Status", "UserType").Create(user).Error() 37 | } 38 | 39 | func (m *UserModel) GetByUsername(ctx context.Context, name string) (*User, error) { 40 | var user User 41 | return &user, db.WithContext(ctx).First(&user, "username=?", name).Error() 42 | } 43 | -------------------------------------------------------------------------------- /rest/login/init.go: -------------------------------------------------------------------------------- 1 | package login 2 | 3 | var ( 4 | Svc ILoginSvc = (*LoginSvc)(nil) 5 | ) 6 | 7 | func Init(url string) { 8 | Svc = NewLoginSvc(url) 9 | } 10 | -------------------------------------------------------------------------------- /rest/login/response.go: -------------------------------------------------------------------------------- 1 | package login 2 | 3 | import ( 4 | "go-gin/internal/httpc" 5 | "go-gin/util/jsonx" 6 | ) 7 | 8 | var ( 9 | ApiResponseSuccessCode = true 10 | ) 11 | 12 | // 解析返回格式固定的结构,返回结构包含success msg param字段 13 | type APIResponse struct { 14 | Code *bool `json:"success"` 15 | Message *string `json:"msg"` 16 | Data any `json:"param"` 17 | } 18 | 19 | var _ httpc.IResponse = (*APIResponse)(nil) 20 | 21 | // 解析响应结构 22 | func (r *APIResponse) Parse(b []byte) error { 23 | err := jsonx.Unmarshal(b, &r) 24 | if err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | 30 | // 验证返回格式 31 | func (r *APIResponse) Valid() bool { 32 | if r.Code == nil || r.Message == nil { 33 | return false 34 | } 35 | return true 36 | } 37 | 38 | // 验证返回状态码 39 | func (r *APIResponse) IsSuccess() bool { 40 | return *r.Code == ApiResponseSuccessCode 41 | } 42 | 43 | // 消息体 44 | func (r *APIResponse) Msg() string { 45 | return *r.Message 46 | } 47 | 48 | // 解析数据体 49 | func (r *APIResponse) ParseData() error { 50 | 51 | // 将 data 字段转换为 JSON 字符串 52 | dataStr, err := jsonx.Marshal(r.Data) 53 | if err != nil { 54 | return err 55 | } 56 | // 尝试将 data 字段解析为给定的结构体类型 57 | err = jsonx.Unmarshal(dataStr, r.Data) 58 | if err != nil { 59 | return err 60 | } 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /rest/login/svc.go: -------------------------------------------------------------------------------- 1 | package login 2 | 3 | import "context" 4 | 5 | type ILoginSvc interface { 6 | Login(context.Context, *LoginReq) (*LoginResp, error) 7 | } 8 | 9 | type LoginReq struct { 10 | Username string 11 | Pwd string 12 | } 13 | 14 | type LoginResp struct { 15 | AppGoodInDepotType string `json:"AppGoodInDepotType"` 16 | App_estimate_his_week_count string `json:"App_estimate_his_week_count"` 17 | AppPredictdishOrder string `json:"AppPredictdishOrder"` 18 | } 19 | -------------------------------------------------------------------------------- /rest/login/svc.impl.go: -------------------------------------------------------------------------------- 1 | package login 2 | 3 | import ( 4 | "context" 5 | "go-gin/internal/httpc" 6 | ) 7 | 8 | const ( 9 | Login_URL = "//logistics/apps/php/login.php?do=login" // 登录接口 10 | ) 11 | 12 | type LoginSvc struct { 13 | httpc.BaseSvc 14 | } 15 | 16 | func NewLoginSvc(url string) ILoginSvc { 17 | return &LoginSvc{ 18 | BaseSvc: *httpc.NewBaseSvc(url), 19 | } 20 | } 21 | 22 | func (us *LoginSvc) Login(ctx context.Context, req *LoginReq) (resp *LoginResp, err error) { 23 | // md5加密 24 | 25 | params := httpc.M{"username": req.Username, "pwd": req.Pwd} 26 | result := APIResponse{Data: &resp} 27 | err = us.Client(). 28 | NewRequest(). 29 | SetContext(ctx). 30 | POST(Login_URL). 31 | SetFormData(params). 32 | SetResult(&result). 33 | Exec() 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /rest/mylogin/init.go: -------------------------------------------------------------------------------- 1 | package mylogin 2 | 3 | var ( 4 | Svc ILoginSvc = (*LoginSvc)(nil) 5 | ) 6 | 7 | func Init(url string) { 8 | Svc = NewLoginSvc(url) 9 | } 10 | -------------------------------------------------------------------------------- /rest/mylogin/response.go: -------------------------------------------------------------------------------- 1 | package mylogin 2 | 3 | import ( 4 | "go-gin/internal/httpc" 5 | "go-gin/util/jsonx" 6 | ) 7 | 8 | var ( 9 | ApiResponseSuccessCode = true 10 | ) 11 | 12 | // 解析返回格式不固定,但是success msg两个标准字段,其它业务字段格式不固定 13 | type APIResponse struct { 14 | Code *bool `json:"success"` 15 | Message *string `json:"msg"` 16 | Data any 17 | } 18 | 19 | var _ httpc.IRepsonseNonStardard = (*APIResponse)(nil) 20 | 21 | // 解析响应结构 22 | func (r *APIResponse) Parse(b []byte) error { 23 | err := jsonx.Unmarshal(b, &r) 24 | if err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | 30 | // 验证返回格式 31 | func (r *APIResponse) Valid() bool { 32 | if r.Code == nil || r.Message == nil { 33 | return false 34 | } 35 | return true 36 | } 37 | 38 | // 验证返回状态码 39 | func (r *APIResponse) IsSuccess() bool { 40 | return *r.Code == ApiResponseSuccessCode 41 | } 42 | 43 | // 消息体 44 | func (r *APIResponse) Msg() string { 45 | return *r.Message 46 | } 47 | 48 | // 解析数据体 49 | func (r *APIResponse) ParseData(b []byte) error { 50 | // 尝试将 data 字段解析为给定的结构体类型 51 | err := jsonx.Unmarshal(b, r.Data) 52 | if err != nil { 53 | return err 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /rest/mylogin/svc.go: -------------------------------------------------------------------------------- 1 | package mylogin 2 | 3 | import "context" 4 | 5 | type ILoginSvc interface { 6 | Login(context.Context, *LoginReq) (*LoginResp, error) 7 | } 8 | 9 | type LoginReq struct { 10 | Username string 11 | Pwd string 12 | } 13 | 14 | type Mqtt struct { 15 | Customer string `json:"mqtt_customer"` 16 | Port string `json:"websocket_port"` 17 | } 18 | 19 | type LoginResp struct { 20 | Lsname string `json:"lsname"` 21 | Rname string `json:"rname"` 22 | MQtt Mqtt `json:"mqtt_config"` 23 | appPointFuncArr []string `json:"appPointFuncArr"` 24 | Param map[string]any `json:"param"` 25 | } 26 | -------------------------------------------------------------------------------- /rest/mylogin/svc.impl.go: -------------------------------------------------------------------------------- 1 | package mylogin 2 | 3 | import ( 4 | "context" 5 | "go-gin/internal/httpc" 6 | ) 7 | 8 | const ( 9 | Login_URL = "/logistics/apps/php/login.php?do=login#hello=json" // 登录接口 10 | ) 11 | 12 | type LoginSvc struct { 13 | httpc.BaseSvc 14 | } 15 | 16 | func NewLoginSvc(url string) ILoginSvc { 17 | return &LoginSvc{ 18 | BaseSvc: *httpc.NewBaseSvc(url), 19 | } 20 | } 21 | 22 | func (us *LoginSvc) Login(ctx context.Context, req *LoginReq) (resp *LoginResp, err error) { 23 | // md5加密 24 | 25 | params := httpc.M{"username": req.Username, "pwd": req.Pwd} 26 | result := APIResponse{Data: &resp} 27 | err = us.Client(). 28 | NewRequest(). 29 | SetContext(ctx). 30 | POST(Login_URL). 31 | SetFormData(params). 32 | SetResult(&result). 33 | Exec() 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /rest/user/init.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | var ( 4 | Svc IUserSvc = (*UserSvc)(nil) 5 | ) 6 | 7 | func Init(url string) { 8 | Svc = NewUserSvc(url) 9 | } 10 | -------------------------------------------------------------------------------- /rest/user/response.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "go-gin/internal/httpc" 5 | "go-gin/util/jsonx" 6 | ) 7 | 8 | var ( 9 | ApiResponseSuccessCode = 200 10 | ) 11 | 12 | // 解析返回格式固定的结构,返回结构包含code message data字段 13 | type APIResponse struct { 14 | Code *int `json:"code"` 15 | Message *string `json:"message"` 16 | Data any `json:"data"` 17 | } 18 | 19 | var _ httpc.IResponse = (*APIResponse)(nil) 20 | 21 | // 解析响应结构 22 | func (r *APIResponse) Parse(b []byte) error { 23 | err := jsonx.Unmarshal(b, &r) 24 | if err != nil { 25 | return err 26 | } 27 | return nil 28 | } 29 | 30 | // 验证返回格式 31 | func (r *APIResponse) Valid() bool { 32 | if r.Code == nil || r.Message == nil { 33 | return false 34 | } 35 | return true 36 | } 37 | 38 | // 验证返回状态码 39 | func (r *APIResponse) IsSuccess() bool { 40 | return *r.Code == ApiResponseSuccessCode 41 | } 42 | 43 | // 消息体 44 | func (r *APIResponse) Msg() string { 45 | return *r.Message 46 | } 47 | 48 | // 解析数据体 49 | func (r *APIResponse) ParseData() error { 50 | 51 | // 将 data 字段转换为 JSON 字符串 52 | dataStr, err := jsonx.Marshal(r.Data) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | // 尝试将 data 字段解析为给定的结构体类型 58 | err = jsonx.Unmarshal(dataStr, r.Data) 59 | if err != nil { 60 | return err 61 | } 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /rest/user/svc.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import "context" 4 | 5 | type IUserSvc interface { 6 | Hello(context.Context, *HelloReq) (*HelloResp, error) 7 | } 8 | 9 | type HelloReq struct { 10 | UserId string 11 | } 12 | 13 | type HelloResp struct { 14 | Uid string `json:"userId"` 15 | Uname string `json:"username"` 16 | } 17 | -------------------------------------------------------------------------------- /rest/user/svc.impl.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | "go-gin/internal/httpc" 6 | "go-gin/util/jsonx" 7 | ) 8 | 9 | const ( 10 | HELLO_URL = "/api/list" // hello的接口路径 11 | ) 12 | 13 | type UserSvc struct { 14 | httpc.BaseSvc 15 | } 16 | 17 | func NewUserSvc(url string) IUserSvc { 18 | return &UserSvc{ 19 | BaseSvc: *httpc.NewBaseSvc(url), 20 | } 21 | } 22 | 23 | func (us *UserSvc) Hello(ctx context.Context, req *HelloReq) (resp *HelloResp, err error) { 24 | result := APIResponse{Data: &resp} 25 | ids, _ := jsonx.MarshalToString([]int{1, 2, 3}) 26 | 27 | userInfo, _ := jsonx.MarshalToString(httpc.M{ 28 | "id": "3", 29 | "name": "张三", 30 | }) 31 | params := httpc.M{"userId": req.UserId, "ids": ids, "info": userInfo} 32 | 33 | err = us.Client(). 34 | NewRequest(). 35 | SetContext(ctx). 36 | POST(HELLO_URL). 37 | SetFormData(params). 38 | AddFormData("userA", []string{"11", "22"}). 39 | SetResult(&result). 40 | Exec() 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /router/api.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "go-gin/controller" 5 | "go-gin/internal/httpx" 6 | ) 7 | 8 | func RegisterApiRoutes(r *httpx.RouterGroup) { 9 | r.GET("/", controller.ApiController.Index) 10 | r.GET("/indexa", controller.ApiController.IndexA) 11 | r.GET("/loginapi", controller.ApiController.IndexB) 12 | r.GET("/mylogin", controller.ApiController.IndexC) 13 | r.Any("/list", controller.ApiController.List) 14 | } 15 | -------------------------------------------------------------------------------- /router/common.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "go-gin/controller" 5 | "go-gin/internal/component/db" 6 | "go-gin/internal/component/redisx" 7 | "go-gin/internal/errorx" 8 | "go-gin/internal/g" 9 | "go-gin/internal/httpx" 10 | "go-gin/util" 11 | ) 12 | 13 | func RegisterCommonRoutes(route *httpx.Engine) { 14 | route.NoMethod(func(ctx *httpx.Context) (any, error) { 15 | return nil, errorx.ErrMethodNotAllowed 16 | }) 17 | route.NoRoute(func(ctx *httpx.Context) (any, error) { 18 | return nil, errorx.ErrNoRoute 19 | }) 20 | // 健康检测 21 | route.GET("/status", func(ctx *httpx.Context) (any, error) { 22 | db_err := db.WithContext(ctx).Ping() 23 | redis_err := redisx.Client().Ping(ctx).Err() 24 | return g.MapStrStr{ 25 | "database": util.When(db_err == nil, "ok", "failed"), 26 | "redis": util.When(redis_err == nil, "ok", "failed"), 27 | }, nil 28 | }) 29 | route.GET("/", controller.UserController.Index) 30 | } 31 | -------------------------------------------------------------------------------- /router/demo.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "fmt" 5 | "go-gin/controller" 6 | "go-gin/event" 7 | "go-gin/internal/eventbus" 8 | "go-gin/internal/g" 9 | "go-gin/internal/httpx" 10 | "go-gin/internal/queue" 11 | "go-gin/middleware" 12 | "go-gin/task" 13 | "go-gin/util" 14 | "time" 15 | ) 16 | 17 | func RegisterDemoRoutes(r *httpx.RouterGroup) { 18 | 19 | rr := r.Group("/") 20 | rr.After(middleware.AfterSampleA()).GET("/", controller.UserController.Index) 21 | 22 | r.GET("/task", func(ctx *httpx.Context) (any, error) { 23 | // err := task.DispatchNow(tasks.NewSampleTask("测试1234")) 24 | // fmt.Println(err) 25 | // err = task.Dispatch(tasks.NewSampleBTask("测试1234"), time.Second) 26 | 27 | // fmt.Println(err) 28 | // task.NewSampleTask("测试1").DispatchNow() 29 | task.NewSampleTask("测试1").DispatchIf(true) 30 | task.NewSampleTask("测试2").Dispatch(5 * time.Second) 31 | // queue.NewOption().Queue(queue.HIGH).TaskID("test").Dispatch(task.NewSampleBTask("hello")) 32 | // queue.NewOption().Queue(queue.HIGH).TaskID("test").DispatchIf(true, task.NewSampleBTask("hello")) 33 | queue.NewOption().LowQueue().TaskID("test").DispatchIf(true, task.NewSampleBTask("hello")) 34 | return "hello world", nil 35 | }) 36 | r.GET("/event", func(ctx *httpx.Context) (any, error) { 37 | eventbus.Fire(ctx, event.NewSampleEvent("hello 测试")) 38 | event.NewSampleEvent("333").FireIf(ctx, true) 39 | // event.NewDemoEvent(&model.User{Name: "hello"}).Fire(ctx) 40 | return "hello world", nil 41 | }) 42 | r.GET("/test", func(ctx *httpx.Context) (any, error) { 43 | 44 | a := 43 45 | fmt.Println(util.IsTrue(a)) 46 | return g.MapStrInt{"hello": 333}, nil 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /router/init.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "go-gin/internal/httpx" 5 | ) 6 | 7 | func Init(route *httpx.Engine) { 8 | RegisterCommonRoutes(route) 9 | RegisterUserRoutes(route.Group("/user")) 10 | RegisterLoginRoutes(route.Group("/")) 11 | RegisterApiRoutes(route.Group("/api")) 12 | RegisterDemoRoutes(route.Group("/demo")) 13 | } 14 | -------------------------------------------------------------------------------- /router/login.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "go-gin/controller" 5 | "go-gin/internal/httpx" 6 | ) 7 | 8 | func RegisterLoginRoutes(r *httpx.RouterGroup) { 9 | r.GET("/login", controller.LoginController.Login) 10 | 11 | // 退出登录 12 | r.GET("/logout", controller.LoginController.LoginOut) 13 | } 14 | -------------------------------------------------------------------------------- /router/user.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "go-gin/controller" 5 | "go-gin/internal/httpx" 6 | ) 7 | 8 | func RegisterUserRoutes(r *httpx.RouterGroup) { 9 | // r.Use(middlewares.TokenCheck()) 10 | // 用户信息 11 | r.GET("/", controller.UserController.Index) 12 | r.GET("/list", controller.UserController.List) 13 | r.Match([]httpx.HttpMethod{httpx.MethodGet, httpx.MethodPost}, "/adduser", controller.UserController.AddUser) 14 | r.Any("/multiadduser", controller.UserController.MultiUserAdd) 15 | } 16 | -------------------------------------------------------------------------------- /task/init.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "go-gin/internal/queue" 5 | ) 6 | 7 | func Init() { 8 | queue.AddHandler(NewSampleTaskHandler()) 9 | queue.AddHandler(NewSampleBTaskHandler()) 10 | } 11 | -------------------------------------------------------------------------------- /task/sample.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "go-gin/internal/queue" 7 | ) 8 | 9 | const TypeSampleTask = "sample" 10 | 11 | func NewSampleTask(p string) *queue.Task { 12 | return queue.NewTask(TypeSampleTask, p) 13 | } 14 | 15 | func NewSampleTaskHandler() *queue.TaskHandler { 16 | return queue.NewTaskHandler(TypeSampleTask, func(ctx context.Context, data []byte) error { 17 | fmt.Println(string(data)) 18 | // Image resizing code ... 19 | return nil 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /task/sampleB.go: -------------------------------------------------------------------------------- 1 | package task 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "go-gin/internal/queue" 8 | ) 9 | 10 | const TypeSampleBTask = "sampleB" 11 | 12 | type SampleBTaskPayload struct { 13 | UserId []string 14 | } 15 | 16 | func NewSampleBTask(p string) *queue.Task { 17 | return queue.NewTask(TypeSampleBTask, SampleBTaskPayload{UserId: []string{p}}) 18 | } 19 | 20 | func NewSampleBTaskHandler() *queue.TaskHandler { 21 | return queue.NewTaskHandler(TypeSampleBTask, 22 | func(ctx context.Context, data []byte) error { 23 | var p SampleBTaskPayload 24 | if err := json.Unmarshal(data, &p); err != nil { 25 | fmt.Println(err) 26 | return err 27 | } 28 | fmt.Println(p.UserId) 29 | // Image resizing code ... 30 | return nil 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /test/bool_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "go-gin/util" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestIsTrue(t *testing.T) { 11 | assert.True(t, util.IsTrue(11), "number not zero should be true") 12 | assert.True(t, util.IsFalse(0), "number zero should be false") 13 | assert.True(t, util.IsTrue("hello"), "string should be true") 14 | assert.True(t, util.IsFalse(""), "empty string should be false") 15 | assert.True(t, util.IsTrue(1.0), "float number should be true") 16 | assert.True(t, util.IsFalse(0.0), "float number should be false") 17 | } 18 | 19 | func TestConditional(t *testing.T) { 20 | 21 | assert.Equal(t, util.When(true, "hello", ""), "hello", "condtional should return first value") 22 | assert.Equal(t, util.When(false, "hello", "aa"), "aa", "condtional should return second value") 23 | } 24 | -------------------------------------------------------------------------------- /transformer/user.go: -------------------------------------------------------------------------------- 1 | package transformer 2 | 3 | import ( 4 | "go-gin/model" 5 | "go-gin/typing" 6 | ) 7 | 8 | func ConvertUserToListData(u []model.User) []typing.ListData { 9 | var resp []typing.ListData 10 | for _, v := range u { 11 | var ageTips string 12 | if v.Age != nil && *v.Age >= 18 { 13 | ageTips = "成年" 14 | } else { 15 | ageTips = "未成年" 16 | } 17 | resp = append(resp, typing.ListData{ 18 | Id: int(v.Id), 19 | Name: v.Name, 20 | AgeTips: ageTips, 21 | Age: 2, 22 | Status: v.Status, 23 | UserType: v.UserType, 24 | UsserTypeText: v.UserType.String(), 25 | }) 26 | } 27 | return resp 28 | } 29 | -------------------------------------------------------------------------------- /typing/query/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fanqingxuan/go-gin/4bd697260b2276fbdbcfe652fa59a4be27dcffb4/typing/query/.gitkeep -------------------------------------------------------------------------------- /typing/user.go: -------------------------------------------------------------------------------- 1 | package typing 2 | 3 | import ( 4 | "go-gin/const/enum" 5 | "go-gin/model" 6 | "time" 7 | ) 8 | 9 | type ListReq struct { 10 | } 11 | 12 | type ListData struct { 13 | Id int `json:"id"` 14 | Name string `json:"name"` 15 | AgeTips string `json:"age_tips"` 16 | Age int `json:"age"` 17 | Status *enum.UserStatus `json:"status"` 18 | UserType enum.UserType `json:"user_type"` 19 | UsserTypeText string `json:"user_type_text"` 20 | } 21 | 22 | type ListResp struct { 23 | Data []ListData `json:"data"` 24 | } 25 | 26 | type AddUserReq struct { 27 | Name string `form:"name" binding:"required" label:"姓名"` 28 | Age int `form:"age" binding:"required" label:"年龄"` 29 | Status bool `form:"status"` 30 | Ctime time.Time `form:"ctime"` 31 | } 32 | 33 | type AddUserResp struct { 34 | Message string `json:"message"` 35 | } 36 | 37 | type LoginReq struct { 38 | Username string `form:"username" binding:"required,email" label:"用户名"` 39 | Pwd string `form:"pass" binding:"required,min=6" label:"密码"` 40 | } 41 | 42 | type LoginResp struct { 43 | Token string `json:"token"` 44 | User model.User `json:"user"` 45 | } 46 | 47 | type UserData struct { 48 | Id int `json:"id" form:"id"` 49 | Name string `json:"name" form:"name"` 50 | Age int `json:"age" form:"age"` 51 | } 52 | 53 | type MultiUserAddReq struct { 54 | Users []UserData `json:"users" form:"users"` 55 | } 56 | 57 | type MultiUserAddResp struct { 58 | Message string `json:"message"` 59 | } 60 | -------------------------------------------------------------------------------- /util/array.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "go-gin/internal/g" 4 | 5 | // InArray 检查某个元素是否在切片中 6 | func InArray[T g.Scalar](item T, slice []T) bool { 7 | for _, v := range slice { 8 | if v == item { 9 | return true 10 | } 11 | } 12 | return false 13 | } 14 | -------------------------------------------------------------------------------- /util/bool.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "go-gin/internal/g" 4 | 5 | // IsTrue 检查给定的标量值是否为真 6 | func IsTrue[T g.Scalar](value T) bool { 7 | var zeroValue T 8 | return value != zeroValue // 检查是否为零值 9 | } 10 | 11 | // IsFalse 检查给定的标量值是否为假 12 | func IsFalse[T g.Scalar](value T) bool { 13 | return !IsTrue(value) 14 | } 15 | 16 | // WhenFunc 执行给定函数,如果给定的标量值为真 17 | func WhenFunc[T g.Scalar](value T, f func()) { 18 | if IsTrue(value) { 19 | f() 20 | } 21 | } 22 | 23 | // When 返回条件表达式的值 24 | func When[T g.Scalar, U any](condition T, trueValue U, falseValue U) U { 25 | if IsTrue(condition) { 26 | return trueValue 27 | } 28 | return falseValue 29 | } 30 | -------------------------------------------------------------------------------- /util/jsonx/json.go: -------------------------------------------------------------------------------- 1 | package jsonx 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "strings" 9 | ) 10 | 11 | // Marshal marshals v into json bytes. 12 | func Marshal(v any) ([]byte, error) { 13 | return json.Marshal(v) 14 | } 15 | 16 | // MarshalToString marshals v into a string. 17 | func MarshalToString(v any) (string, error) { 18 | data, err := Marshal(v) 19 | if err != nil { 20 | return "", err 21 | } 22 | 23 | return string(data), nil 24 | } 25 | 26 | // Unmarshal unmarshals data bytes into v. 27 | func Unmarshal(data []byte, v any) error { 28 | decoder := json.NewDecoder(bytes.NewReader(data)) 29 | if err := unmarshalUseNumber(decoder, v); err != nil { 30 | return formatError(string(data), err) 31 | } 32 | 33 | return nil 34 | } 35 | 36 | // UnmarshalFromString unmarshals v from str. 37 | func UnmarshalFromString(str string, v any) error { 38 | decoder := json.NewDecoder(strings.NewReader(str)) 39 | if err := unmarshalUseNumber(decoder, v); err != nil { 40 | return formatError(str, err) 41 | } 42 | 43 | return nil 44 | } 45 | 46 | // UnmarshalFromReader unmarshals v from reader. 47 | func UnmarshalFromReader(reader io.Reader, v any) error { 48 | var buf strings.Builder 49 | teeReader := io.TeeReader(reader, &buf) 50 | decoder := json.NewDecoder(teeReader) 51 | if err := unmarshalUseNumber(decoder, v); err != nil { 52 | return formatError(buf.String(), err) 53 | } 54 | 55 | return nil 56 | } 57 | 58 | // Encode encodes v into a json string. 59 | func Encode(v any) (string, error) { 60 | return MarshalToString(v) 61 | } 62 | 63 | // Decode decodes str into v. 64 | func Decode(str string, v any) error { 65 | return UnmarshalFromString(str, v) 66 | } 67 | 68 | func unmarshalUseNumber(decoder *json.Decoder, v any) error { 69 | decoder.UseNumber() 70 | return decoder.Decode(v) 71 | } 72 | 73 | func formatError(v string, err error) error { 74 | return fmt.Errorf("string: `%s`, error: `%w`", v, err) 75 | } 76 | --------------------------------------------------------------------------------