├── .DS_Store ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── dataSources.xml ├── git_toolbox_blame.xml ├── git_toolbox_prj.xml ├── modules.xml ├── vcs.xml ├── watcherTasks.xml └── wike-go.iml ├── common └── common.go ├── config.yaml ├── db └── core.db ├── go.mod ├── go.sum ├── logs └── app.log ├── main.go ├── model.conf ├── pkg ├── core │ ├── casbin.go │ ├── core.go │ ├── cron.go │ ├── ctl.go │ ├── db.go │ ├── helper.go │ ├── http.go │ ├── infra.go │ ├── interface.go │ └── middleware.go ├── doc │ └── doc.go ├── func │ ├── bloomfilter │ │ └── bloomfilter.go │ ├── breaker │ │ └── breaker.go │ ├── cache │ │ ├── interface.go │ │ ├── memory │ │ │ ├── cache.go │ │ │ └── cache_test.go │ │ └── redis │ │ │ ├── cache.go │ │ │ └── cache_test.go │ ├── config │ │ └── config.go │ ├── cron │ │ └── cron.go │ ├── ctl │ │ ├── base.go │ │ ├── const.go │ │ ├── curd.go │ │ ├── error.go │ │ ├── fastRouter │ │ │ └── core.go │ │ ├── func.go │ │ ├── json.go │ │ ├── page.go │ │ ├── repeatCheck.go │ │ ├── returnType.go │ │ ├── time.go │ │ └── ws.go │ ├── hash │ │ └── consistent.go │ ├── jwt │ │ ├── jwt.go │ │ └── jwt_test.go │ ├── log │ │ └── zap.go │ ├── memorylog │ │ ├── log.go │ │ └── log_test.go │ ├── rateLimiter │ │ └── rateLimiter.go │ └── retry │ │ └── retry.go ├── modules │ └── ants_service │ │ └── ants.go ├── os │ └── serverMetrics.go ├── tpl │ └── tpl.go └── utils │ ├── copy.go │ ├── duration.go │ ├── ip.go │ ├── lock.go │ ├── map.go │ ├── md5.go │ ├── number.go │ ├── password.go │ ├── queue.go │ ├── random.go │ ├── set.go │ └── validator.go ├── plugin └── repeatCheck.go ├── policy.csv ├── readme.md ├── server ├── api │ ├── api.go │ ├── build.go │ ├── dictionary.go │ ├── job.go │ ├── log.go │ ├── role.go │ └── system.go ├── model │ ├── SysDictionary.go │ ├── api.go │ ├── casbin.go │ └── job.go └── service │ └── service.go ├── web ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── .vscode │ └── extensions.json ├── README.md ├── eslint.config.mjs ├── index.html ├── package-lock.json ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.vue │ ├── assets │ │ └── vue.svg │ ├── axios │ │ └── common.js │ ├── components │ │ ├── HelloWorld.vue │ │ └── MenuItem.vue │ ├── main.ts │ ├── pages │ │ ├── api │ │ │ └── index.vue │ │ ├── cronJob │ │ │ └── index.vue │ │ ├── dict │ │ │ └── index.vue │ │ ├── dictDetail │ │ │ └── index.vue │ │ ├── home │ │ │ └── index.vue │ │ ├── log │ │ │ └── index.vue │ │ ├── role │ │ │ └── index.vue │ │ ├── rule │ │ │ └── index.vue │ │ └── settings │ │ │ └── index.vue │ ├── router │ │ └── index.js │ ├── style.scss │ ├── tools │ │ └── index.js │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts └── 接口文档.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wike2019/wike_go/ed6f7064c11f9709f437634b6634c70c62b44cae/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ./logs/ 2 | ./main.go 3 | ./config.yaml 4 | ./viper.go -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | middleware.go -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sqlite.xerial 6 | true 7 | org.sqlite.JDBC 8 | jdbc:sqlite:$PROJECT_DIR$/core.db 9 | $ProjectFileDir$ 10 | 11 | 12 | sqlite.xerial 13 | true 14 | org.sqlite.JDBC 15 | jdbc:sqlite:$PROJECT_DIR$/db/core.db 16 | $ProjectFileDir$ 17 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/git_toolbox_blame.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/git_toolbox_prj.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/wike-go.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // 根据id 搜索 4 | type IDSearch struct { 5 | ID uint `json:"id" form:"id"` 6 | } 7 | 8 | // 根据关键字搜索 9 | type KeyWordsSearch struct { 10 | KeyWords string `json:"keywords" form:"keywords"` 11 | } 12 | 13 | // 空结构,一般用于参数为空 14 | type Empty struct { 15 | } 16 | 17 | func (this IDSearch) GetID() uint { 18 | return this.ID 19 | } 20 | 21 | type IDSearcher interface { 22 | GetID() uint 23 | } 24 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | port: 9999 2 | development: true 3 | mysql: "root:QAZwsx123123...@tcp(192.168.3.2:3307)/kf_vue?charset=utf8mb4&parseTime=True&loc=Local" 4 | redis: "192.168.3.2:6379" 5 | logPath: "./logs/app.log" 6 | text: "仅限于奇荃网络相关问题处理使用,他用无效。" 7 | LRULimit: 4096 8 | LimitRate: 128 9 | LimitBucket: 512 -------------------------------------------------------------------------------- /db/core.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wike2019/wike_go/ed6f7064c11f9709f437634b6634c70c62b44cae/db/core.db -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wike2019/wike_go 2 | 3 | go 1.23 4 | 5 | require ( 6 | github.com/avast/retry-go v3.0.0+incompatible 7 | github.com/bsm/redislock v0.9.4 8 | github.com/casbin/casbin/v2 v2.100.0 9 | github.com/casbin/gorm-adapter/v3 v3.32.0 10 | github.com/gin-contrib/gzip v0.0.6 11 | github.com/gin-contrib/timeout v1.0.0 12 | github.com/gin-gonic/gin v1.9.1 13 | github.com/glebarez/sqlite v1.7.0 14 | github.com/goccy/go-json v0.10.2 15 | github.com/golang-jwt/jwt/v4 v4.5.0 16 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da 17 | github.com/google/uuid v1.3.1 18 | github.com/gorilla/websocket v1.5.1 19 | github.com/hugh2632/bloomfilter v0.0.0-20220107050508-533b6738df0f 20 | github.com/panjf2000/ants/v2 v2.10.0 21 | github.com/patrickmn/go-cache v2.1.0+incompatible 22 | github.com/redis/go-redis/v9 v9.3.0 23 | github.com/robfig/cron v1.2.0 24 | github.com/shirou/gopsutil/v3 v3.24.5 25 | github.com/shopspring/decimal v1.3.1 26 | github.com/sony/gobreaker v0.5.0 27 | github.com/spf13/viper v1.16.0 28 | github.com/stathat/consistent v1.0.0 29 | go.uber.org/fx v1.23.0 30 | go.uber.org/zap v1.26.0 31 | golang.org/x/crypto v0.21.0 32 | golang.org/x/time v0.5.0 33 | gopkg.in/natefinch/lumberjack.v2 v2.2.1 34 | gorm.io/gorm v1.25.12 35 | ) 36 | 37 | require ( 38 | github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect 39 | github.com/bytedance/sonic v1.11.3 // indirect 40 | github.com/casbin/govaluate v1.2.0 // indirect 41 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 42 | github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect 43 | github.com/chenzhuoyu/iasm v0.9.1 // indirect 44 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 45 | github.com/dustin/go-humanize v1.0.1 // indirect 46 | github.com/fsnotify/fsnotify v1.6.0 // indirect 47 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 48 | github.com/gin-contrib/sse v0.1.0 // indirect 49 | github.com/glebarez/go-sqlite v1.20.3 // indirect 50 | github.com/go-ole/go-ole v1.2.6 // indirect 51 | github.com/go-playground/locales v0.14.1 // indirect 52 | github.com/go-playground/universal-translator v0.18.1 // indirect 53 | github.com/go-playground/validator/v10 v10.19.0 // indirect 54 | github.com/go-redis/redis/v8 v8.8.3 // indirect 55 | github.com/go-sql-driver/mysql v1.7.0 // indirect 56 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect 57 | github.com/golang-sql/sqlexp v0.1.0 // indirect 58 | github.com/hashicorp/hcl v1.0.0 // indirect 59 | github.com/jackc/pgpassfile v1.0.0 // indirect 60 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 61 | github.com/jackc/pgx/v5 v5.5.5 // indirect 62 | github.com/jackc/puddle/v2 v2.2.1 // indirect 63 | github.com/jinzhu/inflection v1.0.0 // indirect 64 | github.com/jinzhu/now v1.1.5 // indirect 65 | github.com/json-iterator/go v1.1.12 // indirect 66 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 67 | github.com/leodido/go-urn v1.4.0 // indirect 68 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 69 | github.com/magiconair/properties v1.8.7 // indirect 70 | github.com/mattn/go-isatty v0.0.20 // indirect 71 | github.com/microsoft/go-mssqldb v1.6.0 // indirect 72 | github.com/mitchellh/mapstructure v1.5.0 // indirect 73 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 74 | github.com/modern-go/reflect2 v1.0.2 // indirect 75 | github.com/onsi/ginkgo v1.16.5 // indirect 76 | github.com/onsi/gomega v1.25.0 // indirect 77 | github.com/pelletier/go-toml/v2 v2.2.0 // indirect 78 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 79 | github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect 80 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 81 | github.com/spf13/afero v1.9.5 // indirect 82 | github.com/spf13/cast v1.5.1 // indirect 83 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 84 | github.com/spf13/pflag v1.0.5 // indirect 85 | github.com/subosito/gotenv v1.4.2 // indirect 86 | github.com/tklauser/go-sysconf v0.3.12 // indirect 87 | github.com/tklauser/numcpus v0.6.1 // indirect 88 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 89 | github.com/ugorji/go/codec v1.2.12 // indirect 90 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 91 | go.opentelemetry.io/otel v0.20.0 // indirect 92 | go.opentelemetry.io/otel/metric v0.20.0 // indirect 93 | go.opentelemetry.io/otel/trace v0.20.0 // indirect 94 | go.uber.org/dig v1.18.0 // indirect 95 | go.uber.org/multierr v1.10.0 // indirect 96 | golang.org/x/arch v0.7.0 // indirect 97 | golang.org/x/net v0.22.0 // indirect 98 | golang.org/x/sync v0.8.0 // indirect 99 | golang.org/x/sys v0.20.0 // indirect 100 | golang.org/x/text v0.14.0 // indirect 101 | google.golang.org/protobuf v1.33.0 // indirect 102 | gopkg.in/ini.v1 v1.67.0 // indirect 103 | gopkg.in/yaml.v3 v3.0.1 // indirect 104 | gorm.io/driver/mysql v1.5.7 // indirect 105 | gorm.io/driver/postgres v1.5.9 // indirect 106 | gorm.io/driver/sqlserver v1.5.3 // indirect 107 | gorm.io/plugin/dbresolver v1.5.3 // indirect 108 | modernc.org/libc v1.22.2 // indirect 109 | modernc.org/mathutil v1.5.0 // indirect 110 | modernc.org/memory v1.5.0 // indirect 111 | modernc.org/sqlite v1.20.3 // indirect 112 | stathat.com/c/consistent v1.0.0 // indirect 113 | ) 114 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/spf13/viper" 6 | "github.com/wike2019/wike_go/pkg/core" 7 | "github.com/wike2019/wike_go/server/api" 8 | "github.com/wike2019/wike_go/server/service" 9 | "log" 10 | ) 11 | 12 | func MyViper(core *core.GCore) *viper.Viper { 13 | //fmt.Println(core) 14 | viper.SetDefault("port", "8888") 15 | viper.SetDefault("logPath", "./logs/app.log") 16 | viper.SetDefault("development", true) 17 | viper.SetConfigFile("config.yaml") // 指定配置文件路径 18 | viper.SetConfigName("config") // 配置文件名称(无扩展名) 19 | viper.SetConfigType("yaml") // 如果配置文件的名称中没有扩展名,则需要配置此项 20 | viper.AddConfigPath(".") // 还可以在工作目录中查找配置 21 | err := viper.ReadInConfig() // 查找并读取配置文件 22 | if err != nil { // 处理读取配置文件的错误 23 | log.Fatalf("Fatal error config file: %s \n", err.Error()) 24 | } 25 | //业务参数 26 | viper.SetDefault("Timeout", 3000) 27 | core.Stop(func() error { 28 | fmt.Println("这里是viper清理") 29 | return nil 30 | }) 31 | return viper.GetViper() 32 | } 33 | func main() { 34 | g := core.God() 35 | g.Stop(func() error { 36 | fmt.Println("这里做全局清理") 37 | return nil 38 | }) 39 | g.Default().GlobalUse(core.CORSMiddleware()) //选择redis作为缓存服务的存储 40 | g.Provide(MyViper).Provide(service.CoreService).MountWithEmpty(api.NewRouter).Run() 41 | } 42 | -------------------------------------------------------------------------------- /model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) 12 | [matchers] 13 | m = r.sub == "root" ||(g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")) -------------------------------------------------------------------------------- /pkg/core/casbin.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/casbin/casbin/v2" 5 | "github.com/casbin/gorm-adapter/v3" 6 | "github.com/wike2019/wike_go/server/model" 7 | "go.uber.org/zap" 8 | "log" 9 | "os" 10 | ) 11 | 12 | /* 13 | * 14 | 配置文件模版 15 | */ 16 | const modelconfig = ` 17 | [request_definition] 18 | r = sub, obj, act 19 | 20 | [policy_definition] 21 | p = sub, obj, act 22 | 23 | [role_definition] 24 | g = _, _ 25 | 26 | [policy_effect] 27 | e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) 28 | [matchers] 29 | m = r.sub == "root" ||(g(r.sub, p.sub) && keyMatch(r.obj, p.obj) && (r.act == p.act || p.act == "*")) 30 | ` 31 | 32 | /* 33 | * 34 | 鉴权系统初始化方法,默认使用sqlite作为数据存储 35 | */ 36 | func NewEnforcer(db *CoreDb) *casbin.Enforcer { 37 | _, err := os.Stat("model.conf") 38 | if os.IsNotExist(err) { 39 | os.WriteFile("model.conf", []byte(modelconfig), os.ModePerm) 40 | } 41 | adapter, err := gormadapter.NewAdapterByDB(db.DB) 42 | if err != nil { 43 | log.Fatalf("Failed to create NewAdapterByDB: %v", err) 44 | } 45 | 46 | // 2. 创建 Casbin Enforcer,加载模型和策略 47 | enforcer, err := casbin.NewEnforcer("model.conf", adapter) 48 | if err != nil { 49 | log.Fatalf("Failed to create enforcer: %v", err) 50 | } 51 | 52 | // 3. 加载策略(如果已经存在) 53 | err = enforcer.LoadPolicy() 54 | if err != nil { 55 | log.Fatalf("Failed to load policy: %v", err) 56 | } 57 | return enforcer 58 | } 59 | 60 | // 核心鉴权系统 61 | type RoleCtl struct { 62 | E *casbin.Enforcer 63 | zap *zap.Logger 64 | } 65 | 66 | func NewCtl(e *casbin.Enforcer, zap *zap.Logger) *RoleCtl { 67 | return &RoleCtl{E: e, zap: zap} 68 | } 69 | 70 | // 添加规则 71 | func (this *RoleCtl) AddRule(role string, path string, method string) { 72 | _, err := this.E.AddPolicy(role, path, method) 73 | if err != nil { 74 | this.zap.Error("添加鉴权规则失败:" + err.Error()) 75 | } 76 | err = this.E.SavePolicy() 77 | if err != nil { 78 | this.zap.Error("保存鉴权规则失败:" + err.Error()) 79 | } 80 | } 81 | 82 | // 添加角色 83 | func (this *RoleCtl) AddRole(role string, parentRole string) { 84 | _, err := this.E.AddGroupingPolicy(role, parentRole) 85 | if err != nil { 86 | this.zap.Error("添加继承关系失败:" + err.Error()) 87 | } 88 | err = this.E.SavePolicy() 89 | if err != nil { 90 | this.zap.Error("保存继承规则失败:" + err.Error()) 91 | } 92 | } 93 | 94 | func (this *RoleCtl) GetAllParentRoles(role string) []string { 95 | parentRoles := make(map[string]bool) // 使用 map 防止重复 96 | var dfs func(r string) 97 | 98 | dfs = func(r string) { 99 | roles, _ := this.E.GetRolesForUser(r) // 获取直接父角色 100 | for _, parentRole := range roles { 101 | if !parentRoles[parentRole] { // 防止重复访问 102 | parentRoles[parentRole] = true 103 | dfs(parentRole) // 递归获取父角色 104 | } 105 | } 106 | } 107 | dfs(role) 108 | // 将 map 转为 slice 返回 109 | result := make([]string, 0, len(parentRoles)) 110 | for r := range parentRoles { 111 | result = append(result, r) 112 | } 113 | return result 114 | } 115 | 116 | func (this *RoleCtl) GetRulesForRole(role string) []model.Rule { 117 | // 使用 GetFilteredPolicy 按照第一个字段(角色/主体)筛选规则 118 | res := make([]model.Rule, 0) 119 | rules, _ := this.E.GetFilteredPolicy(0, role) 120 | for _, item := range rules { 121 | res = append(res, model.Rule{ 122 | RuleSearch: model.RuleSearch{Role: item[0]}, 123 | Path: item[1], 124 | Method: item[2], 125 | }) 126 | } 127 | return res 128 | } 129 | 130 | func (this *RoleCtl) GetRulesForInheritRole(role string) []model.AggregatedRule { 131 | list := this.GetAllParentRoles(role) 132 | res := make([]model.Rule, 0) 133 | for _, item := range list { 134 | res = append(res, this.GetRulesForRole(item)...) 135 | } 136 | return this.AggregateRules(res) 137 | } 138 | func (this *RoleCtl) AggregateRules(rules []model.Rule) []model.AggregatedRule { 139 | // 使用 map 去重,key 是 path + method,value 是角色数组 140 | aggregatedMap := make(map[string]model.AggregatedRule) 141 | 142 | for _, rule := range rules { 143 | // 组合 Path 和 Method 作为 key 144 | key := rule.Path + "|" + rule.Method 145 | 146 | // 检查是否已经存在 147 | if aggregatedRule, exists := aggregatedMap[key]; exists { 148 | // 如果已存在,将 Role 添加到角色数组中(避免重复) 149 | exists := false 150 | for _, r := range aggregatedRule.Roles { 151 | if r == rule.Role { 152 | exists = true 153 | break 154 | } 155 | } 156 | if !exists { 157 | aggregatedRule.Roles = append(aggregatedRule.Roles, rule.Role) 158 | } 159 | aggregatedMap[key] = aggregatedRule 160 | } else { 161 | // 如果不存在,新建 AggregatedRule 162 | aggregatedMap[key] = model.AggregatedRule{ 163 | Path: rule.Path, 164 | Method: rule.Method, 165 | Roles: []string{rule.Role}, 166 | } 167 | } 168 | } 169 | 170 | // 将 map 转换为 slice 171 | result := make([]model.AggregatedRule, 0, len(aggregatedMap)) 172 | for _, aggregatedRule := range aggregatedMap { 173 | result = append(result, aggregatedRule) 174 | } 175 | 176 | return result 177 | } 178 | 179 | func (this *RoleCtl) DeleteRulesForRole(role string) error { 180 | // 使用 RemoveFilteredPolicy 删除与指定角色相关的规则 181 | this.E.RemoveFilteredPolicy(0, role) 182 | // 保存更改到持久化存储 183 | err := this.E.SavePolicy() 184 | return err 185 | } 186 | 187 | func (this *RoleCtl) DeleteRoleInheritance(childRole string, parentRole string) error { 188 | // 删除该角色的所有继承关系 189 | this.E.RemoveGroupingPolicy(childRole, parentRole) 190 | // 保存到持久化存储 191 | err := this.E.SavePolicy() 192 | return err 193 | } 194 | -------------------------------------------------------------------------------- /pkg/core/core.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/spf13/viper" 6 | "go.uber.org/fx" 7 | "go.uber.org/zap" 8 | ) 9 | 10 | type GCore struct { 11 | gin *gin.Engine //gin引擎 12 | app *fx.App //依赖注入 13 | provides []interface{} 14 | supply []interface{} 15 | invokes []interface{} 16 | Controller []interface{} 17 | globalMiddleware []gin.HandlerFunc 18 | CronFunc []map[string]func() 19 | RoleCtl *RoleCtl 20 | StopRun []func() error 21 | Reject bool 22 | Zap *zap.Logger 23 | cfg *viper.Viper 24 | db *CoreDb 25 | } 26 | 27 | const IRoutes = "routes" 28 | 29 | func God() *GCore { 30 | //初始化核心对象 31 | return &GCore{ 32 | gin: nil, 33 | Controller: make([]interface{}, 0), 34 | provides: make([]interface{}, 0), 35 | invokes: make([]interface{}, 0), 36 | globalMiddleware: make([]gin.HandlerFunc, 0), 37 | CronFunc: make([]map[string]func(), 0), 38 | StopRun: make([]func() error, 0), 39 | Zap: nil, 40 | cfg: nil, 41 | } 42 | } 43 | func (this *GCore) Run() { 44 | //通过依赖注入调用启动函数 45 | this.app = fx.New( 46 | Module, 47 | fx.NopLogger, 48 | fx.Provide(fx.Annotate( 49 | this.NewHTTPServer, 50 | fx.ParamTags(CreateGroup(IRoutes)), //将路由接口组注入进来 51 | )), 52 | fx.Supply(this.supply...), //注册supply 53 | fx.Provide(this.provides...), //注册 provides 54 | fx.Invoke(this.invokes...), //注册 invokes 55 | fx.Provide(this.Controller...), //注册 路由 56 | fx.Supply(this), 57 | ) 58 | this.app.Run() //启动app 59 | } 60 | -------------------------------------------------------------------------------- /pkg/core/cron.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/wike2019/wike_go/pkg/func/ctl" 5 | ) 6 | 7 | func (this *GCore) DefaultTask() { 8 | this.Cron("0 0 3 * * *", func() { 9 | // 在这里写入具体的任务逻辑 10 | ctl.ClearChan <- struct{}{} 11 | }, "每天清除数据任务") 12 | } 13 | -------------------------------------------------------------------------------- /pkg/core/ctl.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gin-gonic/gin" 6 | "github.com/wike2019/wike_go/pkg/doc" 7 | "github.com/wike2019/wike_go/pkg/func/ctl" 8 | "net/http" 9 | "reflect" 10 | ) 11 | 12 | // 权限注册函数 13 | func (this *GCore) GetWithRbac(r gin.IRoutes, group Controller, groupName string, path string, handler func(c *gin.Context) interface{}, name string) { 14 | query, body, header, output := group.GetInnerData() 15 | this.db.ApiTable(name, groupName, doc.Input(query, body, header), doc.Output(output), group.Path()+path, http.MethodGet) 16 | r.GET(path, WrapCtxHandler(handler)) 17 | 18 | } 19 | func (this *GCore) PostWithRbac(r gin.IRoutes, group Controller, groupName string, path string, handler func(c *gin.Context) interface{}, name string) { 20 | query, body, header, output := group.GetInnerData() 21 | this.db.ApiTable(name, groupName, doc.Input(query, body, header), doc.Output(output), group.Path()+path, http.MethodPost) 22 | r.POST(path, WrapCtxHandler(handler)) 23 | } 24 | func (this *GCore) DelWithRbac(r gin.IRoutes, group Controller, groupName string, path string, handler func(c *gin.Context) interface{}, name string) { 25 | query, body, header, output := group.GetInnerData() 26 | this.db.ApiTable(name, groupName, doc.Input(query, body, header), doc.Output(output), group.Path()+path, http.MethodDelete) 27 | r.DELETE(path, WrapCtxHandler(handler)) 28 | } 29 | func (this *GCore) PutWithRbac(r gin.IRoutes, group Controller, groupName string, path string, handler func(c *gin.Context) interface{}, name string) { 30 | query, body, header, output := group.GetInnerData() 31 | this.db.ApiTable(name, groupName, doc.Input(query, body, header), doc.Output(output), group.Path()+path, http.MethodPut) 32 | r.PUT(path, WrapCtxHandler(handler)) 33 | } 34 | func WrapCtxHandler(fn func(c *gin.Context) interface{}) gin.HandlerFunc { 35 | return func(c *gin.Context) { 36 | result := fn(c) 37 | if reflect.TypeOf(result).Kind() == reflect.String { 38 | str := fmt.Sprintf("%v", result) 39 | ctl.OKWithMsg(c, str) 40 | } else { 41 | ctl.OK(c, result) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pkg/core/db.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/glebarez/sqlite" 5 | "github.com/wike2019/wike_go/server/model" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type CoreDb struct { 10 | DB *gorm.DB 11 | } 12 | 13 | func InitDb() *CoreDb { 14 | dbSqlite, err := gorm.Open(sqlite.Open("./db/core.db"), &gorm.Config{}) 15 | if err != nil { 16 | panic("failed to connect database") 17 | } 18 | err = dbSqlite.AutoMigrate(&model.API{}, &model.SysDictionary{}, &model.SysDictionaryDetail{}, &model.Job{}, &model.Rule{}, &model.Role{}) 19 | if err != nil { 20 | panic("failed to migrate database") 21 | } 22 | dbSqlite.Model(model.API{}).Where("1=1").Update("status", 2) 23 | dbSqlite.Model(model.Job{}).Where("1=1").Delete(&model.Job{}) 24 | return &CoreDb{ 25 | DB: dbSqlite, 26 | } 27 | } 28 | 29 | func (this *CoreDb) ApiTable(name string, group string, input string, output string, path string, method string) *CoreDb { 30 | res := &model.API{ 31 | APISearch: model.APISearch{ 32 | Name: name, 33 | Group: group, 34 | Path: path, 35 | Method: method, 36 | }, 37 | Input: input, 38 | Output: output, 39 | 40 | Status: 1, 41 | } 42 | historyApi := &model.API{} 43 | err := this.DB.Where("method=? and path=?", method, path).First(historyApi).Error 44 | if err != nil { 45 | this.DB.Create(res) 46 | } else { 47 | historyApi.Input = input 48 | historyApi.Output = output 49 | historyApi.Name = name 50 | historyApi.Group = group 51 | historyApi.Status = 1 52 | this.DB.Save(historyApi) 53 | } 54 | 55 | return this 56 | } 57 | 58 | func (this *CoreDb) GetData() map[string]model.APIGroup { 59 | var list []model.API 60 | this.DB.Find(&list) 61 | 62 | rs := make(map[string]model.APIGroup) 63 | 64 | for _, item := range list { 65 | // 检查分组是否存在 66 | group, ok := rs[item.Group] 67 | if !ok { 68 | // 如果分组不存在,初始化 69 | group = model.APIGroup{ 70 | Group: item.Group, 71 | APIs: make([]model.API, 0), 72 | } 73 | } 74 | 75 | // 修改分组中的 API 列表 76 | group.APIs = append(group.APIs, item) 77 | 78 | // 将修改后的分组放回 map 79 | rs[item.Group] = group 80 | } 81 | 82 | return rs 83 | 84 | } 85 | -------------------------------------------------------------------------------- /pkg/core/helper.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "fmt" 5 | "go.uber.org/fx" 6 | ) 7 | 8 | // 将路由控制器转换成依赖注入模式 9 | 10 | func Create(fn interface{}, resultTag string, param []string) interface{} { 11 | if len(param) > 0 { 12 | return fx.Annotate(fn, fx.ParamTags(param...), fx.ResultTags(resultTag)) 13 | } else { 14 | return fx.Annotate(fn, fx.ResultTags(resultTag)) 15 | } 16 | } 17 | func CreateInterFace(fn interface{}, I interface{}, resultTag string, param []string) interface{} { 18 | if len(param) > 0 { 19 | return fx.Annotate(fn, fx.As(I), fx.ParamTags(param...), fx.ResultTags(resultTag)) 20 | } else { 21 | return fx.Annotate(fn, fx.As(I), fx.ResultTags(resultTag)) 22 | } 23 | } 24 | func ParamList(param ...string) []string { 25 | return param 26 | } 27 | func CreateTag(tag string) string { 28 | return fmt.Sprintf(` name:"%s" `, tag) 29 | } 30 | func CreateGroup(tag string) string { 31 | return fmt.Sprintf(` group:"%s" `, tag) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/core/http.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "compress/gzip" 5 | "context" 6 | "fmt" 7 | ginGzip "github.com/gin-contrib/gzip" 8 | "github.com/gin-gonic/gin" 9 | "github.com/google/uuid" 10 | "github.com/spf13/viper" 11 | cronInit "github.com/wike2019/wike_go/pkg/func/cron" 12 | "github.com/wike2019/wike_go/pkg/tpl" 13 | "go.uber.org/fx" 14 | "go.uber.org/zap" 15 | "net" 16 | "net/http" 17 | "os" 18 | "text/template" 19 | "time" 20 | ) 21 | 22 | func (this *GCore) NewHTTPServer(ControllerList []Controller, db *CoreDb, lc fx.Lifecycle, zap *zap.Logger, cfg *viper.Viper, defaultCron *cronInit.DefaultCron, roleCtl *RoleCtl) *http.Server { 23 | 24 | this.RoleCtl = roleCtl 25 | this.Zap = zap 26 | this.db = db 27 | this.cfg = cfg 28 | r := gin.New() 29 | this.gin = r //缓存gin 30 | this.gin.MaxMultipartMemory = 32 << 20 // 32 MiB //单次上传总文件最大大小 31 | this.gin.Use(this.globalMiddleware...) 32 | //健康检查路由 33 | r.GET("/api/v1/health", func(c *gin.Context) { 34 | c.String(200, "ok") 35 | }) 36 | //获取唯一token路由 37 | r.GET("/api/v1/token", func(c *gin.Context) { 38 | token := uuid.NewString() 39 | c.String(200, token) 40 | }) 41 | for _, route := range ControllerList { 42 | //注册路由 43 | group := r.Group(route.Path()) 44 | route.Build(group, this) 45 | } 46 | srv := &http.Server{ 47 | Addr: ":" + cfg.GetString("port"), 48 | Handler: this.gin, 49 | } 50 | 51 | lc.Append(fx.Hook{ 52 | OnStart: func(ctx context.Context) error { 53 | ln, err := net.Listen("tcp", srv.Addr) 54 | if err != nil { 55 | zap.Error(err.Error()) 56 | return err 57 | } 58 | zap.Debug(fmt.Sprintf("Starting HTTP server at %s", srv.Addr)) 59 | go func() { 60 | if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed { 61 | zap.Error(fmt.Sprintf("HTTP server listen: %s\n", err)) 62 | } 63 | }() 64 | this.DefaultTask() 65 | for _, item := range this.CronFunc { 66 | for k, v := range item { 67 | defaultCron.AddFunc(k, v) 68 | } 69 | } 70 | 71 | go func() { 72 | defaultCron.Start() 73 | }() 74 | 75 | tmpl, err := template.New("markdown").Parse(tpl.MdTemplate) 76 | if err != nil { 77 | zap.Fatal(err.Error()) 78 | } 79 | 80 | // 创建 Markdown 文件 81 | file, err := os.Create("接口文档.md") 82 | if err != nil { 83 | zap.Fatal(err.Error()) 84 | } 85 | 86 | // 渲染模板到文件 87 | err = tmpl.Execute(file, this.db.GetData()) 88 | if err != nil { 89 | zap.Fatal(err.Error()) 90 | } 91 | 92 | err = file.Close() 93 | if err != nil { 94 | zap.Fatal(err.Error()) 95 | } 96 | return nil 97 | }, 98 | OnStop: func(ctx context.Context) error { 99 | //清理资源 100 | defer zap.Sync() 101 | ctx, cancel := context.WithTimeout(ctx, time.Second*10) 102 | defer cancel() 103 | this.Reject = true 104 | defaultCron.Stop() 105 | srv.Shutdown(ctx) 106 | for _, job := range this.StopRun { 107 | err := job() 108 | if err != nil { 109 | zap.Error(fmt.Sprintf("stop func is error: %s\n", err)) 110 | } 111 | } 112 | return nil 113 | }, 114 | }) 115 | return srv 116 | } 117 | 118 | func (this *GCore) Default() *GCore { 119 | this.GlobalUse(ginGzip.Gzip(gzip.DefaultCompression)) //开启压缩 120 | this.GlobalUse(Reject(this)) //优雅关闭 121 | this.GlobalUse(AddTrace()) 122 | this.GlobalUse(CustomRecover(this)) //添加recover中间件和traceId中间件 123 | this.GlobalUse(AccessLog(this)) //添加访问日志中间件 124 | this.GlobalUse(LimitBodySize(32 << 20)) 125 | return this //添加body数据长度限制中间件 126 | //全局中间件 //注册用户自定义全局中间件 127 | //this.gin.Use(rateLimiter.RateLimiter(rateLimiterCache, cfg)) //设置接口根据ip限流 128 | } 129 | -------------------------------------------------------------------------------- /pkg/core/infra.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | cronInit "github.com/wike2019/wike_go/pkg/func/cron" 6 | zaplog "github.com/wike2019/wike_go/pkg/func/log" 7 | "github.com/wike2019/wike_go/server/model" 8 | "go.uber.org/fx" 9 | "net/http" 10 | "reflect" 11 | "runtime" 12 | ) 13 | 14 | var Module = fx.Module("infra", 15 | fx.Provide(zaplog.GetLogger), //默认日志 16 | fx.Provide(cronInit.NewDefaultCron, NewEnforcer, NewCtl), //定时器任务 rbac权限 17 | fx.Invoke(func(*http.Server) {}), 18 | fx.Provide(InitDb), 19 | ) 20 | 21 | // 用于没有参数的依赖注入 22 | func (this *GCore) Config(cfgs ...interface{}) *GCore { 23 | for _, cfg := range cfgs { 24 | t := reflect.TypeOf(cfg) 25 | if t.Kind() != reflect.Ptr { 26 | panic("required ptr object") //必须是指针对象 27 | } 28 | if t.Elem().Kind() != reflect.Struct { 29 | continue 30 | } //处理依赖注入 (new) 31 | v := reflect.ValueOf(cfg) 32 | for i := 0; i < t.NumMethod(); i++ { 33 | method := v.Method(i) 34 | callRet := method.Call(nil) 35 | 36 | if callRet != nil && len(callRet) == 1 { 37 | this.supply = append(this.supply, callRet[0].Interface()) 38 | } 39 | } 40 | } 41 | return this 42 | } 43 | 44 | // 用于有注入参数的依赖注入 45 | func (this *GCore) Provide(list ...interface{}) *GCore { 46 | this.provides = append(this.provides, list...) 47 | return this 48 | } 49 | 50 | // 用于主动调用 51 | func (this *GCore) Invokes(list ...interface{}) *GCore { 52 | this.invokes = append(this.invokes, list...) 53 | return this 54 | } 55 | 56 | // 用于注册全局中间件 57 | func (this *GCore) GlobalUse(middleware ...gin.HandlerFunc) *GCore { 58 | this.globalMiddleware = append(this.globalMiddleware, middleware...) 59 | return this 60 | } 61 | 62 | // 用于挂载带参数控制器 63 | func (this *GCore) Mount(class interface{}, params []string) *GCore { 64 | this.Controller = append(this.Controller, CreateInterFace(class, new(Controller), CreateGroup("routes"), params)) 65 | return this 66 | } 67 | 68 | // 用于挂载不带参数控制器 69 | func (this *GCore) MountWithEmpty(class interface{}) *GCore { 70 | this.Controller = append(this.Controller, CreateInterFace(class, new(Controller), CreateGroup("routes"), []string{})) 71 | return this 72 | } 73 | 74 | // 用于挂载已经存在带对象 75 | func (this *GCore) Supply(supply ...interface{}) *GCore { 76 | this.supply = append(this.supply, supply...) 77 | return this 78 | } 79 | 80 | // 用于添加定时任务 81 | func (this *GCore) Cron(spec string, cmd func(), Job string) *GCore { 82 | this.CronFunc = append(this.CronFunc, map[string]func(){spec: cmd}) 83 | JobTask := &model.Job{ 84 | JobSearch: model.JobSearch{ 85 | Name: Job, 86 | }, 87 | Cron: spec, 88 | Func: runtime.FuncForPC(reflect.ValueOf(cmd).Pointer()).Name(), 89 | } 90 | this.db.DB.Create(JobTask) 91 | return this 92 | } 93 | func (this *GCore) Stop(job func() error) *GCore { 94 | this.StopRun = append(this.StopRun, job) 95 | return this 96 | } 97 | -------------------------------------------------------------------------------- /pkg/core/interface.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | type Controller interface { 8 | Build(r *gin.RouterGroup, GCore *GCore) 9 | Path() string 10 | GetInnerData() (interface{}, interface{}, interface{}, interface{}) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/core/middleware.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "github.com/casbin/casbin/v2" 6 | "github.com/gin-contrib/timeout" 7 | "github.com/gin-gonic/gin" 8 | "github.com/google/uuid" 9 | controller "github.com/wike2019/wike_go/pkg/func/ctl" 10 | "github.com/wike2019/wike_go/pkg/func/rateLimiter" 11 | "github.com/wike2019/wike_go/pkg/utils" 12 | "go.uber.org/zap" 13 | "golang.org/x/time/rate" 14 | "io" 15 | "net/http" 16 | "strconv" 17 | "time" 18 | ) 19 | 20 | // 优雅关闭中间件 21 | func Reject(god *GCore) gin.HandlerFunc { 22 | return func(context *gin.Context) { 23 | if god.Reject { 24 | context.AbortWithStatusJSON(http.StatusServiceUnavailable, gin.H{"message": "服务暂时关闭", "code": 503}) 25 | return 26 | } 27 | context.Next() 28 | } 29 | } 30 | 31 | // 请求追中中间件 32 | func AddTrace() gin.HandlerFunc { 33 | return func(context *gin.Context) { 34 | traceId := "" 35 | traceIdHeader := context.Request.Header.Get("trace_id") 36 | traceIdQuery := context.Query("trace_id") 37 | repeatCheck := context.Query("__check__") 38 | if repeatCheck == "check" && traceIdQuery == "" { 39 | context.AbortWithStatusJSON(200, gin.H{"message": "非法数据", "code": 200, "trace_id": "0"}) 40 | return 41 | } 42 | if traceIdQuery != "" { 43 | traceId = traceIdQuery 44 | } else { 45 | traceId = traceIdHeader 46 | } 47 | if traceId == "" { 48 | traceId = uuid.NewString() 49 | } 50 | context.Set("trace_id", traceId) 51 | context.Next() 52 | } 53 | } 54 | 55 | // 日志中间件 56 | func AccessLog(god *GCore) gin.HandlerFunc { 57 | return func(ctx *gin.Context) { 58 | start := time.Now() 59 | path := ctx.Request.URL.Path 60 | raw := ctx.Request.URL.RawQuery 61 | var reqBody []byte 62 | if ctx.Request.Body != nil { 63 | reqBody, _ = io.ReadAll(ctx.Request.Body) 64 | ctx.Request.Body = io.NopCloser(bytes.NewBuffer(reqBody)) 65 | } 66 | ctx.Next() 67 | latency := time.Now().Sub(start) 68 | clientIP := ctx.ClientIP() 69 | method := ctx.Request.Method 70 | statusCode := ctx.Writer.Status() 71 | if raw != "" { 72 | path = path + "?" + raw 73 | } 74 | if ctx.Query("log") != "false" { 75 | god.Zap.Info("接口访问日志", 76 | zap.String("path", path), 77 | zap.String("method", method), 78 | zap.String("http_host", ctx.Request.Host), 79 | zap.String("ua", ctx.Request.UserAgent()), 80 | zap.String("remote_addr", ctx.Request.RemoteAddr), 81 | zap.Int("request_body_size", len(reqBody)), 82 | zap.Int("status_code", statusCode), 83 | zap.Int("error_code", ctx.GetInt("error_code")), 84 | zap.String("error_msg", ctx.GetString("error_code")), 85 | zap.String("client_ip", clientIP), 86 | zap.Duration("latency", latency), 87 | ) 88 | } 89 | 90 | } 91 | } 92 | 93 | // 跨域中间 94 | func CORSMiddleware() gin.HandlerFunc { 95 | return func(c *gin.Context) { 96 | c.Writer.Header().Set("Access-Control-Allow-Origin", "*") 97 | c.Writer.Header().Set("Access-Control-Allow-Credentials", "true") 98 | c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With,token") 99 | c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE") 100 | 101 | if c.Request.Method == "OPTIONS" { 102 | c.AbortWithStatus(http.StatusNoContent) 103 | return 104 | } 105 | 106 | c.Next() 107 | } 108 | } 109 | 110 | // 异常恢复中间件 111 | func CustomRecover(god *GCore) gin.HandlerFunc { 112 | return func(c *gin.Context) { 113 | defer func() { 114 | if err := recover(); err != nil { 115 | check, ok := err.(controller.StatusError) 116 | if ok { 117 | god.Zap.Warn("主动抛出的错误", zap.String("path", c.Request.URL.Path), zap.String("error", check.Msg), zap.Int("code", check.Code)) 118 | c.AbortWithStatusJSON(check.Code, gin.H{"message": check.Msg, "code": check.Code, "trace_id": c.GetString("trace_id")}) 119 | return 120 | } 121 | god.Zap.Error("接口错误", zap.String("path", c.Request.URL.Path), zap.String("error", err.(error).Error())) 122 | c.AbortWithStatusJSON(500, gin.H{"message": "Internal Server Error", "code": 500, "trace_id": c.GetString("trace_id")}) 123 | return 124 | } 125 | }() 126 | c.Next() 127 | } 128 | } 129 | 130 | // body数据大小限制中间件 131 | func LimitBodySize(maxSize int64) gin.HandlerFunc { 132 | return func(c *gin.Context) { 133 | contentLength := c.Request.Header.Get("Content-Length") 134 | length, err := strconv.ParseInt(contentLength, 10, 64) 135 | if err != nil { 136 | c.Next() 137 | return 138 | } 139 | if length > maxSize { 140 | c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"error": "请求体内容太长", "trace_id": c.GetString("trace_id")}) 141 | return 142 | } 143 | c.Next() 144 | } 145 | } 146 | 147 | // 超时中间件 148 | func TimeoutMiddleware(duration time.Duration) gin.HandlerFunc { 149 | return timeout.New( 150 | timeout.WithTimeout(duration), 151 | timeout.WithHandler(func(c *gin.Context) { 152 | c.Next() 153 | }), 154 | timeout.WithResponse(TimeOutResponse), 155 | ) 156 | } 157 | 158 | // 超时响应 159 | func TimeOutResponse(c *gin.Context) { 160 | c.JSON(http.StatusGatewayTimeout, gin.H{ 161 | "code": http.StatusGatewayTimeout, 162 | "msg": "系统超时了", 163 | "trace_id": c.GetString("trace_id"), 164 | }) 165 | } 166 | 167 | // RBAC中间件 168 | func Authorizer(e *casbin.Enforcer) gin.HandlerFunc { 169 | return func(c *gin.Context) { 170 | role := c.GetString("role") 171 | ok, _ := e.Enforce(role, c.FullPath(), c.Request.Method) 172 | if !ok { 173 | c.JSON(http.StatusNonAuthoritativeInfo, gin.H{ 174 | "code": http.StatusNonAuthoritativeInfo, 175 | "msg": "当前角色没有访问权限", 176 | "trace_id": c.GetString("trace_id"), 177 | }) 178 | c.Abort() 179 | } 180 | } 181 | } 182 | func Root() gin.HandlerFunc { 183 | return func(c *gin.Context) { 184 | c.Set("role", "root") 185 | } 186 | } 187 | 188 | func RateLimiter(rateLimiterCache rateLimiter.GetLimit, r float64, b int) gin.HandlerFunc { 189 | return func(c *gin.Context) { 190 | limiter := rateLimiterCache.GetLimiter(utils.GetClientIP(c), rate.Limit(r), b) 191 | if limiter.Allow() == false { 192 | c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ 193 | "code": http.StatusTooManyRequests, 194 | "error": "请求太频繁了", 195 | "trace_id": c.GetString("trace_id"), 196 | }) 197 | return 198 | } 199 | c.Next() 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /pkg/doc/doc.go: -------------------------------------------------------------------------------- 1 | package doc 2 | 3 | import ( 4 | "fmt" 5 | "gorm.io/gorm" 6 | "reflect" 7 | "strings" 8 | ) 9 | 10 | func Input(query interface{}, body interface{}, header interface{}) string { 11 | str := "" 12 | str += "query参数\n" 13 | str += "---\n" 14 | str += generateStructTable(query, 2, 1, false, true) 15 | str += "---\n" 16 | str += "body参数\n" 17 | str += "---\n" 18 | str += generateStructTable(body, 1, 1, false, true) 19 | str += "---\n" 20 | str += "header参数\n" 21 | str += "---\n" 22 | str += generateStructTable(header, 3, 1, false, true) 23 | str += "---\n" 24 | return str 25 | } 26 | func Output(body interface{}) string { 27 | return generateStructTable(body, 1, 1, false, false) + "---\n" 28 | } 29 | 30 | // 生成表格的函数 31 | func generateStructTableIn(visited map[string]bool, data interface{}, dataType int, level int, Anonymous bool, short bool) string { 32 | if data == nil { 33 | return "" 34 | } 35 | stack := make([]string, 0) 36 | 37 | // 限制递归的最大深度 38 | const maxDepth = 7 39 | if level > maxDepth { 40 | return "" 41 | } 42 | 43 | t := reflect.TypeOf(data) 44 | v := reflect.ValueOf(data) 45 | 46 | // 如果是指针,解引用 47 | if t.Kind() == reflect.Ptr { 48 | t = t.Elem() 49 | if v.IsValid() && !v.IsNil() { 50 | v = v.Elem() 51 | } 52 | } 53 | 54 | // 如果不是结构体,返回空 55 | if t.Kind() != reflect.Struct { 56 | return "" 57 | } 58 | 59 | md := strings.Builder{} 60 | if !Anonymous { 61 | if level > 5 { 62 | level = 5 63 | } 64 | // 添加标题 65 | md.WriteString(fmt.Sprintf("%s## %s\n\n", strings.Repeat("#", level), t.Name())) 66 | } 67 | 68 | // 根据 dataType 确定标签名称 69 | defaultData := "| 字段名 | 类型 | 标签 (json) | 描述 | 是否必填 | 是否搜索 |\n" 70 | switch dataType { 71 | case 2: 72 | defaultData = "| 字段名 | 类型 | 标签 (form) | 描述 | 是否必填 | 是否搜索 |\n" 73 | case 3: 74 | defaultData = "| 字段名 | 类型 | 标签 (header) | 描述 | 是否必填 | 是否搜索 |\n" 75 | } 76 | defaultDataLine := "|--------|------|------------|------|------|------|\n" 77 | if short == false { 78 | defaultData = "| 字段名 | 类型 | 标签 (json) | 描述 | \n" 79 | defaultDataLine = "|--------|------|------------|------| \n" 80 | } 81 | if !Anonymous { 82 | // 表格标题 83 | md.WriteString(defaultData) 84 | md.WriteString(defaultDataLine) 85 | } 86 | 87 | // 遍历结构体字段 88 | for i := 0; i < t.NumField(); i++ { 89 | field := t.Field(i) 90 | fieldValue := v.Field(i) 91 | 92 | // 如果是匿名字段,将其字段展开到当前结构体 93 | if field.Anonymous { 94 | if fieldValue.Kind() == reflect.Struct || fieldValue.Kind() == reflect.Array || fieldValue.Kind() == reflect.Slice { 95 | // 使用结构体类型的名称来追踪已经访问过的结构体 96 | typeName := field.Type.String() 97 | if visited[typeName] && field.Tag.Get("deep") != "true" { 98 | // 如果该结构体已经访问过,则跳过 99 | continue 100 | } 101 | visited[typeName] = true 102 | } 103 | // 如果匿名字段是指针,需要解引用 104 | if fieldValue.Kind() == reflect.Ptr && !fieldValue.IsNil() { 105 | stack = append(stack, generateStructTableIn(visited, fieldValue.Elem().Interface(), dataType, level+1, field.Anonymous, short)) 106 | } else if fieldValue.Kind() == reflect.Struct { 107 | stack = append(stack, generateStructTableIn(visited, fieldValue.Interface(), dataType, level+1, field.Anonymous, short)) 108 | } 109 | continue 110 | } 111 | 112 | // 获取字段信息 113 | name := field.Name 114 | fieldType := field.Type.String() 115 | tag := field.Tag.Get("json") 116 | if dataType == 2 { 117 | tag = field.Tag.Get("form") 118 | } else if dataType == 3 { 119 | tag = field.Tag.Get("header") 120 | } 121 | description := field.Tag.Get("desc") 122 | if description == "" { 123 | description = field.Tag.Get("comment") // 支持 `comment` 标签 124 | } 125 | required := field.Tag.Get("required") 126 | require := "否" 127 | if required == "true" { 128 | require = "是" 129 | } 130 | search := "-" 131 | searched := field.Tag.Get("search") 132 | if searched != "" { 133 | search = "是" 134 | } 135 | 136 | // 判断是否是结构体或者数组类型 137 | 138 | if short == false { 139 | md.WriteString(fmt.Sprintf("| %s | %s | %s | %s | \n", name, fieldType, tag, description)) 140 | } else { 141 | // 添加字段到表格 142 | md.WriteString(fmt.Sprintf("| %s | %s | %s | %s | %s | %s | \n", name, fieldType, tag, description, require, search)) 143 | } 144 | 145 | // 检查是否是标准库类型,如果是则跳过递归 146 | if isStandardLibraryType(field.Type) { 147 | 148 | continue 149 | } 150 | if fieldValue.Kind() == reflect.Struct || fieldValue.Kind() == reflect.Array || fieldValue.Kind() == reflect.Slice { 151 | // 使用结构体类型的名称来追踪已经访问过的结构体 152 | typeName := field.Type.String() 153 | if visited[typeName] && field.Tag.Get("deep") != "true" { 154 | // 如果该结构体已经访问过,则跳过 155 | continue 156 | } 157 | visited[typeName] = true 158 | } 159 | // 判断是否是可寻址的值类型 160 | 161 | //ptrAddr := fieldValue.Addr().Pointer() 162 | // 如果字段是指针类型,递归处理 163 | if fieldValue.Kind() == reflect.Ptr { 164 | // 如果是指针类型,解引用后递归处理 165 | if !fieldValue.IsNil() { 166 | stack = append(stack, generateStructTableIn(visited, fieldValue.Elem().Interface(), dataType, level+1, field.Anonymous, short)) 167 | } else { 168 | if short == false { 169 | md.WriteString(fmt.Sprintf("| %s | %s | 空指针 | - | \n", name, fieldType)) 170 | } else { 171 | // 添加字段到表格 172 | md.WriteString(fmt.Sprintf("| %s | %s | - | 空指针 | - |\n", name, fieldType)) 173 | } 174 | } 175 | } else if fieldValue.Kind() == reflect.Struct { 176 | // 如果是结构体类型,递归处理 177 | stack = append(stack, generateStructTableIn(visited, fieldValue.Interface(), dataType, level+1, field.Anonymous, short)) 178 | } else if fieldValue.Kind() == reflect.Slice { 179 | // 如果是切片,递归处理第一个元素 180 | elementType := field.Type.Elem() // 获取切片的元素类型 181 | if fieldValue.Len() > 0 { 182 | element := fieldValue.Index(0).Interface() 183 | if short == false { 184 | md.WriteString(fmt.Sprintf("| %s | []%s | %s | %s | \n", name, elementType.Name(), tag, description)) 185 | } else { 186 | // 添加字段到表格 187 | md.WriteString(fmt.Sprintf("| %s | []%s | %s | %s | %s | %s | \n", name, elementType.Name(), tag, description, require, search)) 188 | } 189 | stack = append(stack, generateStructTableIn(visited, element, dataType, level+1, field.Anonymous, short)) 190 | } else { 191 | // 如果切片为空,递归生成切片元素类型 192 | emptyElement := reflect.New(elementType).Elem().Interface() 193 | stack = append(stack, generateStructTableIn(visited, emptyElement, dataType, level+1, field.Anonymous, short)) 194 | } 195 | continue 196 | } 197 | 198 | } 199 | 200 | // 收集并返回最终结果 201 | res := md.String() 202 | for _, item := range stack { 203 | res += item 204 | } 205 | return res 206 | } 207 | 208 | // 生成表格的函数 209 | func generateStructTable(data interface{}, dataType int, level int, Anonymous bool, short bool) string { 210 | if data == nil { 211 | return "" 212 | } 213 | stack := make([]string, 0) 214 | visited := make(map[string]bool) // 用于记录已访问过的结构体类型名称 215 | 216 | // 限制递归的最大深度 217 | const maxDepth = 7 218 | if level > maxDepth { 219 | return "" 220 | } 221 | 222 | t := reflect.TypeOf(data) 223 | v := reflect.ValueOf(data) 224 | 225 | // 如果是指针,解引用 226 | if t.Kind() == reflect.Ptr { 227 | t = t.Elem() 228 | if v.IsValid() && !v.IsNil() { 229 | v = v.Elem() 230 | } 231 | } 232 | 233 | // 如果不是结构体,返回空 234 | if t.Kind() != reflect.Struct { 235 | return "" 236 | } 237 | 238 | md := strings.Builder{} 239 | if !Anonymous { 240 | if level > 5 { 241 | level = 5 242 | } 243 | // 添加标题 244 | md.WriteString(fmt.Sprintf("%s## %s\n\n", strings.Repeat("#", level), t.Name())) 245 | } 246 | 247 | // 根据 dataType 确定标签名称 248 | defaultData := "| 字段名 | 类型 | 标签 (json) | 描述 | 是否必填 | 是否搜索 |\n" 249 | switch dataType { 250 | case 2: 251 | defaultData = "| 字段名 | 类型 | 标签 (form) | 描述 | 是否必填 | 是否搜索 |\n" 252 | case 3: 253 | defaultData = "| 字段名 | 类型 | 标签 (header) | 描述 | 是否必填 | 是否搜索 |\n" 254 | } 255 | defaultDataLine := "|--------|------|------------|------|------|------|\n" 256 | if short == false { 257 | defaultData = "| 字段名 | 类型 | 标签 (json) | 描述 | \n" 258 | defaultDataLine = "|--------|------|------------|------| \n" 259 | } 260 | if !Anonymous { 261 | // 表格标题 262 | md.WriteString(defaultData) 263 | md.WriteString(defaultDataLine) 264 | } 265 | 266 | // 遍历结构体字段 267 | for i := 0; i < t.NumField(); i++ { 268 | field := t.Field(i) 269 | fieldValue := v.Field(i) 270 | 271 | // 如果是匿名字段,将其字段展开到当前结构体 272 | if field.Anonymous { 273 | if fieldValue.Kind() == reflect.Struct || fieldValue.Kind() == reflect.Array || fieldValue.Kind() == reflect.Slice { 274 | // 使用结构体类型的名称来追踪已经访问过的结构体 275 | typeName := field.Type.String() 276 | if visited[typeName] && field.Tag.Get("deep") != "true" { 277 | // 如果该结构体已经访问过,则跳过 278 | continue 279 | } 280 | visited[typeName] = true 281 | } 282 | // 如果匿名字段是指针,需要解引用 283 | if fieldValue.Kind() == reflect.Ptr && !fieldValue.IsNil() { 284 | stack = append(stack, generateStructTableIn(visited, fieldValue.Elem().Interface(), dataType, level+1, field.Anonymous, short)) 285 | } else if fieldValue.Kind() == reflect.Struct { 286 | stack = append(stack, generateStructTableIn(visited, fieldValue.Interface(), dataType, level+1, field.Anonymous, short)) 287 | } 288 | continue 289 | } 290 | 291 | // 获取字段信息 292 | name := field.Name 293 | fieldType := field.Type.String() 294 | tag := field.Tag.Get("json") 295 | if dataType == 2 { 296 | tag = field.Tag.Get("form") 297 | } else if dataType == 3 { 298 | tag = field.Tag.Get("header") 299 | } 300 | description := field.Tag.Get("desc") 301 | if description == "" { 302 | description = field.Tag.Get("comment") // 支持 `comment` 标签 303 | } 304 | required := field.Tag.Get("required") 305 | require := "否" 306 | if required == "true" { 307 | require = "是" 308 | } 309 | search := "-" 310 | searched := field.Tag.Get("search") 311 | if searched != "" { 312 | search = "是" 313 | } 314 | 315 | // 判断是否是结构体或者数组类型 316 | 317 | if short == false { 318 | md.WriteString(fmt.Sprintf("| %s | %s | %s | %s | \n", name, fieldType, tag, description)) 319 | } else { 320 | // 添加字段到表格 321 | md.WriteString(fmt.Sprintf("| %s | %s | %s | %s | %s | %s | \n", name, fieldType, tag, description, require, search)) 322 | } 323 | 324 | // 检查是否是标准库类型,如果是则跳过递归 325 | if isStandardLibraryType(field.Type) { 326 | 327 | continue 328 | } 329 | if fieldValue.Kind() == reflect.Struct || fieldValue.Kind() == reflect.Array || fieldValue.Kind() == reflect.Slice { 330 | // 使用结构体类型的名称来追踪已经访问过的结构体 331 | typeName := field.Type.String() 332 | if visited[typeName] && field.Tag.Get("deep") != "true" { 333 | // 如果该结构体已经访问过,则跳过 334 | continue 335 | } 336 | visited[typeName] = true 337 | } 338 | // 判断是否是可寻址的值类型 339 | 340 | //ptrAddr := fieldValue.Addr().Pointer() 341 | // 如果字段是指针类型,递归处理 342 | if fieldValue.Kind() == reflect.Ptr { 343 | // 如果是指针类型,解引用后递归处理 344 | if !fieldValue.IsNil() { 345 | stack = append(stack, generateStructTableIn(visited, fieldValue.Elem().Interface(), dataType, level+1, field.Anonymous, short)) 346 | } else { 347 | if short == false { 348 | md.WriteString(fmt.Sprintf("| %s | %s | 空指针 | - | \n", name, fieldType)) 349 | } else { 350 | // 添加字段到表格 351 | md.WriteString(fmt.Sprintf("| %s | %s | - | 空指针 | - |\n", name, fieldType)) 352 | } 353 | } 354 | } else if fieldValue.Kind() == reflect.Struct { 355 | // 如果是结构体类型,递归处理 356 | stack = append(stack, generateStructTableIn(visited, fieldValue.Interface(), dataType, level+1, field.Anonymous, short)) 357 | } else if fieldValue.Kind() == reflect.Slice { 358 | // 如果是切片,递归处理第一个元素 359 | elementType := field.Type.Elem() // 获取切片的元素类型 360 | if fieldValue.Len() > 0 { 361 | element := fieldValue.Index(0).Interface() 362 | if short == false { 363 | md.WriteString(fmt.Sprintf("| %s | []%s | %s | %s | \n", name, elementType.Name(), tag, description)) 364 | } else { 365 | // 添加字段到表格 366 | md.WriteString(fmt.Sprintf("| %s | []%s | %s | %s | %s | %s | \n", name, elementType.Name(), tag, description, require, search)) 367 | } 368 | stack = append(stack, generateStructTableIn(visited, element, dataType, level+1, field.Anonymous, short)) 369 | } else { 370 | // 如果切片为空,递归生成切片元素类型 371 | emptyElement := reflect.New(elementType).Elem().Interface() 372 | stack = append(stack, generateStructTableIn(visited, emptyElement, dataType, level+1, field.Anonymous, short)) 373 | } 374 | continue 375 | } 376 | 377 | } 378 | 379 | // 收集并返回最终结果 380 | res := md.String() 381 | for _, item := range stack { 382 | res += item 383 | } 384 | return res 385 | } 386 | 387 | func isStandardLibraryType(t reflect.Type) bool { 388 | // 如果类型是 nil,直接返回 false 389 | if t == nil { 390 | return false 391 | } 392 | if t == reflect.TypeOf(gorm.DeletedAt{}) { 393 | return true 394 | } 395 | // 对切片类型的元素类型检查 396 | if t.Kind() == reflect.Slice { 397 | t = t.Elem() 398 | } 399 | 400 | // 获取包路径 401 | pkgPath := t.PkgPath() 402 | 403 | // Go 标准库的类型通常在 "" 或 "time" 等包路径中 404 | if pkgPath == "" || pkgPath == "time" { 405 | return true 406 | } 407 | 408 | return false 409 | } 410 | -------------------------------------------------------------------------------- /pkg/func/bloomfilter/bloomfilter.go: -------------------------------------------------------------------------------- 1 | package bloomfilter 2 | 3 | import ( 4 | "github.com/hugh2632/bloomfilter" 5 | ) 6 | 7 | // 布隆过滤器 8 | type Bloom struct { 9 | Bloom bloomfilter.IFilter 10 | } 11 | 12 | // 默认布隆过滤器 13 | func NewBloom() *Bloom { 14 | return &Bloom{Bloom: bloomfilter.NewMemoryFilter(make([]byte, 10240), bloomfilter.DefaultHash...)} 15 | } 16 | 17 | func (this *Bloom) Exists(key string) bool { 18 | return this.Bloom.Exists([]byte(key)) 19 | } 20 | func (this *Bloom) Clear() error { 21 | return this.Bloom.Clear() 22 | } 23 | func (this *Bloom) Add(key string) { 24 | this.Bloom.Push([]byte(key)) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/func/breaker/breaker.go: -------------------------------------------------------------------------------- 1 | package breaker 2 | 3 | import ( 4 | "github.com/sony/gobreaker" 5 | "time" 6 | ) 7 | 8 | var DefaultToTrip = func(counts gobreaker.Counts) bool { 9 | failureRatio := float64(counts.TotalFailures) / float64(counts.Requests) 10 | return counts.Requests >= 6 && failureRatio >= 0.6 11 | } 12 | 13 | func NewCircuitBreaker(name string, MaxRequests uint32, Interval time.Duration, Timeout time.Duration) *gobreaker.CircuitBreaker { 14 | var cbSettings gobreaker.Settings 15 | cbSettings.Name = name 16 | cbSettings.MaxRequests = MaxRequests 17 | cbSettings.Interval = Interval 18 | cbSettings.Timeout = Timeout 19 | cbSettings.ReadyToTrip = DefaultToTrip 20 | return gobreaker.NewCircuitBreaker(cbSettings) 21 | } 22 | 23 | func NewCircuitBreakerWithSettings(settings gobreaker.Settings) *gobreaker.CircuitBreaker { 24 | return gobreaker.NewCircuitBreaker(settings) 25 | } 26 | 27 | /* 28 | demo 29 | this.Breaker.Execute(this.Job) 30 | **/ 31 | -------------------------------------------------------------------------------- /pkg/func/cache/interface.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // 缓存接口 8 | type Cacher interface { 9 | Set(k string, x interface{}, d time.Duration) 10 | Get(k string, obj interface{}) bool 11 | Delete(k string) bool 12 | } 13 | 14 | // 实际缓存服务 15 | type Service struct { 16 | cache Cacher 17 | } 18 | 19 | func ServiceCache(cache Cacher) *Service { 20 | return &Service{cache: cache} 21 | } 22 | 23 | // 缓存业务函数 24 | func FindWithCallBack[T any](key string, time time.Duration, CacheService *Service, fn func() T) T { 25 | var data T 26 | ok := CacheService.cache.Get(key, &data) 27 | if ok { 28 | return data 29 | } 30 | data = fn() 31 | CacheService.cache.Set(key, data, time) 32 | return data 33 | } 34 | -------------------------------------------------------------------------------- /pkg/func/cache/memory/cache.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/patrickmn/go-cache" 6 | "time" 7 | ) 8 | 9 | const ( 10 | NoExpiration time.Duration = -1 11 | DefaultExpiration time.Duration = 0 12 | ) 13 | 14 | // 内存缓存 15 | type Cache struct { 16 | MemoryCache *cache.Cache 17 | } 18 | 19 | func NewCache() *Cache { 20 | cache := cache.New(5*time.Minute, 10*time.Minute) 21 | return &Cache{MemoryCache: cache} 22 | } 23 | 24 | // 接口方法 25 | func (this *Cache) Get(k string, obj interface{}) bool { 26 | data, found := this.MemoryCache.Get(k) 27 | if found { 28 | err := json.Unmarshal(data.([]byte), obj) 29 | if err != nil { 30 | return false 31 | } 32 | return found 33 | } 34 | return false 35 | } 36 | func (this *Cache) Set(k string, x interface{}, d time.Duration) { 37 | b, _ := json.Marshal(x) 38 | this.MemoryCache.Set(k, b, d) 39 | } 40 | 41 | func (this *Cache) Delete(k string) bool { 42 | this.MemoryCache.Delete(k) 43 | return true 44 | } 45 | -------------------------------------------------------------------------------- /pkg/func/cache/memory/cache_test.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | type Test struct { 9 | Name string 10 | } 11 | 12 | func TestNewCache(t *testing.T) { 13 | c := NewCache() 14 | // Set the value of the key "foo" to "bar", with the default expiration time 15 | 16 | a := &Test{ 17 | Name: "caasc", 18 | } 19 | c.Set("a", a, NoExpiration) 20 | c.Set("a1", "11131", NoExpiration) 21 | b := &Test{} 22 | c.Get("a", b) 23 | td := "" 24 | c.Get("a1", &td) 25 | fmt.Println(b.Name) 26 | fmt.Println(td) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/func/cache/redis/cache.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/redis/go-redis/v9" 7 | "time" 8 | ) 9 | 10 | // redis 缓存 11 | type Cache struct { 12 | redis *redis.Client 13 | } 14 | 15 | const ( 16 | NoExpiration time.Duration = 0 17 | DefaultExpiration time.Duration = 5 * time.Minute 18 | ) 19 | 20 | func NewCache(redisCache *redis.Client) *Cache { 21 | return &Cache{ 22 | redis: redisCache, 23 | } 24 | } 25 | 26 | // 接口方法 27 | func (this *Cache) Set(k string, x interface{}, d time.Duration) { 28 | b, _ := json.Marshal(x) 29 | this.redis.Set( 30 | context.Background(), 31 | k, 32 | b, 33 | d, 34 | ) 35 | } 36 | func (this *Cache) Get(k string, obj interface{}) bool { 37 | b, err := this.redis.Get(context.Background(), k).Result() 38 | if err == nil { 39 | err = json.Unmarshal([]byte(b), obj) 40 | if err == nil { 41 | return true 42 | } 43 | } 44 | return false 45 | } 46 | 47 | func (this *Cache) Delete(k string) bool { 48 | err := this.redis.Del(context.Background(), k).Err() 49 | if err == nil { 50 | return true 51 | } 52 | return false 53 | } 54 | -------------------------------------------------------------------------------- /pkg/func/cache/redis/cache_test.go: -------------------------------------------------------------------------------- 1 | package redis 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | type Test struct { 8 | Name string 9 | } 10 | 11 | func TestNewCache(t *testing.T) { 12 | 13 | //c := NewCache() 14 | //// Set the value of the key "foo" to "bar", with the default expiration time 15 | // 16 | //a := &Test{ 17 | // Name: "caasc", 18 | //} 19 | //c.Set("a", a, NoExpiration) 20 | //c.Set("a1", "1111", NoExpiration) 21 | //b := &Test{} 22 | //c.Get("a", b) 23 | //td := "" 24 | //c.Get("a1", &td) 25 | //fmt.Println(b.Name) 26 | //fmt.Println(td) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/func/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "log" 6 | ) 7 | 8 | // 配置中心 9 | func Config() *viper.Viper { 10 | viper.SetDefault("port", "8888") 11 | viper.SetDefault("logPath", "./logs/app.log") 12 | viper.SetDefault("development", true) 13 | viper.SetConfigFile("config.yaml") // 指定配置文件路径 14 | viper.SetConfigName("config") // 配置文件名称(无扩展名) 15 | viper.SetConfigType("yaml") // 如果配置文件的名称中没有扩展名,则需要配置此项 16 | viper.AddConfigPath(".") // 还可以在工作目录中查找配置 17 | err := viper.ReadInConfig() // 查找并读取配置文件 18 | if err != nil { // 处理读取配置文件的错误 19 | log.Fatalf("Fatal error config file: %s \n", err.Error()) 20 | } 21 | //业务参数 22 | viper.SetDefault("Timeout", 3000) 23 | viper.SetDefault("LimitRate", 512) 24 | viper.SetDefault("LimitBucket", 1024) 25 | viper.SetDefault("LRULimit", 4096*5) 26 | return viper.GetViper() 27 | } 28 | -------------------------------------------------------------------------------- /pkg/func/cron/cron.go: -------------------------------------------------------------------------------- 1 | package cronInit 2 | 3 | import ( 4 | "github.com/robfig/cron" 5 | ) 6 | 7 | type DefaultCron struct { 8 | *cron.Cron 9 | } 10 | 11 | // 定时任务函数 12 | func NewDefaultCron() *DefaultCron { 13 | return &DefaultCron{ 14 | Cron: cron.New(), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pkg/func/ctl/base.go: -------------------------------------------------------------------------------- 1 | package ctl 2 | 3 | import ( 4 | "errors" 5 | "github.com/gin-gonic/gin" 6 | "github.com/wike2019/wike_go/common" 7 | "io" 8 | "net/http" 9 | "strconv" 10 | ) 11 | 12 | type Controller struct { 13 | Page 14 | *gin.Context 15 | query interface{} 16 | body interface{} 17 | header interface{} 18 | output interface{} 19 | Data []byte 20 | } 21 | 22 | func (r *Controller) clear() { 23 | r.query = nil 24 | r.body = nil 25 | r.header = nil 26 | r.output = nil 27 | r.Data = nil 28 | } 29 | func (r *Controller) SetDoc(input interface{}, output interface{}) { 30 | r.clear() 31 | r.getFuncMateInfo(input, output) 32 | 33 | } 34 | func (r *Controller) SetDocRaw(paramInstance interface{}, bodyInstance interface{}, headerInstance interface{}, returnInstance interface{}) { 35 | r.clear() 36 | r.query = paramInstance 37 | r.body = bodyInstance 38 | r.header = headerInstance 39 | r.output = returnInstance 40 | } 41 | func (r *Controller) SetEmpty() { 42 | r.clear() 43 | r.SetDocRaw(common.Empty{}, common.Empty{}, common.Empty{}, DataList[string]{}) 44 | } 45 | func (r *Controller) SetEmptyWithHeader(header interface{}) { 46 | r.clear() 47 | r.SetDocRaw(common.Empty{}, common.Empty{}, header, DataList[string]{}) 48 | } 49 | func (r *Controller) GetInnerData() (interface{}, interface{}, interface{}, interface{}) { 50 | defer r.clear() 51 | return r.query, r.body, r.header, r.output 52 | } 53 | func (r *Controller) PageInit() { 54 | queryStr := r.Context.Query("page") 55 | countStr := r.Context.Query("count") 56 | // 转换为 int 57 | if queryStr != "" { 58 | page, err := strconv.Atoi(queryStr) 59 | Error(err, 400) 60 | r.CurPage = page 61 | } 62 | if countStr != "" { 63 | count, err := strconv.Atoi(countStr) 64 | Error(err, 400) 65 | r.Count = count 66 | } 67 | if r.CurPage == 0 { 68 | r.CurPage = 1 69 | } 70 | if r.Count == 0 { 71 | r.Count = 10 72 | } 73 | r.Offset = (r.CurPage - 1) * r.Count 74 | } 75 | func (r *Controller) SetContext(ctx *gin.Context) *Controller { 76 | clone := new(Controller) 77 | body, err := io.ReadAll(ctx.Request.Body) 78 | if err != nil { 79 | Error(errors.New("参数错误"+err.Error()), 400) 80 | return nil 81 | } 82 | clone.Data = body 83 | clone.Context = ctx 84 | return clone 85 | } 86 | func (r *Controller) OK(data interface{}) { 87 | r.JSON(http.StatusOK, data) 88 | } 89 | func (r *Controller) OKWithMsg(msg string) { 90 | r.JSON(http.StatusOK, gin.H{ 91 | "msg": msg, 92 | "traceId": r.GetString("trace_id"), 93 | "code": Success, 94 | }) 95 | } 96 | 97 | func OK(r *gin.Context, data interface{}) { 98 | r.JSON(http.StatusOK, data) 99 | } 100 | func OKWithMsg(r *gin.Context, msg string) { 101 | r.JSON(http.StatusOK, gin.H{ 102 | "msg": msg, 103 | "traceId": r.GetString("trace_id"), 104 | "code": Success, 105 | }) 106 | } 107 | func List[T any](msg string, list []T, context *Controller) *PageList[T] { 108 | return &PageList[T]{ 109 | Data: list, 110 | Page: context.Page, 111 | Msg: msg, 112 | TraceId: context.GetString("trace_id"), 113 | Code: Success, 114 | } 115 | } 116 | func Item[T any](msg string, list T, context *Controller) *DataList[T] { 117 | return &DataList[T]{ 118 | Data: list, 119 | Msg: msg, 120 | TraceId: context.GetString("trace_id"), 121 | Code: Success, 122 | } 123 | } 124 | 125 | func (r *Controller) Failed(code errorCode, msg string) { 126 | errMsg := codeMsg[code] + ": " + msg 127 | if code != Success { 128 | r.Set("error_code", int(code)) 129 | r.Set("error_msg", msg) 130 | } 131 | r.AbortWithStatusJSON(http.StatusOK, gin.H{ 132 | "code": code, 133 | "msg": errMsg, 134 | "data": nil, 135 | "trace_id": r.GetString("trace_id"), 136 | }) 137 | } 138 | -------------------------------------------------------------------------------- /pkg/func/ctl/const.go: -------------------------------------------------------------------------------- 1 | package ctl 2 | 3 | type errorCode int 4 | 5 | // 核心控制器基础方法 6 | const ( 7 | Success errorCode = 200 8 | Failed errorCode = 500 9 | ParamError errorCode = 400 10 | NotFound errorCode = 404 11 | UnAuthorized errorCode = 401 12 | ) 13 | 14 | var codeMsg = map[errorCode]string{ 15 | Success: "正常", 16 | Failed: "系统异常", 17 | ParamError: "参数错误", 18 | NotFound: "记录不存在", 19 | UnAuthorized: "未授权", 20 | } 21 | -------------------------------------------------------------------------------- /pkg/func/ctl/curd.go: -------------------------------------------------------------------------------- 1 | package ctl 2 | 3 | import ( 4 | "fmt" 5 | "gorm.io/gorm" 6 | "reflect" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | type DBChange func(db *gorm.DB) *gorm.DB 12 | 13 | // GetGormColumnMap 解析结构体并返回字段名到数据库列名的映射 14 | func GetGormColumnMap(obj interface{}, db *gorm.DB) (*gorm.DB, error) { 15 | columnMap := make(map[string]string) 16 | 17 | // 获取结构体的类型 18 | t := reflect.TypeOf(obj) 19 | v := reflect.ValueOf(obj) 20 | // 如果是指针,获取其元素 21 | if t.Kind() == reflect.Ptr { 22 | t = t.Elem() 23 | v = v.Elem() 24 | } 25 | 26 | // 确保传入的是结构体 27 | if t.Kind() != reflect.Struct { 28 | return nil, fmt.Errorf("传入的参数不是结构体") 29 | } 30 | 31 | // 遍历结构体的所有字段 32 | for i := 0; i < t.NumField(); i++ { 33 | field := t.Field(i) 34 | value := v.Field(i) 35 | // 跳过匿名字段 36 | if field.Anonymous { 37 | continue 38 | } 39 | 40 | // 获取字段名 41 | fieldName := field.Name 42 | 43 | // 获取 GORM 标签 44 | gormTag := field.Tag.Get("gorm") 45 | 46 | columnName := "" 47 | 48 | if gormTag != "" { 49 | // 解析标签,查找是否有 "column" 指定 50 | tags := strings.Split(gormTag, ";") 51 | for _, tag := range tags { 52 | tag = strings.TrimSpace(tag) 53 | if strings.HasPrefix(tag, "column:") { 54 | columnName = strings.TrimPrefix(tag, "column:") 55 | break 56 | } 57 | } 58 | } 59 | 60 | if columnName == "" { 61 | // 如果没有指定 column 标签,使用默认的命名规则 62 | columnName = CamelCaseToSnakeCase(fieldName) 63 | } 64 | 65 | //columnMap[fieldName] = columnName 66 | orderTag := field.Tag.Get("order") 67 | if orderTag != "" { 68 | db = db.Order(fmt.Sprintf(" `%s` %s ", columnName, orderTag)) 69 | } 70 | 71 | searchTag := field.Tag.Get("search") 72 | 73 | op := field.Tag.Get("op") 74 | if op == "" { 75 | op = "=" 76 | } 77 | 78 | if searchTag == "true" && !value.IsZero() { 79 | columnMap[fieldName] = columnName 80 | db = db.Where(fmt.Sprintf(" `%s` %s ?", columnName, op), value.Interface()) 81 | } 82 | if searchTag == "or" && !value.IsZero() { 83 | columnMap[fieldName] = columnName 84 | db = db.Or(fmt.Sprintf(" `%s` %s ?", columnName, op), value.Interface()) 85 | } 86 | 87 | if searchTag == "like" && !value.IsZero() { 88 | columnMap[fieldName] = columnName 89 | db = db.Where(fmt.Sprintf(" `%s` like ?", columnName), fmt.Sprintf("%%%v%%", value.Interface())) 90 | } 91 | } 92 | 93 | return db, nil 94 | } 95 | 96 | // CamelCaseToSnakeCase 将驼峰式命名转换为下划线式命名 97 | func CamelCaseToSnakeCase(str string) string { 98 | var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") 99 | var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") 100 | 101 | snake := matchFirstCap.ReplaceAllString(str, "${1}_${2}") 102 | snake = matchAllCap.ReplaceAllString(snake, "${1}_${2}") 103 | return strings.ToLower(snake) 104 | } 105 | 106 | func CreateItem[T any](db *gorm.DB, obj T) error { 107 | return db.Create(obj).Error 108 | } 109 | func UpdateItem[T any](db *gorm.DB, obj T, id uint) error { 110 | var model T 111 | return db.Model(model).Where("id=?", id).Debug().Updates(obj).Error 112 | } 113 | func DeleteItem[T any](db *gorm.DB, id uint) error { 114 | var model T 115 | return db.Model(model).Where("id=?", id).Debug().Delete(&model).Error 116 | } 117 | 118 | func ListItem[T any](db *gorm.DB, Offset, Count int, obj interface{}, call DBChange) ([]T, int64, error) { 119 | var list []T 120 | var model T 121 | count := int64(0) 122 | db = db.Model(model) 123 | db, err := GetGormColumnMap(obj, db) 124 | if err != nil { 125 | return list, 0, err 126 | } 127 | if call != nil { 128 | db = call(db) 129 | } 130 | err = db.Count(&count).Error 131 | if err != nil { 132 | return list, 0, err 133 | } 134 | 135 | err = db.Limit(Count).Offset(Offset).Debug().Find(&list).Error 136 | if err != nil { 137 | return list, 0, err 138 | } 139 | return list, count, nil 140 | } 141 | func GetItemByID[T any](db *gorm.DB, id int, call DBChange) (T, error) { 142 | var res T 143 | if call != nil { 144 | db = call(db) 145 | } 146 | err := db.Where("id=?", id).First(&res).Error 147 | return res, err 148 | } 149 | 150 | func GetItem[T any](db *gorm.DB, id uint, call DBChange) (T, error) { 151 | var res T 152 | var model T 153 | db = db.Model(model) 154 | 155 | if call != nil { 156 | db = call(db) 157 | } 158 | err := db.Debug().Where("id=?", id).First(&res).Error 159 | 160 | if err != nil { 161 | return res, err 162 | } 163 | return res, nil 164 | } 165 | func GetItemByFunc[T any](db *gorm.DB, call DBChange) (T, error) { 166 | var res T 167 | var model T 168 | db = db.Model(model) 169 | 170 | if call != nil { 171 | db = call(db) 172 | } 173 | err := db.Debug().First(&res).Error 174 | 175 | if err != nil { 176 | return res, err 177 | } 178 | return res, nil 179 | } 180 | func ListItemAll[T any](db *gorm.DB, obj interface{}, call DBChange) ([]T, error) { 181 | var list []T 182 | var model T 183 | db = db.Model(model) 184 | db, err := GetGormColumnMap(obj, db) 185 | if err != nil { 186 | return list, err 187 | } 188 | if call != nil { 189 | db = call(db) 190 | } 191 | err = db.Debug().Find(&list).Error 192 | if err != nil { 193 | return list, err 194 | } 195 | return list, nil 196 | } 197 | -------------------------------------------------------------------------------- /pkg/func/ctl/error.go: -------------------------------------------------------------------------------- 1 | package ctl 2 | 3 | import ( 4 | "errors" 5 | "gorm.io/gorm" 6 | ) 7 | 8 | type StatusError struct { 9 | Msg string `json:"msg"` 10 | Code int `json:"code"` 11 | } 12 | 13 | func Error(err error, code int) { 14 | if err != nil { 15 | panic(StatusError{Msg: err.Error(), Code: code}) 16 | } 17 | } 18 | func (r *Controller) IsExist(err error) bool { 19 | if errors.Is(err, gorm.ErrRecordNotFound) { 20 | return false 21 | } 22 | return true 23 | } 24 | func IsExist(err error) bool { 25 | if errors.Is(err, gorm.ErrRecordNotFound) { 26 | return false 27 | } 28 | return true 29 | } 30 | -------------------------------------------------------------------------------- /pkg/func/ctl/fastRouter/core.go: -------------------------------------------------------------------------------- 1 | package fastRouter 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gin-gonic/gin" 6 | "github.com/wike2019/wike_go/common" 7 | "github.com/wike2019/wike_go/pkg/core" 8 | "github.com/wike2019/wike_go/pkg/func/ctl" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | func FastRouter[Q common.IDSearcher, T any, H any](DB *gorm.DB, DBInstance *core.CoreDb, r *gin.RouterGroup, GCore *core.GCore, path string, name string, groupName string, prefix string) { 13 | res := &FastInstance[Q, T, H]{ 14 | DB: DB, 15 | path: path, 16 | name: name, 17 | prefix: prefix, 18 | groupName: groupName, 19 | DBInstance: DBInstance, 20 | } 21 | res.Build(r, GCore) 22 | } 23 | 24 | type FastInstance[Q common.IDSearcher, T any, H any] struct { 25 | ctl.Controller 26 | DB *gorm.DB 27 | path string 28 | name string 29 | groupName string 30 | prefix string 31 | DBInstance *core.CoreDb 32 | } 33 | 34 | func (this *FastInstance[Q, T, H]) Build(r *gin.RouterGroup, GCore *core.GCore) { 35 | var query Q 36 | var data T 37 | var header H 38 | this.SetDocRaw(common.Empty{}, data, header, ctl.DataList[string]{}) 39 | GCore.PostWithRbac(r, this, this.groupName, this.path+"/create", this.Create, "添加"+this.name) 40 | this.SetDocRaw(query, data, header, ctl.DataList[string]{}) 41 | GCore.PostWithRbac(r, this, this.groupName, this.path+"/update", this.Update, "修改"+this.name) 42 | this.SetDocRaw(query, common.Empty{}, header, ctl.DataList[string]{}) 43 | GCore.DelWithRbac(r, this, this.groupName, this.path+"/delete", this.Del, "删除"+this.name) 44 | } 45 | 46 | func (this *FastInstance[Q, T, H]) Path() string { 47 | if this.prefix[0] != '/' { 48 | this.prefix = "/" + this.prefix 49 | } 50 | return this.prefix 51 | } 52 | 53 | func (this *FastInstance[Q, T, H]) Create(context *gin.Context) interface{} { 54 | c := this.SetContext(context) 55 | var data T 56 | err := json.Unmarshal(c.Data, &data) 57 | ctl.Error(err, 400) 58 | err = ctl.CreateItem(this.DB, &data) 59 | ctl.Error(err, 400) 60 | return "添加" + this.name + "成功" 61 | } 62 | func (this *FastInstance[Q, T, H]) Update(context *gin.Context) interface{} { 63 | var query Q 64 | var data T 65 | err := context.ShouldBindQuery(&query) 66 | ctl.Error(err, 400) 67 | c := this.SetContext(context) 68 | err = json.Unmarshal(c.Data, &data) 69 | ctl.Error(err, 400) 70 | id := query.GetID() 71 | err = ctl.UpdateItem[T](this.DB, data, id) 72 | ctl.Error(err, 400) 73 | return "修改" + this.name + "成功" 74 | } 75 | 76 | func (this *FastInstance[Q, T, H]) Del(context *gin.Context) interface{} { 77 | var query Q 78 | err := context.ShouldBindQuery(&query) 79 | ctl.Error(err, 400) 80 | this.SetContext(context) 81 | id := query.GetID() 82 | err = ctl.DeleteItem[T](this.DB, id) 83 | ctl.Error(err, 400) 84 | return "删除" + this.name + "成功" 85 | } 86 | -------------------------------------------------------------------------------- /pkg/func/ctl/func.go: -------------------------------------------------------------------------------- 1 | package ctl 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | func (r *Controller) getFuncMateInfo(input interface{}, output interface{}) { 9 | // 获取函数的类型信息 10 | funcVal := reflect.ValueOf(output) 11 | funcType := funcVal.Type() 12 | 13 | funcInVal := reflect.ValueOf(input) 14 | funcInType := funcInVal.Type() 15 | 16 | // 确保传入的确实是一个函数 17 | if funcType.Kind() != reflect.Func { 18 | panic("类型错误") 19 | } 20 | if funcInType.Kind() != reflect.Func { 21 | panic("类型错误") 22 | } 23 | 24 | // 获取函数的参数类型和返回值类型 25 | if funcType.NumOut() != 2 { 26 | panic(fmt.Sprintf("%s-%d", "数据不合法", funcType.NumOut())) 27 | } 28 | if funcInType.NumOut() != 4 { 29 | panic(fmt.Sprintf("%s-%d", "数据不合法", funcInType.NumOut())) 30 | } 31 | // 获取参数类型(假设为结构体A) 32 | paramType := funcInType.Out(0).Elem() 33 | bodyType := funcInType.Out(1).Elem() 34 | headerType := funcInType.Out(2).Elem() 35 | // 获取返回值类型(假设为结构体B) 36 | returnType := funcType.Out(0).Elem() 37 | 38 | // 使用反射创建参数类型和返回值类型的实例 39 | paramInstance := reflect.New(paramType) 40 | bodyInstance := reflect.New(bodyType) 41 | headerInstance := reflect.New(headerType) 42 | returnInstance := reflect.New(returnType) 43 | r.query = paramInstance.Interface() 44 | r.body = bodyInstance.Interface() 45 | r.header = headerInstance.Interface() 46 | r.output = returnInstance.Interface() 47 | } 48 | -------------------------------------------------------------------------------- /pkg/func/ctl/json.go: -------------------------------------------------------------------------------- 1 | package ctl 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "errors" 7 | ) 8 | 9 | type Arr []interface{} 10 | 11 | // MarshalJSON 自定义 JSON 序列化 12 | func (t Arr) MarshalJSON() ([]byte, error) { 13 | return json.Marshal([]interface{}(t)) 14 | } 15 | 16 | // UnmarshalJSON 自定义 JSON 反序列化 17 | func (t *Arr) UnmarshalJSON(data []byte) error { 18 | var m []interface{} 19 | if err := json.Unmarshal(data, &m); err != nil { 20 | return err 21 | } 22 | *t = m 23 | return nil 24 | } 25 | 26 | // Value 将 Json 转换为数据库存储的字符串 27 | func (t Arr) Value() (driver.Value, error) { 28 | if t == nil { 29 | return "[]", nil 30 | } 31 | return json.Marshal(t) 32 | } 33 | 34 | // Scan 从数据库中加载 JSON 数据 35 | func (t *Arr) Scan(value interface{}) error { 36 | switch v := value.(type) { 37 | case string: // 如果是字符串 38 | return t.parseJsonString(v) 39 | case []byte: // 如果是字节数组 40 | return t.parseJsonString(string(v)) 41 | default: // 其他类型无法处理 42 | return errors.New("failed to scan Json: value is not a valid JSON type") 43 | } 44 | } 45 | 46 | // 解析 JSON 字符串为 []interface{} 47 | func (t *Arr) parseJsonString(str string) error { 48 | var m []interface{} 49 | if err := json.Unmarshal([]byte(str), &m); err != nil { 50 | return err 51 | } 52 | *t = m 53 | return nil 54 | } 55 | 56 | // Json 定义为 map[string]interface{} 的别名 57 | type Json map[string]interface{} 58 | 59 | // MarshalJSON 自定义 JSON 序列化 60 | func (t Json) MarshalJSON() ([]byte, error) { 61 | return json.Marshal(map[string]interface{}(t)) 62 | } 63 | 64 | // UnmarshalJSON 自定义 JSON 反序列化 65 | func (t *Json) UnmarshalJSON(data []byte) error { 66 | var m map[string]interface{} 67 | if err := json.Unmarshal(data, &m); err != nil { 68 | return err 69 | } 70 | *t = m 71 | return nil 72 | } 73 | 74 | // Value 将 Json 转换为数据库存储的字符串 75 | func (t Json) Value() (driver.Value, error) { 76 | if t == nil { 77 | return "{}", nil 78 | } 79 | return json.Marshal(t) 80 | } 81 | 82 | // Scan 从数据库中加载 JSON 数据 83 | func (t *Json) Scan(value interface{}) error { 84 | switch v := value.(type) { 85 | case string: // 如果是字符串 86 | return t.parseJsonString(v) 87 | case []byte: // 如果是字节数组 88 | return t.parseJsonString(string(v)) 89 | default: // 其他类型无法处理 90 | return errors.New("failed to scan Json: value is not a valid JSON type") 91 | } 92 | } 93 | 94 | // 解析 JSON 字符串为 map[string]interface{} 95 | func (t *Json) parseJsonString(str string) error { 96 | var m map[string]interface{} 97 | if err := json.Unmarshal([]byte(str), &m); err != nil { 98 | return err 99 | } 100 | *t = m 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /pkg/func/ctl/page.go: -------------------------------------------------------------------------------- 1 | package ctl 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | type Page struct { 8 | SumPage int `json:"sumPage" desc:"总页数"` // 总页数 9 | SumCount int `json:"sumCount" desc:"总条数"` // 总条数 10 | CurPage int `json:"page" form:"page" desc:"当前页" required:"true" ` // 当前页 11 | Offset int `json:"-"` // 起始量 12 | Count int `json:"count" form:"count" desc:"每页返回数量" required:"true"` // 每页返回数量 13 | } 14 | 15 | // PageTime 适用 列表-分页-时间范围 16 | type Time struct { 17 | StartTime int64 `json:"startTime" form:"startTime"` // 开始时间 18 | EndTime int64 `json:"endTime" form:"endTime"` // 结束时间 19 | } 20 | 21 | // FormatPage 格式化分页数据-起始量 22 | 23 | // FormatTotal 格式化分页数据-总页数 24 | func (p *Page) FormatTotal(total int64) { 25 | p.SumCount = int(total) 26 | if p.Count > 0 { 27 | p.SumPage = int(math.Ceil(float64(p.SumCount) / float64(p.Count))) 28 | } else { 29 | p.SumPage = 1 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/func/ctl/repeatCheck.go: -------------------------------------------------------------------------------- 1 | package ctl 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/wike2019/wike_go/plugin" 6 | "gorm.io/gorm" 7 | "time" 8 | ) 9 | 10 | // 一致性避免重试不幂等 11 | func RepeatCheck(context *gin.Context, tx *gorm.DB) error { 12 | traceId, _ := context.Get("trace_id") 13 | data := &plugin.RepeatCheck{ 14 | TraceId: traceId.(string), 15 | } 16 | return tx.Create(data).Error 17 | } 18 | 19 | var ClearChan chan struct{} 20 | 21 | func init() { 22 | ClearChan = make(chan struct{}, 10) 23 | } 24 | 25 | // todo 26 | func Clear(db *gorm.DB) { 27 | for _ = range ClearChan { 28 | //定期清理老数据 29 | for { 30 | result := db.Where("time < ?", time.Now().AddDate(0, 0, -2)).Limit(5000).Delete(&plugin.RepeatCheck{}) 31 | if result.Error != nil { 32 | continue 33 | } 34 | if result.RowsAffected < 5000 { 35 | break 36 | } 37 | } 38 | } 39 | } 40 | 41 | // 开启幂等服务 42 | func RepeatCheckStart(db *gorm.DB) { 43 | db.AutoMigrate(&plugin.RepeatCheck{}) 44 | Clear(db) 45 | } 46 | 47 | // 关闭幂等服务 48 | func RepeatCheckStop() { 49 | close(ClearChan) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/func/ctl/returnType.go: -------------------------------------------------------------------------------- 1 | package ctl 2 | 3 | type PageList[T any] struct { 4 | Data []T `json:"data"` 5 | Page Page `json:"page"` 6 | Msg string `json:"msg"` 7 | TraceId string `json:"trace_id" desc:"追踪id"` 8 | Code errorCode `json:"code"` 9 | } 10 | 11 | type DataList[T any] struct { 12 | Msg string `json:"msg"` 13 | TraceId string `json:"trace_id" desc:"追踪id"` 14 | Code errorCode `json:"code"` 15 | Data T `json:"data"` 16 | } 17 | -------------------------------------------------------------------------------- /pkg/func/ctl/time.go: -------------------------------------------------------------------------------- 1 | package ctl 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type LocalTimeInt int64 12 | 13 | func (t LocalTimeInt) MarshalJSON() ([]byte, error) { 14 | tTime := time.Unix(int64(t), 0) 15 | // 如果时间值是空或者0值 返回为null 如果写空字符串会报错 16 | if &t == nil || tTime.IsZero() { 17 | return []byte("null"), nil 18 | } 19 | return []byte(fmt.Sprintf("\"%s\"", tTime.Format("2006-01-02 15:04:05"))), nil 20 | } 21 | 22 | // UnmarshalJSON 为 LocalTimeInt 类型自定义 JSON 反序列化方法 23 | func (lti *LocalTimeInt) UnmarshalJSON(data []byte) error { 24 | // 将 JSON 中的数字解析为 int64 25 | var timestamp int64 26 | if err := json.Unmarshal(data, ×tamp); err != nil { 27 | return err 28 | } 29 | 30 | // 将 int64 的时间戳转换为 time.Time,然后更新 LocalTimeInt 的值 31 | *lti = LocalTimeInt(timestamp) 32 | return nil 33 | } 34 | 35 | // ToTime 方法将 LocalTimeInt 类型转换为 time.Time 36 | func (lti LocalTimeInt) ToTime() time.Time { 37 | return time.Unix(int64(lti), 0) 38 | } 39 | 40 | type LocalTime time.Time 41 | 42 | func (t LocalTime) Value() (driver.Value, error) { 43 | var zeroTime time.Time 44 | tlt := time.Time(t) 45 | if tlt.UnixNano() == zeroTime.UnixNano() { 46 | return nil, nil 47 | } 48 | return tlt, nil 49 | } 50 | 51 | func (t *LocalTime) Scan(v interface{}) error { 52 | if value, ok := v.(time.Time); ok { 53 | *t = LocalTime(value) 54 | return nil 55 | } 56 | return fmt.Errorf("can not convert %v to timestamp", v) 57 | } 58 | 59 | func (t *LocalTime) String() string { 60 | // 如果时间 null 那么我们需要把返回的值进行修改 61 | if t == nil || t.IsZero() { 62 | return "" 63 | } 64 | return fmt.Sprintf("%s", time.Time(*t).Format("2006-01-02 15:04:05")) 65 | } 66 | 67 | func (t *LocalTime) IsZero() bool { 68 | return time.Time(*t).IsZero() 69 | } 70 | 71 | func (t *LocalTime) UnmarshalJSON(data []byte) error { 72 | 73 | if string(data) == "null" { 74 | return nil 75 | } 76 | var err error 77 | //前端接收的时间字符串 78 | str := string(data) 79 | //去除接收的str收尾多余的" 80 | timeStr := strings.Trim(str, "\"") 81 | 82 | local, _ := time.LoadLocation("Asia/Shanghai") 83 | 84 | t1, err := time.ParseInLocation("2006-01-02 15:04:05", timeStr, local) 85 | *t = LocalTime(t1) 86 | return err 87 | } 88 | 89 | func (t LocalTime) MarshalJSON() ([]byte, error) { 90 | tTime := time.Time(t) 91 | // 如果时间值是空或者0值 返回为null 如果写空字符串会报错 92 | if &t == nil || t.IsZero() { 93 | return []byte("null"), nil 94 | } 95 | return []byte(fmt.Sprintf("\"%s\"", tTime.Format("2006-01-02 15:04:05"))), nil 96 | } 97 | -------------------------------------------------------------------------------- /pkg/func/ctl/ws.go: -------------------------------------------------------------------------------- 1 | package ctl 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/gorilla/websocket" 6 | "github.com/wike2019/wike_go/pkg/utils" 7 | "net/http" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | const Default = "default" 13 | 14 | // websocket 广播函数 15 | var wsUpGrader = websocket.Upgrader{ 16 | ReadBufferSize: 1024, 17 | WriteBufferSize: 1024, 18 | // 允许跨域 19 | CheckOrigin: func(r *http.Request) bool { 20 | return true 21 | }, 22 | } 23 | 24 | // websocket 注册函数 25 | func WsHandler(c *gin.Context) { 26 | roomId := c.DefaultQuery("room", Default) 27 | conn, err := wsUpGrader.Upgrade(c.Writer, c.Request, nil) 28 | if err != nil { 29 | http.NotFound(c.Writer, c.Request) 30 | return 31 | } 32 | record := WC.Data.Get(roomId) 33 | record = append(record, conn) 34 | WC.Data.Set(roomId, record) 35 | } 36 | 37 | var WC *WsClient 38 | 39 | func init() { 40 | WC = &WsClient{ 41 | Data: utils.NewMap[[]*websocket.Conn](), 42 | } 43 | go func() { 44 | for { 45 | time.Sleep(10 * time.Minute) 46 | for _, k := range WC.Data.Keys() { 47 | ping(k) 48 | } 49 | } 50 | }() 51 | } 52 | 53 | type WsClient struct { 54 | Data *utils.MapSync[[]*websocket.Conn] 55 | lock sync.Mutex 56 | } 57 | 58 | func Broadcast(room string, data string) { 59 | // 创建一个新的切片存储仍然有效的连接 60 | WC.lock.Lock() 61 | defer WC.lock.Unlock() 62 | activeConns := make([]*websocket.Conn, 0) 63 | connList := WC.Data.Get(room) 64 | for _, conn := range connList { 65 | conn.SetWriteDeadline(time.Now().Add(30 * time.Second)) 66 | // 尝试向连接发送消息 67 | if err := conn.WriteMessage(websocket.TextMessage, []byte(data)); err != nil { 68 | conn.Close() 69 | } else { 70 | activeConns = append(activeConns, conn) 71 | } 72 | } 73 | 74 | WC.Data.Set(room, activeConns) 75 | } 76 | func ping(room string) { 77 | WC.lock.Lock() 78 | defer WC.lock.Unlock() 79 | // 创建一个新的切片存储仍然有效的连接 80 | activeConns := make([]*websocket.Conn, 0) 81 | connList := WC.Data.Get(room) 82 | for _, conn := range connList { 83 | conn.SetWriteDeadline(time.Now().Add(10 * time.Second)) 84 | // 尝试向连接发送消息 85 | if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil { 86 | conn.Close() 87 | } else { 88 | activeConns = append(activeConns, conn) 89 | } 90 | } 91 | WC.Data.Set(room, activeConns) 92 | } 93 | -------------------------------------------------------------------------------- /pkg/func/hash/consistent.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import "github.com/stathat/consistent" 4 | 5 | type Hash struct { 6 | *consistent.Consistent 7 | } 8 | 9 | func New() *Hash { 10 | return &Hash{ 11 | Consistent: consistent.New(), 12 | } 13 | } 14 | func (this *Hash) Get(key string) (string, error) { 15 | return this.Consistent.Get(key) 16 | } 17 | func (this *Hash) Add(node string) { 18 | this.Consistent.Add(node) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/func/jwt/jwt.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "fmt" 5 | "github.com/golang-jwt/jwt/v4" 6 | "time" 7 | ) 8 | 9 | type InfoData[T any] struct { 10 | Core T 11 | jwt.RegisteredClaims 12 | } 13 | 14 | func Create[T any](info T, duration time.Duration, SECRET string, Issuer string) (string, error) { 15 | // Create a new token 16 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, 17 | InfoData[T]{ 18 | Core: info, 19 | RegisteredClaims: jwt.RegisteredClaims{ 20 | ExpiresAt: jwt.NewNumericDate(time.Now().Add(duration)), 21 | Issuer: Issuer, 22 | }, 23 | }) 24 | 25 | // Sign the token with a secret key 26 | tokenString, err := token.SignedString([]byte(SECRET)) 27 | if err != nil { 28 | return "", err 29 | } 30 | return tokenString, nil 31 | } 32 | func Parse[T any](token string, SECRET string) (*T, error) { 33 | var res InfoData[T] 34 | // Parse the token 35 | info, err := jwt.ParseWithClaims(token, &res, func(token *jwt.Token) (interface{}, error) { 36 | return []byte(SECRET), nil 37 | }) 38 | if err != nil { 39 | return nil, err 40 | } 41 | if claims, ok := info.Claims.(*InfoData[T]); ok && info.Valid { 42 | return &claims.Core, nil 43 | } else { 44 | return nil, fmt.Errorf("token不合法,或者已过期") 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pkg/func/jwt/jwt_test.go: -------------------------------------------------------------------------------- 1 | package jwt 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | type Info struct { 10 | Name string 11 | } 12 | 13 | func TestJWT(t *testing.T) { 14 | core := Info{ 15 | Name: "我是wike", 16 | } 17 | //生成一个token 18 | token, err := Create[Info](core, time.Second*10) 19 | fmt.Println(token, err) 20 | //根据token得到对象 21 | info, err := Parse[Info](token) 22 | fmt.Println(info, err) 23 | } 24 | -------------------------------------------------------------------------------- /pkg/func/log/zap.go: -------------------------------------------------------------------------------- 1 | package zaplog 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | "github.com/wike2019/wike_go/pkg/func/memorylog" 6 | "go.uber.org/zap" 7 | "go.uber.org/zap/zapcore" 8 | "gopkg.in/natefinch/lumberjack.v2" 9 | "os" 10 | "sync" 11 | ) 12 | 13 | var once sync.Once 14 | var logger *zap.Logger 15 | 16 | func GetLogger(*viper.Viper) *zap.Logger { 17 | once.Do(func() { 18 | encoderConfig := zapcore.EncoderConfig{ 19 | // Keys can be anything except the empty string. 20 | TimeKey: "T", 21 | LevelKey: "L", 22 | NameKey: "N", 23 | CallerKey: "C", 24 | MessageKey: "M", 25 | StacktraceKey: "S", 26 | LineEnding: zapcore.DefaultLineEnding, 27 | EncodeLevel: zapcore.LowercaseLevelEncoder, 28 | EncodeTime: zapcore.ISO8601TimeEncoder, 29 | EncodeDuration: zapcore.SecondsDurationEncoder, 30 | EncodeCaller: zapcore.ShortCallerEncoder, // ShortCallerEncoder will print function name and line number: pkg/file.go:line 31 | } 32 | Level := zap.InfoLevel 33 | if viper.GetBool("development") { 34 | Level = zap.DebugLevel 35 | } 36 | // 创建一个写入文件的 zapcore.Core 37 | fileSyncWriter := zapcore.AddSync(&lumberjack.Logger{ 38 | Filename: viper.GetString("logPath"), 39 | MaxSize: 100, 40 | MaxBackups: 3, 41 | MaxAge: 7, 42 | Compress: true, 43 | LocalTime: true, 44 | }) 45 | fileCore := zapcore.NewCore( 46 | zapcore.NewConsoleEncoder(encoderConfig), 47 | fileSyncWriter, 48 | Level, 49 | ) 50 | 51 | // 创建一个写入控制台的 zapcore.Core 52 | consoleSyncWriter := zapcore.AddSync(os.Stdout) 53 | consoleEncoderConfig := zap.NewDevelopmentEncoderConfig() 54 | consoleEncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 55 | consoleCore := zapcore.NewCore( 56 | zapcore.NewConsoleEncoder(encoderConfig), 57 | consoleSyncWriter, 58 | Level, 59 | ) 60 | memorylog.LogInfo = &memorylog.LogStruct{ 61 | Log: make(memorylog.LogList, 0, 2000), 62 | } 63 | tempSyncWriter := zapcore.AddSync(memorylog.LogInfo) 64 | tempSyncCore := zapcore.NewCore( 65 | zapcore.NewConsoleEncoder(encoderConfig), 66 | tempSyncWriter, 67 | Level, 68 | ) 69 | 70 | // 使用 zapcore.NewTee 创建一个包含两个 zapcore.Core 的 zapcore.Core 71 | core := zapcore.NewTee(fileCore, consoleCore, tempSyncCore) 72 | 73 | // 创建 zap 日志记录器 74 | logger = zap.New(core, zap.AddCaller()) 75 | }) 76 | return logger 77 | } 78 | -------------------------------------------------------------------------------- /pkg/func/memorylog/log.go: -------------------------------------------------------------------------------- 1 | package memorylog 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // 内存日志 8 | type Log struct { 9 | Text string 10 | } 11 | 12 | type LogList []*Log 13 | 14 | type LogStruct struct { 15 | Log LogList 16 | } 17 | 18 | var LogInfo *LogStruct 19 | 20 | func (this *LogStruct) Write(text []byte) (int, error) { 21 | var old LogList 22 | if len(this.Log) >= 2001 { 23 | old = this.Log[:2000] 24 | } else { 25 | old = this.Log 26 | } 27 | this.Log = append([]*Log{{Text: string(text)}}, old...) 28 | return len(text), nil 29 | } 30 | 31 | func (this *LogStruct) Show() { 32 | for _, item := range this.Log { 33 | fmt.Println(item.Text) 34 | } 35 | } 36 | func (this *LogStruct) All() LogList { 37 | return this.Log 38 | } 39 | -------------------------------------------------------------------------------- /pkg/func/memorylog/log_test.go: -------------------------------------------------------------------------------- 1 | package memorylog 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestLog(t *testing.T) { 9 | count := 0 10 | f := &LogStruct{} 11 | for { 12 | count = count + 1 13 | 14 | f.Write([]byte(fmt.Sprintf("%d", count))) 15 | //fmt.Println(count) 16 | if count == 3000 { 17 | break 18 | } 19 | } 20 | fmt.Println("ok") 21 | 22 | f.Show() 23 | } 24 | -------------------------------------------------------------------------------- /pkg/func/rateLimiter/rateLimiter.go: -------------------------------------------------------------------------------- 1 | package rateLimiter 2 | 3 | import ( 4 | "github.com/golang/groupcache/lru" 5 | "golang.org/x/time/rate" 6 | "sync" 7 | ) 8 | 9 | type LimiterCache struct { 10 | cache *lru.Cache 11 | mu sync.Mutex 12 | } 13 | 14 | func NewRateLimiterCache(maxEntries int) GetLimit { 15 | return &LimiterCache{ 16 | cache: lru.New(maxEntries), 17 | } 18 | } 19 | 20 | func (rlc *LimiterCache) GetLimiter(key string, r rate.Limit, b int) *rate.Limiter { 21 | rlc.mu.Lock() 22 | defer rlc.mu.Unlock() 23 | 24 | if lim, ok := rlc.cache.Get(key); ok { 25 | return lim.(*rate.Limiter) 26 | } 27 | 28 | lim := rate.NewLimiter(r, b) 29 | rlc.cache.Add(key, lim) 30 | return lim 31 | } 32 | func RateLimit() GetLimit { 33 | return NewRateLimiterCache(40960) 34 | } 35 | 36 | type GetLimit interface { 37 | GetLimiter(key string, r rate.Limit, b int) *rate.Limiter 38 | } 39 | -------------------------------------------------------------------------------- /pkg/func/retry/retry.go: -------------------------------------------------------------------------------- 1 | package retry 2 | 3 | import ( 4 | "github.com/avast/retry-go" 5 | "time" 6 | ) 7 | 8 | type Retry struct { 9 | job func() error 10 | times int 11 | delay time.Duration 12 | } 13 | 14 | func NewRetry(job func() error) *Retry { 15 | return &Retry{job: job, times: 3, delay: time.Second * 1} 16 | } 17 | func (r *Retry) Do() error { 18 | return retry.Do(r.job, retry.Attempts(uint(r.times)), retry.Delay(r.delay), retry.LastErrorOnly(true), retry.DelayType(retry.BackOffDelay)) 19 | } 20 | func (r *Retry) SetTimes(times int) *Retry { 21 | r.times = times 22 | return r 23 | } 24 | func (r *Retry) SetDelay(delay time.Duration) *Retry { 25 | r.delay = delay 26 | return r 27 | } 28 | -------------------------------------------------------------------------------- /pkg/modules/ants_service/ants.go: -------------------------------------------------------------------------------- 1 | package ants_service 2 | 3 | import ( 4 | "github.com/panjf2000/ants/v2" 5 | "sync" 6 | ) 7 | 8 | type AntsCtl struct { 9 | *ants.Pool 10 | *sync.WaitGroup 11 | Total int 12 | Ok int 13 | err error 14 | Fail int 15 | } 16 | 17 | func NewPool(size int) (*AntsCtl, error) { 18 | pool, err := ants.NewPool(size) 19 | return &AntsCtl{ 20 | Pool: pool, 21 | WaitGroup: &sync.WaitGroup{}, 22 | }, err 23 | } 24 | func (this *AntsCtl) SetTotal(total int) { 25 | this.Total = total 26 | this.Add(this.Total) 27 | } 28 | func (this *AntsCtl) Submit(task func() error) error { 29 | return this.Pool.Submit(func() { 30 | defer this.Done() 31 | err := task() 32 | if err != nil { 33 | this.Fail++ 34 | if this.err == nil { 35 | this.err = err 36 | } 37 | return 38 | } 39 | this.Ok++ 40 | }) 41 | } 42 | func (this *AntsCtl) Error() error { 43 | return this.err 44 | } 45 | func (this *AntsCtl) Wait() { 46 | this.WaitGroup.Wait() 47 | } 48 | -------------------------------------------------------------------------------- /pkg/os/serverMetrics.go: -------------------------------------------------------------------------------- 1 | package os 2 | 3 | import ( 4 | "github.com/shirou/gopsutil/v3/cpu" 5 | "github.com/shirou/gopsutil/v3/disk" 6 | "github.com/shirou/gopsutil/v3/mem" 7 | "runtime" 8 | "time" 9 | ) 10 | 11 | const ( 12 | B = 1 13 | KB = 1024 * B 14 | MB = 1024 * KB 15 | GB = 1024 * MB 16 | ) 17 | 18 | type Server struct { 19 | Os Os `json:"os"` 20 | Cpu Cpu `json:"cpu"` 21 | Ram Ram `json:"ram"` 22 | Disk *Disk `json:"disk"` 23 | } 24 | 25 | type Os struct { 26 | GOOS string `json:"goos"` 27 | NumCPU int `json:"numCpu"` 28 | Compiler string `json:"compiler"` 29 | GoVersion string `json:"goVersion"` 30 | NumGoroutine int `json:"numGoroutine"` 31 | } 32 | 33 | type Cpu struct { 34 | Cpus []float64 `json:"cpus"` 35 | Cores int `json:"cores"` 36 | } 37 | 38 | type Ram struct { 39 | UsedMB int `json:"usedMb"` 40 | TotalMB int `json:"totalMb"` 41 | UsedPercent int `json:"usedPercent"` 42 | } 43 | 44 | type Disk struct { 45 | MountPoint string `json:"mountPoint"` 46 | UsedMB int `json:"usedMb"` 47 | UsedGB int `json:"usedGb"` 48 | TotalMB int `json:"totalMb"` 49 | TotalGB int `json:"totalGb"` 50 | UsedPercent int `json:"usedPercent"` 51 | } 52 | 53 | func InitOS() (o Os) { 54 | o.GOOS = runtime.GOOS 55 | o.NumCPU = runtime.NumCPU() 56 | o.Compiler = runtime.Compiler 57 | o.GoVersion = runtime.Version() 58 | o.NumGoroutine = runtime.NumGoroutine() 59 | return o 60 | } 61 | 62 | func InitCPU() (c Cpu, err error) { 63 | if cores, err := cpu.Counts(false); err != nil { 64 | return c, err 65 | } else { 66 | c.Cores = cores 67 | } 68 | if cpus, err := cpu.Percent(time.Duration(200)*time.Millisecond, true); err != nil { 69 | return c, err 70 | } else { 71 | c.Cpus = cpus 72 | } 73 | return c, nil 74 | } 75 | 76 | func InitRAM() (r Ram, err error) { 77 | if u, err := mem.VirtualMemory(); err != nil { 78 | return r, err 79 | } else { 80 | r.UsedMB = int(u.Used) / MB 81 | r.TotalMB = int(u.Total) / MB 82 | r.UsedPercent = int(u.UsedPercent) 83 | } 84 | return r, nil 85 | } 86 | 87 | func InitDisk(path string) (d *Disk, err error) { 88 | u, err := disk.Usage(path) 89 | if err != nil { 90 | return nil, err 91 | } 92 | return &Disk{ 93 | MountPoint: path, 94 | UsedMB: int(u.Used) / MB, 95 | UsedGB: int(u.Used) / GB, 96 | TotalMB: int(u.Total) / MB, 97 | TotalGB: int(u.Total) / GB, 98 | UsedPercent: int(u.UsedPercent), 99 | }, nil 100 | } 101 | -------------------------------------------------------------------------------- /pkg/tpl/tpl.go: -------------------------------------------------------------------------------- 1 | package tpl 2 | 3 | const MdTemplate = `# 接口文档 4 | 5 | {{- range . }} 6 | ## 接口分组:{{ .Group }} 7 | 8 | {{- range .APIs }} 9 | 10 | ### 接口名称:{{ .Name }} {{ if eq .Status 2 }}~~已废弃~~{{ end }} 11 | 12 | #### 请求路径:{{ .Path }} 13 | 14 | #### 请求方式:{{ .Method }} 15 | 16 | #### 请求参数: 17 | 18 | {{ .Input }} 19 | 20 | #### 请求结果: 21 | 22 | {{ .Output }} 23 | 24 | {{ end }} 25 | {{ end }} 26 | ` 27 | -------------------------------------------------------------------------------- /pkg/utils/copy.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | ) 8 | 9 | // 对象拷贝 10 | func CopyProperties(dst, src interface{}) (err error) { 11 | // 防止意外panic 12 | defer func() { 13 | if e := recover(); e != nil { 14 | err = errors.New(fmt.Sprintf("%v", e)) 15 | } 16 | }() 17 | dstType, dstValue := reflect.TypeOf(dst), reflect.ValueOf(dst) 18 | srcType, srcValue := reflect.TypeOf(src), reflect.ValueOf(src) 19 | // dst必须结构体指针类型 20 | if dstType.Kind() != reflect.Ptr || dstType.Elem().Kind() != reflect.Struct { 21 | return errors.New("dst type should be a struct pointer") 22 | } 23 | // src必须为结构体或者结构体指针 24 | if srcType.Kind() == reflect.Ptr { 25 | srcType, srcValue = srcType.Elem(), srcValue.Elem() 26 | } 27 | if srcType.Kind() != reflect.Struct { 28 | return errors.New("src type should be a struct or a struct pointer") 29 | } 30 | // 取具体内容 31 | dstType, dstValue = dstType.Elem(), dstValue.Elem() 32 | // 属性个数 33 | propertyNums := dstType.NumField() 34 | for i := 0; i < propertyNums; i++ { 35 | // 属性 36 | property := dstType.Field(i) 37 | // 待填充属性值 38 | propertyValue := srcValue.FieldByName(property.Name) 39 | // 无效,说明src没有这个属性 || 属性同名但类型不同 40 | if !propertyValue.IsValid() || property.Type != propertyValue.Type() { 41 | continue 42 | } 43 | if dstValue.Field(i).CanSet() { 44 | dstValue.Field(i).Set(propertyValue) 45 | } 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/utils/duration.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | ) 8 | 9 | func ParseDuration(d string) (time.Duration, error) { 10 | d = strings.TrimSpace(d) 11 | dr, err := time.ParseDuration(d) 12 | if err == nil { 13 | return dr, nil 14 | } 15 | if strings.Contains(d, "d") { 16 | index := strings.Index(d, "d") 17 | 18 | hour, _ := strconv.Atoi(d[:index]) 19 | dr = time.Hour * 24 * time.Duration(hour) 20 | ndr, err := time.ParseDuration(d[index+1:]) 21 | if err != nil { 22 | return dr, nil 23 | } 24 | return dr + ndr, nil 25 | } 26 | 27 | dv, err := strconv.ParseInt(d, 10, 64) 28 | return time.Duration(dv), err 29 | } 30 | 31 | //ParseDuration("1d5h20m") 32 | //ParseDuration("1d") 33 | //ParseDuration("5h20m") 34 | -------------------------------------------------------------------------------- /pkg/utils/ip.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "strings" 6 | ) 7 | 8 | func GetClientIP(c *gin.Context) string { 9 | ClientIP := c.ClientIP() 10 | RemoteIP := c.RemoteIP() 11 | ip := c.Request.Header.Get("X-Forwarded-For") 12 | if strings.Contains(ip, "127.0.0.1") || ip == "" { 13 | ip = c.Request.Header.Get("X-real-ip") 14 | } 15 | if ip == "" { 16 | ip = "127.0.0.1" 17 | } 18 | if RemoteIP != "127.0.0.1" { 19 | ip = RemoteIP 20 | } 21 | if ClientIP != "127.0.0.1" { 22 | ip = ClientIP 23 | } 24 | return ip 25 | } 26 | -------------------------------------------------------------------------------- /pkg/utils/lock.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "github.com/bsm/redislock" 6 | "github.com/redis/go-redis/v9" 7 | "time" 8 | ) 9 | 10 | type RedisLock struct { 11 | client *redis.Client 12 | mutex *redislock.Client 13 | LockData *redislock.Lock 14 | } 15 | 16 | func NewRedisLock(client *redis.Client) *RedisLock { 17 | return &RedisLock{client: client} 18 | } 19 | func (RedisLock) String() string { 20 | return "redis" 21 | } 22 | 23 | func (r *RedisLock) Lock(key string, ttl int64, options *redislock.Options) (*redislock.Lock, error) { 24 | if r.mutex == nil { 25 | r.mutex = redislock.New(r.client) 26 | } 27 | lock, err := r.mutex.Obtain(context.TODO(), key, time.Duration(ttl)*time.Second, options) 28 | r.LockData = lock 29 | // 设置一个定时任务来自动续期锁 30 | go func() { 31 | for { 32 | time.Sleep(5 * time.Second) // 每 5 秒续期一次 33 | err := r.LockData.Refresh(context.Background(), 10*time.Second, options) // 延长锁的有效期 34 | if err != nil { 35 | return 36 | } 37 | } 38 | }() 39 | return r.LockData, err 40 | } 41 | -------------------------------------------------------------------------------- /pkg/utils/map.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "sync" 4 | 5 | // 并发安全map 6 | type MapSync[T any] struct { 7 | m map[string]T 8 | lock sync.RWMutex 9 | } 10 | 11 | func (this *MapSync[T]) Set(key string, val T) { 12 | this.lock.Lock() 13 | defer this.lock.Unlock() 14 | this.m[key] = val 15 | } 16 | 17 | func (this *MapSync[T]) Get(key string) T { 18 | this.lock.RLock() 19 | defer this.lock.RUnlock() 20 | return this.m[key] 21 | } 22 | func (this *MapSync[T]) Delete(key string) { 23 | this.lock.Lock() 24 | defer this.lock.Unlock() 25 | delete(this.m, key) 26 | } 27 | 28 | func (this *MapSync[T]) Keys() []string { 29 | this.lock.RLock() 30 | defer this.lock.RUnlock() 31 | keys := make([]string, 0) 32 | for k := range this.m { 33 | keys = append(keys, k) 34 | } 35 | return keys 36 | } 37 | func (this *MapSync[T]) Values() []T { 38 | this.lock.RLock() 39 | defer this.lock.RUnlock() 40 | values := make([]T, 0) 41 | for k := range this.m { 42 | values = append(values, this.m[k]) 43 | } 44 | return values 45 | } 46 | func NewMap[T any]() *MapSync[T] { 47 | return &MapSync[T]{m: make(map[string]T)} 48 | } 49 | -------------------------------------------------------------------------------- /pkg/utils/md5.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | ) 7 | 8 | func CheckMd5(content []byte, chunkMd5 string) (isSame bool) { 9 | fileMd5 := MD5V(content) 10 | if fileMd5 == chunkMd5 { 11 | return true // 可以继续上传 12 | } else { 13 | return false // 切片不完整,废弃 14 | } 15 | } 16 | func MD5V(str []byte, b ...byte) string { 17 | h := md5.New() 18 | h.Write(str) 19 | return hex.EncodeToString(h.Sum(b)) 20 | } 21 | -------------------------------------------------------------------------------- /pkg/utils/number.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/shopspring/decimal" 6 | "strconv" 7 | ) 8 | 9 | // 数学计算 10 | func Decimal(num float64) float64 { 11 | num, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", num), 64) 12 | return num 13 | } 14 | 15 | // AddDecimal 加法 16 | func AddDecimal(d1 decimal.Decimal, d2 decimal.Decimal) decimal.Decimal { 17 | return d1.Add(d2) 18 | } 19 | 20 | // SubDecimal 减法 21 | func SubDecimal(d1 decimal.Decimal, d2 decimal.Decimal) decimal.Decimal { 22 | return d1.Sub(d2) 23 | } 24 | 25 | // MulDecimal 乘法 26 | func MulDecimal(d1 decimal.Decimal, d2 decimal.Decimal) decimal.Decimal { 27 | return d1.Mul(d2) 28 | } 29 | 30 | // DivDecimal 除法 31 | func DivDecimal(d1 decimal.Decimal, d2 decimal.Decimal) decimal.Decimal { 32 | return d1.Div(d2) 33 | } 34 | 35 | // IntDecimal 转换成int 36 | func IntDecimal(d decimal.Decimal) int64 { 37 | return d.IntPart() 38 | } 39 | 40 | // DecimalFloat 转换成float 41 | func DecimalFloat(d decimal.Decimal) float64 { 42 | f, exact := d.Float64() 43 | if !exact { 44 | return f 45 | } 46 | return 0 47 | } 48 | 49 | // FloatDecimal 浮点型到decimal 50 | func FloatDecimal(d float64) decimal.Decimal { 51 | return decimal.NewFromFloat(d) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/utils/password.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "golang.org/x/crypto/bcrypt" 4 | 5 | // 加解密 密码 6 | func PasswordHash(pwd string) (string, error) { 7 | bytes, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) 8 | if err != nil { 9 | return "", err 10 | } 11 | return string(bytes), err 12 | } 13 | 14 | func PasswordVerify(pwd, hash string) bool { 15 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(pwd)) 16 | return err == nil 17 | } 18 | -------------------------------------------------------------------------------- /pkg/utils/queue.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/redis/go-redis/v9" 7 | ) 8 | 9 | type RedisQueue struct { 10 | client *redis.Client 11 | } 12 | 13 | func NewRedisQueue(client *redis.Client) *RedisQueue { 14 | return &RedisQueue{client: client} 15 | } 16 | 17 | func (this *RedisQueue) Push(queueName string, message interface{}) error { 18 | msg, _ := json.Marshal(message) 19 | return this.client.LPush(context.Background(), queueName, msg).Err() 20 | } 21 | 22 | func (this *RedisQueue) Pop(queueName string, obj interface{}) error { 23 | message, err := this.client.BLPop(context.Background(), 0, queueName).Result() 24 | if err != nil { 25 | return err 26 | } 27 | err = json.Unmarshal([]byte(message[1]), obj) 28 | if err != nil { 29 | return err 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /pkg/utils/random.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | var letters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 9 | 10 | // RandomString 生成随机长度的字符串 11 | func RandomString(length int64) string { 12 | b := make([]rune, length) 13 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 14 | for i := range b { 15 | b[i] = letters[r.Intn(62)] 16 | } 17 | return string(b) 18 | } 19 | 20 | // GetRandomNum 生成随机数字(包括边界) 21 | func GetRandomNum(min, max int) int64 { 22 | rand.New(rand.NewSource(time.Now().UnixNano())) 23 | return int64(rand.Intn(max-min+1) + min) 24 | } 25 | -------------------------------------------------------------------------------- /pkg/utils/set.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // set 集合 8 | type Set struct { 9 | mapData *MapSync[Empty] 10 | } 11 | type Empty struct{} 12 | 13 | func NewSet() *Set { 14 | return &Set{ 15 | mapData: NewMap[Empty](), 16 | } 17 | } 18 | func (this *Set) Add(value interface{}) error { 19 | b, err := json.Marshal(value) 20 | if err != nil { 21 | return err 22 | } 23 | this.mapData.Set(string(b), Empty{}) 24 | return err 25 | } 26 | func (this *Set) List(dist interface{}) error { 27 | keys := this.mapData.Keys() 28 | res := make([]interface{}, len(keys)) 29 | for i, item := range keys { 30 | var obj interface{} 31 | err := json.Unmarshal([]byte(item), &obj) 32 | if err != nil { 33 | return err 34 | } 35 | res[i] = obj 36 | } 37 | b, err := json.Marshal(res) 38 | if err != nil { 39 | return err 40 | } 41 | err = json.Unmarshal(b, dist) 42 | if err != nil { 43 | return err 44 | } 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/utils/validator.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type Rules map[string][]string 12 | 13 | type RulesMap map[string]Rules 14 | 15 | var CustomizeMap = make(map[string]Rules) 16 | 17 | //@author: [piexlmax](https://github.com/piexlmax) 18 | //@function: RegisterRule 19 | //@description: 注册自定义规则方案建议在路由初始化层即注册 20 | //@param: key string, rule Rules 21 | //@return: err error 22 | 23 | func RegisterRule(key string, rule Rules) (err error) { 24 | if CustomizeMap[key] != nil { 25 | return errors.New(key + "已注册,无法重复注册") 26 | } else { 27 | CustomizeMap[key] = rule 28 | return nil 29 | } 30 | } 31 | 32 | //@author: [piexlmax](https://github.com/piexlmax) 33 | //@function: NotEmpty 34 | //@description: 非空 不能为其对应类型的0值 35 | //@return: string 36 | 37 | func NotEmpty() string { 38 | return "notEmpty" 39 | } 40 | 41 | // @author: [zooqkl](https://github.com/zooqkl) 42 | // @function: RegexpMatch 43 | // @description: 正则校验 校验输入项是否满足正则表达式 44 | // @param: rule string 45 | // @return: string 46 | 47 | func RegexpMatch(rule string) string { 48 | return "regexp=" + rule 49 | } 50 | 51 | //@author: [piexlmax](https://github.com/piexlmax) 52 | //@function: Lt 53 | //@description: 小于入参(<) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 54 | //@param: mark string 55 | //@return: string 56 | 57 | func Lt(mark string) string { 58 | return "lt=" + mark 59 | } 60 | 61 | //@author: [piexlmax](https://github.com/piexlmax) 62 | //@function: Le 63 | //@description: 小于等于入参(<=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 64 | //@param: mark string 65 | //@return: string 66 | 67 | func Le(mark string) string { 68 | return "le=" + mark 69 | } 70 | 71 | //@author: [piexlmax](https://github.com/piexlmax) 72 | //@function: Eq 73 | //@description: 等于入参(==) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 74 | //@param: mark string 75 | //@return: string 76 | 77 | func Eq(mark string) string { 78 | return "eq=" + mark 79 | } 80 | 81 | //@author: [piexlmax](https://github.com/piexlmax) 82 | //@function: Ne 83 | //@description: 不等于入参(!=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 84 | //@param: mark string 85 | //@return: string 86 | 87 | func Ne(mark string) string { 88 | return "ne=" + mark 89 | } 90 | 91 | //@author: [piexlmax](https://github.com/piexlmax) 92 | //@function: Ge 93 | //@description: 大于等于入参(>=) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 94 | //@param: mark string 95 | //@return: string 96 | 97 | func Ge(mark string) string { 98 | return "ge=" + mark 99 | } 100 | 101 | //@author: [piexlmax](https://github.com/piexlmax) 102 | //@function: Gt 103 | //@description: 大于入参(>) 如果为string array Slice则为长度比较 如果是 int uint float 则为数值比较 104 | //@param: mark string 105 | //@return: string 106 | 107 | func Gt(mark string) string { 108 | return "gt=" + mark 109 | } 110 | 111 | // 112 | //@author: [piexlmax](https://github.com/piexlmax) 113 | //@function: Verify 114 | //@description: 校验方法 115 | //@param: st interface{}, roleMap Rules(入参实例,规则map) 116 | //@return: err error 117 | 118 | func Verify(st interface{}, roleMap Rules) (err error) { 119 | compareMap := map[string]bool{ 120 | "lt": true, 121 | "le": true, 122 | "eq": true, 123 | "ne": true, 124 | "ge": true, 125 | "gt": true, 126 | } 127 | 128 | typ := reflect.TypeOf(st) 129 | val := reflect.ValueOf(st) // 获取reflect.Type类型 130 | 131 | kd := val.Kind() // 获取到st对应的类别 132 | if kd != reflect.Struct { 133 | return errors.New("expect struct") 134 | } 135 | num := val.NumField() 136 | // 遍历结构体的所有字段 137 | for i := 0; i < num; i++ { 138 | tagVal := typ.Field(i) 139 | val := val.Field(i) 140 | if tagVal.Type.Kind() == reflect.Struct { 141 | if err = Verify(val.Interface(), roleMap); err != nil { 142 | return err 143 | } 144 | } 145 | if len(roleMap[tagVal.Name]) > 0 { 146 | for _, v := range roleMap[tagVal.Name] { 147 | switch { 148 | case v == "notEmpty": 149 | if isBlank(val) { 150 | return errors.New(tagVal.Name + "值不能为空") 151 | } 152 | case strings.Split(v, "=")[0] == "regexp": 153 | if !regexpMatch(strings.Split(v, "=")[1], val.String()) { 154 | return errors.New(tagVal.Name + "格式校验不通过") 155 | } 156 | case compareMap[strings.Split(v, "=")[0]]: 157 | if !compareVerify(val, v) { 158 | return errors.New(tagVal.Name + "长度或值不在合法范围," + v) 159 | } 160 | } 161 | } 162 | } 163 | } 164 | return nil 165 | } 166 | 167 | //@author: [piexlmax](https://github.com/piexlmax) 168 | //@function: compareVerify 169 | //@description: 长度和数字的校验方法 根据类型自动校验 170 | //@param: value reflect.Value, VerifyStr string 171 | //@return: bool 172 | 173 | func compareVerify(value reflect.Value, VerifyStr string) bool { 174 | switch value.Kind() { 175 | case reflect.String: 176 | return compare(len([]rune(value.String())), VerifyStr) 177 | case reflect.Slice, reflect.Array: 178 | return compare(value.Len(), VerifyStr) 179 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 180 | return compare(value.Uint(), VerifyStr) 181 | case reflect.Float32, reflect.Float64: 182 | return compare(value.Float(), VerifyStr) 183 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 184 | return compare(value.Int(), VerifyStr) 185 | default: 186 | return false 187 | } 188 | } 189 | 190 | //@author: [piexlmax](https://github.com/piexlmax) 191 | //@function: isBlank 192 | //@description: 非空校验 193 | //@param: value reflect.Value 194 | //@return: bool 195 | 196 | func isBlank(value reflect.Value) bool { 197 | switch value.Kind() { 198 | case reflect.String, reflect.Slice: 199 | return value.Len() == 0 200 | case reflect.Bool: 201 | return !value.Bool() 202 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 203 | return value.Int() == 0 204 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 205 | return value.Uint() == 0 206 | case reflect.Float32, reflect.Float64: 207 | return value.Float() == 0 208 | case reflect.Interface, reflect.Ptr: 209 | return value.IsNil() 210 | } 211 | return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface()) 212 | } 213 | 214 | //@author: [piexlmax](https://github.com/piexlmax) 215 | //@function: compare 216 | //@description: 比较函数 217 | //@param: value interface{}, VerifyStr string 218 | //@return: bool 219 | 220 | func compare(value interface{}, VerifyStr string) bool { 221 | VerifyStrArr := strings.Split(VerifyStr, "=") 222 | val := reflect.ValueOf(value) 223 | switch val.Kind() { 224 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 225 | VInt, VErr := strconv.ParseInt(VerifyStrArr[1], 10, 64) 226 | if VErr != nil { 227 | return false 228 | } 229 | switch { 230 | case VerifyStrArr[0] == "lt": 231 | return val.Int() < VInt 232 | case VerifyStrArr[0] == "le": 233 | return val.Int() <= VInt 234 | case VerifyStrArr[0] == "eq": 235 | return val.Int() == VInt 236 | case VerifyStrArr[0] == "ne": 237 | return val.Int() != VInt 238 | case VerifyStrArr[0] == "ge": 239 | return val.Int() >= VInt 240 | case VerifyStrArr[0] == "gt": 241 | return val.Int() > VInt 242 | default: 243 | return false 244 | } 245 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 246 | VInt, VErr := strconv.Atoi(VerifyStrArr[1]) 247 | if VErr != nil { 248 | return false 249 | } 250 | switch { 251 | case VerifyStrArr[0] == "lt": 252 | return val.Uint() < uint64(VInt) 253 | case VerifyStrArr[0] == "le": 254 | return val.Uint() <= uint64(VInt) 255 | case VerifyStrArr[0] == "eq": 256 | return val.Uint() == uint64(VInt) 257 | case VerifyStrArr[0] == "ne": 258 | return val.Uint() != uint64(VInt) 259 | case VerifyStrArr[0] == "ge": 260 | return val.Uint() >= uint64(VInt) 261 | case VerifyStrArr[0] == "gt": 262 | return val.Uint() > uint64(VInt) 263 | default: 264 | return false 265 | } 266 | case reflect.Float32, reflect.Float64: 267 | VFloat, VErr := strconv.ParseFloat(VerifyStrArr[1], 64) 268 | if VErr != nil { 269 | return false 270 | } 271 | switch { 272 | case VerifyStrArr[0] == "lt": 273 | return val.Float() < VFloat 274 | case VerifyStrArr[0] == "le": 275 | return val.Float() <= VFloat 276 | case VerifyStrArr[0] == "eq": 277 | return val.Float() == VFloat 278 | case VerifyStrArr[0] == "ne": 279 | return val.Float() != VFloat 280 | case VerifyStrArr[0] == "ge": 281 | return val.Float() >= VFloat 282 | case VerifyStrArr[0] == "gt": 283 | return val.Float() > VFloat 284 | default: 285 | return false 286 | } 287 | default: 288 | return false 289 | } 290 | } 291 | 292 | func regexpMatch(rule, matchStr string) bool { 293 | return regexp.MustCompile(rule).MatchString(matchStr) 294 | } 295 | -------------------------------------------------------------------------------- /plugin/repeatCheck.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "time" 6 | ) 7 | 8 | type RepeatCheck struct { 9 | ID uint64 `gorm:"primaryKey;column:id;comment:主键"` 10 | TraceId string `gorm:"column:trace_id;type:varchar(255);uniqueIndex:idx_id;comment:追踪id"` 11 | Time time.Time `gorm:"column:time;type:date;comment:插入日期"` 12 | } 13 | 14 | func (this *RepeatCheck) TableName() string { 15 | return "RepeatCheck" 16 | } 17 | 18 | // BeforeCreate Gorm钩子,在创建记录之前调用 19 | func (m *RepeatCheck) BeforeCreate(tx *gorm.DB) (err error) { 20 | m.Time = time.Now() 21 | return 22 | } 23 | 24 | // BeforeUpdate Gorm钩子,在更新记录之前调用 25 | func (m *RepeatCheck) BeforeUpdate(tx *gorm.DB) (err error) { 26 | m.Time = time.Now() 27 | return 28 | } 29 | -------------------------------------------------------------------------------- /policy.csv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wike2019/wike_go/ed6f7064c11f9709f437634b6634c70c62b44cae/policy.csv -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # wike_go 是一款web框架,基于成熟的gin框架进行二次封装,提供了一些常用的功能,让开发者更专注于业务开发。 2 | 3 | ## 特性 4 | 5 | 1 基于gin完美兼容,支持更优化的api 6 | 7 | 2 依赖注入,基于fx的二次封装,更简单的使用 8 | 9 | 3 丰富的工具函数 10 | 11 | ## 申明 12 | 框架刚成立,需要很多测试,和完善,功能也会慢慢完善,欢迎大家提出宝贵的意见,一起完善这个框架 13 | 14 | ## 项目接口文档 15 | http://wg.ng2-oa.com/ 16 | 17 | ### 待完善 18 | -------------------------------------------------------------------------------- /server/api/api.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gin-gonic/gin" 6 | "github.com/wike2019/wike_go/common" 7 | "github.com/wike2019/wike_go/pkg/func/ctl" 8 | "github.com/wike2019/wike_go/server/model" 9 | ) 10 | 11 | func (this CoreCtl) getApiParams(context *gin.Context) (*ctl.Page, *model.APISearch, *common.Empty, *ctl.Controller) { 12 | Item := &model.APISearch{} 13 | c := this.SetContext(context) 14 | c.PageInit() 15 | err := json.Unmarshal(c.Data, Item) 16 | ctl.Error(err, 400) 17 | return &c.Page, Item, &common.Empty{}, c 18 | } 19 | func (this CoreCtl) getApi(context *gin.Context) interface{} { 20 | res, err := this.service.GetApiService(this.getApiParams(context)) 21 | ctl.Error(err, 400) 22 | return res 23 | } 24 | 25 | func (this CoreCtl) getApiFrontCommonParams(context *gin.Context) (*common.Empty, *model.APISearch, *common.Empty, *ctl.Controller) { 26 | c := this.SetContext(context) 27 | return &common.Empty{}, &model.APISearch{}, &common.Empty{}, c 28 | } 29 | func (this CoreCtl) getApiFront(context *gin.Context) interface{} { 30 | res, err := this.service.GetApiFrontService(this.getApiFrontCommonParams(context)) 31 | ctl.Error(err, 400) 32 | return res 33 | } 34 | -------------------------------------------------------------------------------- /server/api/build.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | model2 "github.com/wike2019/wike_go/common" 6 | "github.com/wike2019/wike_go/pkg/core" 7 | "github.com/wike2019/wike_go/pkg/func/ctl" 8 | "github.com/wike2019/wike_go/pkg/func/ctl/fastRouter" 9 | "github.com/wike2019/wike_go/pkg/os" 10 | "github.com/wike2019/wike_go/server/model" 11 | "github.com/wike2019/wike_go/server/service" 12 | ) 13 | 14 | type CoreCtl struct { 15 | ctl.Controller 16 | DBInstance *core.CoreDb 17 | roleCtl *core.RoleCtl 18 | service *service.Service 19 | } 20 | 21 | func NewRouter(DB *core.CoreDb, roleCtl *core.RoleCtl, service *service.Service) *CoreCtl { 22 | return &CoreCtl{DBInstance: DB, roleCtl: roleCtl, service: service} 23 | } 24 | func (this CoreCtl) Path() string { 25 | return "/core" 26 | } 27 | func (this CoreCtl) CoreDb() *core.CoreDb { 28 | return this.DBInstance 29 | } 30 | func (this *CoreCtl) Build(r *gin.RouterGroup, GCore *core.GCore) { 31 | //t := r.Use(core.Root(), core.Authorizer(this.roleCtl.E)) 32 | fastRouter.FastRouter[model2.IDSearch, model.SysDictionary, model2.Empty](this.DBInstance.DB, this.DBInstance, r, GCore, "/dictionary", "字典", "系统接口", "core") 33 | fastRouter.FastRouter[model2.IDSearch, model.SysDictionaryDetail, model2.Empty](this.DBInstance.DB, this.DBInstance, r, GCore, "/dictionaryDetail", "字典详细", "系统接口", "core") 34 | fastRouter.FastRouter[model2.IDSearch, model.Role, model2.Empty](this.DBInstance.DB, this.DBInstance, r, GCore, "/role", "角色", "系统接口", "core") 35 | 36 | this.SetDoc(this.getApiParams, this.service.GetApiService) 37 | GCore.PostWithRbac(r, this, "系统内部接口", "/api", this.getApi, "获取接口列表") 38 | 39 | this.SetDoc(this.getApiFrontCommonParams, this.service.GetApiFrontService) 40 | GCore.GetWithRbac(r, this, "系统内部接口", "/getApiFront", this.getApiFront, "获得接口列表") 41 | 42 | this.SetDoc(this.dictionaryListParams, this.service.DictionaryListService) 43 | GCore.PostWithRbac(r, this, "系统内部接口", "/dictionaryList", this.dictionaryList, "获取字典列表") 44 | this.SetDoc(this.dictionaryItemParams, this.service.DictionaryItemService) 45 | GCore.GetWithRbac(r, this, "系统内部接口", "/dictionaryItem", this.dictionaryItem, "模糊搜索字典") 46 | 47 | this.SetDoc(this.dictionarySearchParams, this.service.DictionarySearchService) 48 | GCore.GetWithRbac(r, this, "系统内部接口", "/dictionarySearch", this.dictionarySearch, "模糊搜索字典") 49 | this.SetDocRaw(nil, nil, nil, ctl.DataList[os.Server]{}) 50 | GCore.GetWithRbac(r, this, "系统内部接口", "/systemInfo", this.SystemInfo, "获取服务器信息") 51 | 52 | this.SetDoc(this.roleListParams, this.service.RoleListService) 53 | GCore.GetWithRbac(r, this, "系统内部接口", "/roleList", this.roleList, "获取角色名称列表") 54 | 55 | this.SetDoc(this.ruleCreateParams, this.service.RuleCreateService) 56 | GCore.PostWithRbac(r, this, "系统内部接口", "/ruleCreate", this.ruleCreate, "创建规则") 57 | //// 58 | this.SetDoc(this.ruleInfoParams, this.service.RuleInfoService) 59 | GCore.GetWithRbac(r, this, "系统内部接口", "/ruleInfo", this.ruleInfo, "获得接口权限详细") 60 | //// 61 | this.SetEmpty() 62 | GCore.GetWithRbac(r, this, "系统内部接口", "/ruleSync", this.ruleSync, "同步权限") 63 | this.SetEmpty() 64 | GCore.GetWithRbac(r, this, "系统内部接口", "/log", this.log, "接口日志查询") 65 | this.SetEmptyWithHeader(&model2.Empty{}) 66 | GCore.GetWithRbac(r, this, "系统内部接口", "/jobList", this.jobList, "定时任务查询") 67 | 68 | this.SetDocRaw(nil, nil, nil, ctl.DataList[[]model.Role]{}) 69 | GCore.GetWithRbac(r, this, "系统内部接口", "/roleDataList", this.roleDataList, "获取角色列表") 70 | 71 | } 72 | -------------------------------------------------------------------------------- /server/api/dictionary.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gin-gonic/gin" 6 | "github.com/wike2019/wike_go/common" 7 | "github.com/wike2019/wike_go/pkg/func/ctl" 8 | "github.com/wike2019/wike_go/server/model" 9 | ) 10 | 11 | func (this CoreCtl) dictionaryListParams(context *gin.Context) (*ctl.Page, *model.SysDictionarySearch, *common.Empty, *ctl.Controller) { 12 | Item := &model.SysDictionarySearch{} 13 | c := this.SetContext(context) 14 | c.PageInit() 15 | err := json.Unmarshal(c.Data, Item) 16 | ctl.Error(err, 400) 17 | return &c.Page, Item, &common.Empty{}, c 18 | } 19 | 20 | func (this CoreCtl) dictionaryList(context *gin.Context) interface{} { 21 | data, err := this.service.DictionaryListService(this.dictionaryListParams(context)) 22 | ctl.Error(err, 400) 23 | return data 24 | } 25 | 26 | func (this CoreCtl) dictionaryItemParams(context *gin.Context) (*common.IDSearch, *common.Empty, *common.Empty, *ctl.Controller) { 27 | query := &common.IDSearch{} 28 | c := this.SetContext(context) 29 | err := c.ShouldBindQuery(query) 30 | ctl.Error(err, 400) 31 | return query, &common.Empty{}, &common.Empty{}, c 32 | } 33 | func (this CoreCtl) dictionaryItem(context *gin.Context) interface{} { 34 | data, err := this.service.DictionaryItemService(this.dictionaryItemParams(context)) 35 | ctl.Error(err, 400) 36 | return data 37 | } 38 | 39 | func (this CoreCtl) dictionarySearchParams(context *gin.Context) (*common.KeyWordsSearch, *common.Empty, *common.Empty, *ctl.Controller) { 40 | query := &common.KeyWordsSearch{} 41 | c := this.SetContext(context) 42 | err := c.ShouldBindQuery(query) 43 | ctl.Error(err, 400) 44 | return query, &common.Empty{}, &common.Empty{}, c 45 | } 46 | func (this CoreCtl) dictionarySearch(context *gin.Context) interface{} { 47 | data, err := this.service.DictionarySearchService(this.dictionarySearchParams(context)) 48 | ctl.Error(err, 400) 49 | return data 50 | } 51 | -------------------------------------------------------------------------------- /server/api/job.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/wike2019/wike_go/pkg/func/ctl" 6 | "github.com/wike2019/wike_go/server/model" 7 | ) 8 | 9 | func (this CoreCtl) jobList(context *gin.Context) interface{} { 10 | c := this.SetContext(context) 11 | Item := model.Job{} 12 | data, err := ctl.ListItemAll[model.Job](this.DBInstance.DB, Item, nil) 13 | ctl.Error(err, 400) 14 | res := ctl.Item("获取日志成功", data, c) 15 | return res 16 | } 17 | -------------------------------------------------------------------------------- /server/api/log.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/wike2019/wike_go/pkg/func/ctl" 6 | "github.com/wike2019/wike_go/pkg/func/memorylog" 7 | ) 8 | 9 | func (this CoreCtl) log(context *gin.Context) interface{} { 10 | c := this.SetContext(context) 11 | res := ctl.Item("获取日志成功", memorylog.LogInfo.All(), c) 12 | return res 13 | } 14 | -------------------------------------------------------------------------------- /server/api/role.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/gin-gonic/gin" 6 | "github.com/wike2019/wike_go/common" 7 | "github.com/wike2019/wike_go/pkg/func/ctl" 8 | "github.com/wike2019/wike_go/server/model" 9 | ) 10 | 11 | // 12 | // func (this CoreCtl) roleCreate(context *gin.Context) { 13 | // Item := &model2.Role{} 14 | // c := this.SetContext(context) 15 | // err := json.Unmarshal(c.Data, Item) 16 | // ctl.Error(err, 400) 17 | // if Item.Parent == "" { 18 | // Item.Parent = "nologin" 19 | // } 20 | // err = ctl.CreateItem[*model2.Role](this.DB.DB, Item) 21 | // ctl.Error(err, 400) 22 | // c.Success("添加角色成功", Item) 23 | // } 24 | 25 | func (this CoreCtl) roleListParams(context *gin.Context) (*model.Role, *common.Empty, *common.Empty, *ctl.Controller) { 26 | query := &model.Role{} 27 | c := this.SetContext(context) 28 | err := c.ShouldBindQuery(query) 29 | ctl.Error(err, 400) 30 | return query, &common.Empty{}, &common.Empty{}, c 31 | } 32 | func (this CoreCtl) roleList(context *gin.Context) interface{} { 33 | list, err := this.service.RoleListService(this.roleListParams(context)) 34 | ctl.Error(err, 400) 35 | return list 36 | } 37 | 38 | func (this CoreCtl) roleDataList(context *gin.Context) interface{} { 39 | c := this.SetContext(context) 40 | list, err := ctl.ListItemAll[*model.Role](this.DBInstance.DB, model.Role{}, nil) 41 | ctl.Error(err, 400) 42 | data := ctl.Item[[]*model.Role]("获取api列表成功", list, c) 43 | return data 44 | } 45 | 46 | // 47 | // func (this CoreCtl) roleDelete(context *gin.Context) { 48 | // Item := &common.IDSearch{} 49 | // c := this.SetContext(context) 50 | // err := c.ShouldBindQuery(Item) 51 | // ctl.Error(err, 400) 52 | // err = ctl.DeleteItem[*model2.Role](this.DB.DB, Item.ID) 53 | // ctl.Error(err, 400) 54 | // c.Success("删除角色成功", Item) 55 | // } 56 | 57 | func (this CoreCtl) ruleCreateParams(context *gin.Context) (*common.Empty, *model.RuleInput, *common.Empty, *ctl.Controller) { 58 | Item := &model.RuleInput{} 59 | c := this.SetContext(context) 60 | err := json.Unmarshal(c.Data, Item) 61 | ctl.Error(err, 400) 62 | return &common.Empty{}, Item, &common.Empty{}, c 63 | } 64 | func (this CoreCtl) ruleCreate(context *gin.Context) interface{} { 65 | data, err := this.service.RuleCreateService(this.ruleCreateParams(context)) 66 | ctl.Error(err, 400) 67 | return data 68 | } 69 | 70 | func (this CoreCtl) ruleSync(context *gin.Context) interface{} { 71 | c := this.SetContext(context) 72 | err := this.DBInstance.DB.Exec("delete from casbin_rule").Error 73 | ctl.Error(err, 400) 74 | search := model.Rule{} 75 | data, err := ctl.ListItemAll[model.Rule](this.DBInstance.DB, search, nil) 76 | for _, item := range data { 77 | this.roleCtl.AddRule(item.Role, item.Path, item.Method) 78 | } 79 | search2 := model.Role{} 80 | data2, err := ctl.ListItemAll[model.Role](this.DBInstance.DB, search2, nil) 81 | for _, item := range data2 { 82 | this.roleCtl.AddRole(item.Child, item.Parent) 83 | } 84 | return ctl.Item[common.Empty]("信息同步成功", common.Empty{}, c) 85 | } 86 | 87 | func (this CoreCtl) ruleInfoParams(context *gin.Context) (*model.RoleInput, *common.Empty, *common.Empty, *ctl.Controller) { 88 | query := &model.RoleInput{} 89 | c := this.SetContext(context) 90 | err := c.Context.ShouldBindQuery(query) 91 | ctl.Error(err, 400) 92 | return query, &common.Empty{}, &common.Empty{}, c 93 | } 94 | func (this CoreCtl) ruleInfo(context *gin.Context) interface{} { 95 | res, err := this.service.RuleInfoService(this.ruleInfoParams(context)) 96 | ctl.Error(err, 400) 97 | return res 98 | } 99 | -------------------------------------------------------------------------------- /server/api/system.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/wike2019/wike_go/pkg/func/ctl" 6 | "github.com/wike2019/wike_go/pkg/os" 7 | ) 8 | 9 | func (this CoreCtl) SystemInfo(context *gin.Context) interface{} { 10 | c := this.SetContext(context) 11 | path := c.DefaultQuery("path", "/") 12 | var s os.Server 13 | s.Os = os.InitOS() 14 | cpu, err := os.InitCPU() 15 | if err != nil { 16 | ctl.Error(err, 400) 17 | } 18 | s.Cpu = cpu 19 | ram, err := os.InitRAM() 20 | if err != nil { 21 | ctl.Error(err, 400) 22 | } 23 | s.Ram = ram 24 | disk, err := os.InitDisk(path) 25 | if err != nil { 26 | ctl.Error(err, 400) 27 | } 28 | s.Disk = disk 29 | return ctl.Item[os.Server]("获取服务器信息成功", s, c) 30 | } 31 | -------------------------------------------------------------------------------- /server/model/SysDictionary.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "gorm.io/gorm" 4 | 5 | type SysDictionary struct { 6 | gorm.Model 7 | SysDictionarySearch 8 | Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:描述"` // 描述 9 | SysDictionaryDetails []SysDictionaryDetail `json:"sysDictionaryDetails" form:"sysDictionaryDetails"` 10 | } 11 | type SysDictionarySearch struct { 12 | Name string `json:"name" form:"name" gorm:"column:name;comment:字典名(中)" search:"like"` // 字典名(中) 13 | Type string `json:"type" form:"type" gorm:"column:type;comment:分类;unique" search:"like"` // 分类,添加唯一索引 14 | Status int `json:"status" form:"status" gorm:"column:status;comment:状态" search:"true"` // 状态 15 | } 16 | 17 | func (this *SysDictionarySearch) TableName() string { 18 | return "sys_dictionaries" 19 | } 20 | 21 | func (SysDictionary) TableName() string { 22 | return "sys_dictionaries" 23 | } 24 | 25 | type SysDictionaryDetail struct { 26 | gorm.Model 27 | Label string `json:"label" form:"label" gorm:"column:label;comment:展示值"` // 展示值 28 | Value string `json:"value" form:"value" gorm:"column:value;comment:字典值"` // 字典值 29 | Extend string `json:"extend" form:"extend" gorm:"column:extend;comment:扩展值"` // 扩展值 30 | Status int `json:"status" form:"status" gorm:"column:status;comment:启用状态"` // 启用状态 31 | Sort int `json:"sort" form:"sort" gorm:"column:sort;comment:排序标记"` // 排序标记 32 | SysDictionaryID int `json:"sysDictionaryID" form:"sysDictionaryID" gorm:"column:sys_dictionary_id;comment:关联标记"` // 关联标记 33 | Desc string `json:"desc" form:"desc" gorm:"column:desc;comment:描述"` // 描述 34 | } 35 | 36 | func (SysDictionaryDetail) TableName() string { 37 | return "sys_dictionary_details" 38 | } 39 | -------------------------------------------------------------------------------- /server/model/api.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "gorm.io/gorm" 4 | 5 | // api接口模型 6 | type API struct { 7 | gorm.Model // 主键 8 | Input string `gorm:"size:2000"` // 入参数 9 | Output string `gorm:"size:2000"` // 出参数 10 | Status int `gorm:"type:int"` 11 | APISearch 12 | } 13 | 14 | // 可以用于搜索的字段 15 | type APISearch struct { 16 | Group string `gorm:"size:300" search:"like"` // 路由分组 17 | Name string `gorm:"size:300" search:"like"` // 路由名称 18 | Path string `gorm:"size:300" search:"like"` // 路由路径 19 | Method string `gorm:"size:100" search:"true"` // 请求方法 20 | } 21 | 22 | func (this *API) TableName() string { 23 | return "apis" 24 | } 25 | func (this *APISearch) TableName() string { 26 | return "apis" 27 | } 28 | 29 | // 子路有 30 | type ChildAPi struct { 31 | Value uint `json:"value"` 32 | Label string `json:"label"` 33 | Path string 34 | Method string 35 | } 36 | 37 | // 前端路由列表 38 | type APIFront struct { 39 | Value uint `json:"value"` 40 | Label string `json:"label"` 41 | Children []ChildAPi `json:"children"` 42 | } 43 | 44 | // 前端路由权限列表 45 | type APIDataForRole struct { 46 | APIList []Rule `json:"apiList"` 47 | APIParentList []Rule `json:"apiParentList"` 48 | ApiIDs []uint `json:"apiIDs"` 49 | } 50 | 51 | // 接口分组结构 52 | type APIGroup struct { 53 | Group string 54 | APIs []API 55 | } 56 | -------------------------------------------------------------------------------- /server/model/casbin.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "gorm.io/gorm" 4 | 5 | // 路由规则 6 | type Rule struct { 7 | gorm.Model 8 | RuleSearch 9 | Path string `json:"path"` 10 | Method string `json:"method"` 11 | APIId uint `json:"apiId"` 12 | } 13 | 14 | func (this *Rule) TableName() string { 15 | return "sys_rule" 16 | } 17 | 18 | // 路由搜索字段 19 | type RuleSearch struct { 20 | Role string `json:"role" search:"true"` 21 | } 22 | 23 | func (this *RuleSearch) TableName() string { 24 | return "sys_rule" 25 | } 26 | 27 | // 根据接口获取路由角色列表 28 | type AggregatedRule struct { 29 | Path string `json:"path"` 30 | Method string `json:"method"` 31 | Roles []string `json:"roles"` 32 | } 33 | 34 | // 角色列表 35 | type Role struct { 36 | gorm.Model 37 | Parent string `json:"parent"` 38 | Child string `json:"children"` 39 | } 40 | type RoleService struct { 41 | List []Role 42 | Names []string 43 | } 44 | 45 | func (this *Role) TableName() string { 46 | return "sys_role" 47 | } 48 | 49 | // 添加规则 50 | type RuleInput struct { 51 | Role string `json:"role"` 52 | APIId []uint `json:"apiId"` 53 | } 54 | 55 | // 添加角色 56 | type RoleInput struct { 57 | Role string `form:"role" search:"true"` 58 | } 59 | -------------------------------------------------------------------------------- /server/model/job.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "gorm.io/gorm" 4 | 5 | type Job struct { 6 | gorm.Model // 主键 7 | JobSearch 8 | Cron string `gorm:"size:2000"` 9 | Func string `gorm:"size:2000"` // 入参数 10 | } 11 | type JobSearch struct { 12 | Name string `gorm:"size:300" search:"like"` // 路由名称 13 | } 14 | 15 | func (this *JobSearch) TableName() string { 16 | return "jobs" 17 | } 18 | 19 | func (this *Job) TableName() string { 20 | return "jobs" 21 | } 22 | -------------------------------------------------------------------------------- /server/service/service.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | model2 "github.com/wike2019/wike_go/common" 5 | "github.com/wike2019/wike_go/pkg/core" 6 | "github.com/wike2019/wike_go/pkg/func/ctl" 7 | model "github.com/wike2019/wike_go/server/model" 8 | "gorm.io/gorm" 9 | ) 10 | 11 | type Service struct { 12 | DB *core.CoreDb 13 | } 14 | 15 | func (this *Service) GetApiService(query *ctl.Page, body *model.APISearch, header *model2.Empty, context *ctl.Controller) (*ctl.PageList[model.API], error) { 16 | data, total, err := ctl.ListItem[model.API](this.DB.DB, query.Offset, query.Count, body, nil) 17 | if err != nil { 18 | return nil, err 19 | } 20 | context.FormatTotal(total) 21 | 22 | return ctl.List[model.API]("获取api列表成功", data, context), nil 23 | } 24 | 25 | func (this *Service) GetApiFrontService(query *model2.Empty, body *model.APISearch, header *model2.Empty, context *ctl.Controller) (*ctl.DataList[[]model.APIFront], error) { 26 | data, err := ctl.ListItemAll[model.API](this.DB.DB, body, nil) 27 | if err != nil { 28 | return nil, err 29 | } 30 | res := this.convertToAPIFront(data) 31 | 32 | return ctl.Item[[]model.APIFront]("获取api列表成功", res, context), nil 33 | } 34 | func CoreService(DB *core.CoreDb) *Service { 35 | return &Service{DB: DB} 36 | } 37 | func (this *Service) convertToAPIFront(apiList []model.API) []model.APIFront { 38 | groupedAPIs := make(map[string][]model.ChildAPi) 39 | 40 | // 按 Group 分类 41 | for _, api := range apiList { 42 | child := model.ChildAPi{ 43 | Value: api.ID, 44 | Label: api.Name, 45 | Path: api.Path, 46 | Method: api.Method, 47 | } 48 | groupedAPIs[api.Group] = append(groupedAPIs[api.Group], child) 49 | } 50 | 51 | // 组装 APIFront 结构 52 | var apiFrontList []model.APIFront 53 | for group, children := range groupedAPIs { 54 | apiFrontList = append(apiFrontList, model.APIFront{ 55 | Value: 0, 56 | Label: group, 57 | Children: children, 58 | }) 59 | } 60 | 61 | return apiFrontList 62 | } 63 | 64 | func (this *Service) DictionaryListService(query *ctl.Page, body *model.SysDictionarySearch, header *model2.Empty, context *ctl.Controller) (*ctl.PageList[model.SysDictionary], error) { 65 | data, total, err := ctl.ListItem[model.SysDictionary](this.DB.DB, query.Offset, query.Count, body, nil) 66 | if err != nil { 67 | return nil, err 68 | } 69 | context.FormatTotal(total) 70 | 71 | return ctl.List[model.SysDictionary]("获取字典列表成功", data, context), nil 72 | } 73 | func (this *Service) DictionaryItemService(query *model2.IDSearch, body *model2.Empty, header *model2.Empty, context *ctl.Controller) (*ctl.DataList[model.SysDictionary], error) { 74 | res, err := ctl.GetItem[model.SysDictionary](this.DB.DB, query.ID, func(db *gorm.DB) *gorm.DB { 75 | return db.Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB { 76 | return db.Order("`sort` ASC") // 按 Author 的 name 字段排序 77 | }) 78 | }) 79 | if !ctl.IsExist(err) { 80 | return ctl.Item[model.SysDictionary]("获取字典列表成功", res, context), nil 81 | } 82 | return ctl.Item[model.SysDictionary]("获取字典列表成功", res, context), err 83 | } 84 | func (this *Service) DictionarySearchService(query *model2.KeyWordsSearch, body *model2.Empty, header *model2.Empty, context *ctl.Controller) (*ctl.DataList[model.SysDictionary], error) { 85 | res, err := ctl.GetItemByFunc[model.SysDictionary](this.DB.DB, func(db *gorm.DB) *gorm.DB { 86 | return db.Where("type=?", query.KeyWords).Preload("SysDictionaryDetails", func(db *gorm.DB) *gorm.DB { 87 | return db.Order("`sort` ASC").Where("status=2") // 按 Author 的 name 字段排序 88 | }) 89 | }) 90 | return ctl.Item[model.SysDictionary]("获取字典列表成功", res, context), err 91 | } 92 | 93 | func (this Service) RoleListService(query *model.Role, body *model2.Empty, header *model2.Empty, context *ctl.Controller) (*ctl.DataList[model.RoleService], error) { 94 | list, err := ctl.ListItemAll[model.Role](this.DB.DB, query, nil) 95 | res := make([]string, 0) 96 | for _, item := range list { 97 | res = append(res, item.Child) 98 | } 99 | return ctl.Item[model.RoleService]("获取api列表成功", model.RoleService{ 100 | List: list, 101 | Names: res, 102 | }, context), err 103 | } 104 | 105 | func (this Service) RuleCreateService(query *model2.Empty, body *model.RuleInput, header *model2.Empty, context *ctl.Controller) (*ctl.DataList[model2.Empty], error) { 106 | err := this.DB.DB.Transaction(func(tx *gorm.DB) error { 107 | delObj := model.Rule{} 108 | err := tx.Where(" role =?", body.Role).Delete(&delObj).Error 109 | for _, item := range body.APIId { 110 | if item == 0 { 111 | continue 112 | } 113 | old := model.Rule{} 114 | err = tx.Where("api_id=? and role =?", item, body.Role).First(&old).Error 115 | if ctl.IsExist(err) { 116 | continue 117 | } 118 | api := model.API{} 119 | err = tx.Where("id=?", item).First(&api).Error 120 | if err != nil { 121 | return err 122 | } 123 | data := &model.Rule{} 124 | data.APIId = item 125 | data.Role = body.Role 126 | data.Path = api.Path 127 | data.Method = api.Method 128 | err = tx.Create(data).Error 129 | if err != nil { 130 | return err 131 | } 132 | } 133 | return nil 134 | }) 135 | return ctl.Item[model2.Empty]("同步接口成功", model2.Empty{}, context), err 136 | } 137 | 138 | func (this Service) RuleInfoService(query *model.RoleInput, body *model2.Empty, header *model2.Empty, context *ctl.Controller) (*ctl.DataList[model.APIDataForRole], error) { 139 | data, err := ctl.ListItemAll[model.Rule](this.DB.DB, query, nil) 140 | if err != nil { 141 | return nil, err 142 | } 143 | ids := make([]uint, 0) 144 | for _, item := range data { 145 | ids = append(ids, item.APIId) 146 | } 147 | res := model.APIDataForRole{} 148 | res.ApiIDs = ids 149 | res.APIList = data 150 | var roleList []model.Role 151 | err = this.DB.DB.Raw(`WITH RECURSIVE parent_chain AS ( 152 | -- 初始查询,找到child = 'admin'的记录 153 | SELECT id, parent, child 154 | FROM sys_role 155 | WHERE child = ? 156 | 157 | UNION ALL 158 | 159 | -- 递归查询,找到每个parent的parent 160 | SELECT sr.id, sr.parent, sr.child 161 | FROM sys_role sr 162 | INNER JOIN parent_chain pc ON sr.child = pc.parent 163 | ) 164 | SELECT * 165 | FROM parent_chain;`, query.Role).Debug().Find(&roleList).Error 166 | if err != nil { 167 | return nil, err 168 | } 169 | roleStr := make([]string, 0) 170 | for _, item := range roleList { 171 | if item.Parent != "" { 172 | roleStr = append(roleStr, item.Parent) 173 | } 174 | } 175 | var apiList []model.Rule 176 | err = this.DB.DB.Raw(`SELECT * 177 | FROM ( 178 | SELECT *, ROW_NUMBER() OVER (PARTITION BY api_id ORDER BY created_at ASC) AS rn 179 | FROM sys_rule 180 | WHERE role IN (?) and sys_rule.deleted_at IS NULL 181 | 182 | ) t 183 | WHERE t.rn = 1;`, roleStr).Debug().Find(&apiList).Error 184 | if err != nil { 185 | return nil, err 186 | } 187 | res.APIParentList = apiList 188 | return ctl.Item[model.APIDataForRole]("同步接口成功", res, context), err 189 | } 190 | -------------------------------------------------------------------------------- /web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-recommended', 8 | 'eslint:recommended', 9 | ], 10 | rules: { 11 | 'vue/multi-word-component-names': 'off', // 关闭多单词规则 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /web/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "tabWidth": 2, 6 | "useTabs": false 7 | } 8 | -------------------------------------------------------------------------------- /web/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wike_go_web", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc -b && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "axios": "^1.7.9", 13 | "element-plus": "^2.9.1", 14 | "github-markdown-css": "^5.8.1", 15 | "marked": "^15.0.7", 16 | "vue": "^3.5.13", 17 | "vue-router": "^4.5.0" 18 | }, 19 | "devDependencies": { 20 | "@eslint/js": "^9.17.0", 21 | "@typescript-eslint/eslint-plugin": "^8.18.1", 22 | "@typescript-eslint/parser": "^8.18.1", 23 | "@vitejs/plugin-vue": "^5.2.1", 24 | "@vue/tsconfig": "^0.7.0", 25 | "eslint": "^9.17.0", 26 | "eslint-config-prettier": "^9.1.0", 27 | "eslint-plugin-prettier": "^5.2.1", 28 | "eslint-plugin-vue": "^9.32.0", 29 | "globals": "^15.14.0", 30 | "prettier": "^3.4.2", 31 | "sass": "^1.83.0", 32 | "sass-embedded": "^1.83.0", 33 | "typescript": "~5.6.2", 34 | "typescript-eslint": "^8.18.1", 35 | "vite": "^6.0.3", 36 | "vite-plugin-eslint": "^1.8.1", 37 | "vite-plugin-style-import": "^2.0.0", 38 | "vue-tsc": "^2.1.10" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /web/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 25 | 26 | 33 | -------------------------------------------------------------------------------- /web/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web/src/axios/common.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import {ElMessage} from "element-plus"; 3 | 4 | // 创建 Axios 实例并配置 5 | const apiClient = axios.create({ 6 | baseURL: '/core/', // API 的基础 URL 7 | timeout: 30000, // 请求超时时间(毫秒) 8 | }); 9 | apiClient.interceptors.response.use( 10 | function (response) { 11 | // 对响应数据做点什么 12 | if (response.data.code!=200){ 13 | ElMessage.error(response.data.msg) 14 | return 15 | } 16 | return response.data; 17 | }, function (error) { 18 | // 对响应错误做点什么 19 | ElMessage.error(error+'系统异常') 20 | } 21 | ); 22 | 23 | 24 | export {apiClient} -------------------------------------------------------------------------------- /web/src/components/HelloWorld.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 36 | 37 | 42 | -------------------------------------------------------------------------------- /web/src/components/MenuItem.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | -------------------------------------------------------------------------------- /web/src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import './style.scss' 3 | import App from './App.vue' 4 | import ElementPlus from 'element-plus'; 5 | import 'element-plus/dist/index.css' 6 | import router from './router' 7 | import 'github-markdown-css/github-markdown.css'; 8 | 9 | 10 | 11 | const app=createApp(App).use(ElementPlus).use(router) 12 | 13 | 14 | app.mount('#app'); 15 | -------------------------------------------------------------------------------- /web/src/pages/api/index.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 149 | 150 | 156 | -------------------------------------------------------------------------------- /web/src/pages/cronJob/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 55 | 56 | 61 | -------------------------------------------------------------------------------- /web/src/pages/dict/index.vue: -------------------------------------------------------------------------------- 1 | 123 | 124 | 243 | 244 | 249 | -------------------------------------------------------------------------------- /web/src/pages/dictDetail/index.vue: -------------------------------------------------------------------------------- 1 | 106 | 107 | 223 | 224 | 229 | -------------------------------------------------------------------------------- /web/src/pages/home/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 36 | 37 | 42 | -------------------------------------------------------------------------------- /web/src/pages/log/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 41 | 42 | 47 | -------------------------------------------------------------------------------- /web/src/pages/role/index.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 184 | 185 | 191 | -------------------------------------------------------------------------------- /web/src/pages/rule/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 143 | 144 | 151 | -------------------------------------------------------------------------------- /web/src/pages/settings/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 46 | 47 | 50 | -------------------------------------------------------------------------------- /web/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router'; 2 | 3 | // 基础路由 4 | const routes = [ 5 | { 6 | path: '/', 7 | component: () => import('../pages/home/index.vue') // 默认首页 8 | }, 9 | { 10 | path: '/api', 11 | component: () => import('../pages/api/index.vue') // 默认首页 12 | }, 13 | { 14 | path: '/dict', 15 | component: () => import('../pages/dict/index.vue') // 默认首页 16 | }, 17 | { 18 | path: '/dictDetail', 19 | component: () => import('../pages/dictDetail/index.vue') // 默认首页 20 | }, 21 | { 22 | path: '/role', 23 | component: () => import('../pages/role/index.vue') // 默认首页 24 | }, 25 | { 26 | path: '/rule', 27 | component: () => import('../pages/rule/index.vue') // 默认首页 28 | }, 29 | { 30 | path: '/cronJob', 31 | component: () => import('../pages/cronJob/index.vue') // 默认首页 32 | }, 33 | { 34 | path: '/log', 35 | component: () => import('../pages/log/index.vue') // 默认首页 36 | }, 37 | { 38 | path: '/settings', 39 | component: () => import('../pages/settings/index.vue') // 默认首页 40 | }, 41 | { 42 | path: '/:catchAll(.*)', // 捕获所有未匹配的路由 43 | redirect:"/" 44 | }, 45 | ]; 46 | 47 | const router = createRouter({ 48 | history: createWebHashHistory(), 49 | routes 50 | }); 51 | // 批量加载 `pages` 目录下的所有 .vue 文件 52 | const modules = import.meta.glob('../pages/**/*.vue'); 53 | function parseRoutes(routes) { 54 | return routes.map(route => { 55 | const parsedRoute = { 56 | path: route.path, 57 | name: route.name, 58 | // 动态加载组件,调整路径格式 59 | component: modules[`../pages${route.component}.vue`] || (() => Promise.reject(`Component not found: ${route.component}`)) 60 | }; 61 | 62 | // 如果存在子路由,递归解析 63 | if (route.children && route.children.length > 0) { 64 | parsedRoute.children = parseRoutes(route.children); 65 | } 66 | 67 | return parsedRoute; 68 | }); 69 | } 70 | 71 | export function addDynamicRoutes(routeData) { 72 | const dynamicRoutes = parseRoutes(routeData); 73 | dynamicRoutes.forEach(route => { 74 | router.addRoute(route); // 动态添加到路由 75 | }); 76 | } 77 | 78 | export default router; 79 | -------------------------------------------------------------------------------- /web/src/style.scss: -------------------------------------------------------------------------------- 1 | /* 2 | *Author wike 3 | */ 4 | @charset "utf-8";html{background-color:#fff;color:#000;font-size:12px} 5 | body,ul,ol,dl,dd,h1,h2,h3,h4,h5,h6,figure,form,fieldset,legend,input,textarea,button,p,blockquote,th,td,pre,xmp{margin:0;padding:0} 6 | body,input,textarea,button,select,pre,xmp,tt,code,kbd,samp{line-height:1.5;font-family:tahoma,arial,"Hiragino Sans GB",simsun,sans-serif} 7 | h1,h2,h3,h4,h5,h6,small,big,input,textarea,button,select{font-size:100%} 8 | h1,h2,h3,h4,h5,h6{font-family:tahoma,arial,"Hiragino Sans GB","微软雅黑",simsun,sans-serif} 9 | table{border-collapse:collapse;border-spacing:0;text-align:inherit} 10 | caption,th{text-align:inherit} 11 | ul,ol,menu{list-style:none} 12 | fieldset,img{border:0} 13 | img,object,input,textarea,button,select{vertical-align:middle} 14 | article,aside,footer,header,section,nav,figure,figcaption,hgroup,details,menu{display:block} 15 | audio,canvas,video{display:inline-block;*display:inline;*zoom:1} 16 | blockquote:before,blockquote:after,q:before,q:after{content:"\0020"} 17 | textarea{overflow:auto;resize:vertical} 18 | input,textarea,button,select,a{outline:0 none;border: none;} 19 | button::-moz-focus-inner,input::-moz-focus-inner{padding:0;border:0} 20 | mark{background-color:transparent} 21 | a{text-decoration: none} 22 | sup,sub{vertical-align:baseline} 23 | html {overflow-x: hidden;height: 100%;font-size: 12px;-webkit-tap-highlight-color: whitesmoke;} 24 | body {font-family: Arial, "Microsoft Yahei", "Helvetica Neue", Helvetica, sans-serif;font-size:12px;line-height: 1;-webkit-text-size-adjust: none;} 25 | hr {height: .02rem;margin: .1rem 0;border: medium none;border-top: .02rem solid #cacaca;} 26 | a {color: #303133;text-decoration: none;cursor: pointer;margin:0 5px;font-weight: bold} 27 | a:hover{color:#8c939d} 28 | /* 滚动槽 */ 29 | ::-webkit-scrollbar { 30 | width: 6px; 31 | height: 6px; 32 | } 33 | ::-webkit-scrollbar-track { 34 | border-radius: 3px; 35 | background: rgba(0,0,0,0.06); 36 | -webkit-box-shadow: inset 0 0 5px rgba(0,0,0,0.08); 37 | } 38 | /* 滚动条滑块 */ 39 | ::-webkit-scrollbar-thumb { 40 | border-radius: 3px; 41 | background: rgba(0,0,0,0.12); 42 | -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.2); 43 | } 44 | 45 | :root { 46 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 47 | line-height: 1.5; 48 | font-weight: 400; 49 | color-scheme: light dark; 50 | 51 | 52 | font-synthesis: none; 53 | text-rendering: optimizeLegibility; 54 | -webkit-font-smoothing: antialiased; 55 | -moz-osx-font-smoothing: grayscale; 56 | -webkit-text-size-adjust: 100%; 57 | } 58 | 59 | a { 60 | font-weight: 500; 61 | color: #646cff; 62 | text-decoration: inherit; 63 | } 64 | a:hover { 65 | color: #535bf2; 66 | } 67 | 68 | h1 { 69 | font-size: 3.2em; 70 | line-height: 1.1; 71 | } 72 | 73 | button { 74 | border-radius: 8px; 75 | border: 1px solid transparent; 76 | padding: 0.6em 1.2em; 77 | font-size: 1em; 78 | font-weight: 500; 79 | font-family: inherit; 80 | background-color: #1a1a1a; 81 | cursor: pointer; 82 | transition: border-color 0.25s; 83 | } 84 | button:hover { 85 | border-color: #646cff; 86 | } 87 | button:focus, 88 | button:focus-visible { 89 | outline: 4px auto -webkit-focus-ring-color; 90 | } 91 | 92 | .card { 93 | padding: 2em; 94 | } 95 | .main{flex: 1;display: block} 96 | main{display: flex;flex: 1;margin-right:15px} -------------------------------------------------------------------------------- /web/src/tools/index.js: -------------------------------------------------------------------------------- 1 | function timeFormat(dateStr) { 2 | const date = new Date(dateStr); 3 | return date.toLocaleString('zh-CN', { 4 | year: 'numeric', 5 | month: '2-digit', 6 | day: '2-digit', 7 | hour: '2-digit', 8 | minute: '2-digit', 9 | second: '2-digit', 10 | hour12: false // 使用24小时制 11 | }); 12 | } 13 | export {timeFormat} -------------------------------------------------------------------------------- /web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /web/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | 6 | /* Linting */ 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noUncheckedSideEffectImports": true 12 | }, 13 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] 14 | } 15 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import eslintPlugin from "vite-plugin-eslint"; 4 | import { fileURLToPath, URL } from 'node:url'; 5 | 6 | export default defineConfig({ 7 | plugins: [vue(), eslintPlugin()], 8 | resolve: { 9 | alias: { 10 | '@': fileURLToPath(new URL('./src', import.meta.url)), // 配置 @ 为 src 目录 11 | }, 12 | }, 13 | base: 'static', 14 | server: { 15 | host: '0.0.0.0', 16 | port: 5173, 17 | proxy: { 18 | // 代理配置 19 | '/core': { 20 | target: 'http://127.0.0.1:9999', // 后端服务地址 21 | changeOrigin: true, // 需要虚拟主机站点 22 | }, 23 | } 24 | }, 25 | }); 26 | 27 | 28 | 29 | 30 | --------------------------------------------------------------------------------