├── .env.example ├── .gitignore ├── LICENSE ├── README.md ├── artisan ├── artisan.go └── seeds │ └── seeds.go ├── common └── common.go ├── conf └── conf.go ├── controller ├── Index.go ├── access │ └── access.go ├── admin │ └── admin.go ├── broadcast │ └── broadcast.go ├── captcha │ └── captcha.go ├── category │ └── category.go ├── file │ └── file.go ├── login │ └── login.go ├── menu │ └── menu.go ├── queue │ └── queue.go ├── role │ └── role.go ├── rule │ └── rule.go ├── test │ └── test.go └── upload │ └── upload.go ├── crontab └── crontabs.go ├── go.mod ├── go.sum ├── logs └── .gitignore ├── main.go ├── middleware ├── accessLog │ └── accessLog.go ├── authCheck │ └── authCheck.go ├── global.go ├── loginCheck │ └── loginCheck.go └── loginLimiter │ └── loginLimiter.go ├── migrations ├── bin │ └── .gitignore ├── migrate_2021_06_28_111049_create_admin_table │ └── migrate.go ├── migrate_2021_07_31_152020_create_rule_table │ └── migrate.go ├── migrate_2021_08_04_112919_create_role_table │ └── migrate.go ├── migrate_2021_08_04_143652_create_role_detail_table │ └── migrate.go ├── migrate_2021_08_05_174801_update_role_detail_table │ └── migrate.go ├── migrate_2021_08_07_112213_update_rule_table │ └── migrate.go ├── migrate_2021_08_07_150304_create_menu_table │ └── migrate.go ├── migrate_2021_08_09_153916_create_category_table │ └── migrate.go ├── migrate_2021_08_17_165541_update_menu_table │ └── migrate.go ├── migrate_2021_08_18_143020_create_access_table │ └── migrate.go ├── migrate_2021_08_18_155238_update_access_table │ └── migrate.go └── migrate_2021_09_09_094754_create_file_table │ └── migrate.go ├── model ├── Access.go ├── Admin.go ├── Base.go ├── Category.go ├── File.go ├── Menu.go ├── Role.go ├── RoleDetail.go └── Rule.go ├── queue └── queue.go ├── routes └── routes.go ├── search └── search.go ├── storage └── .gitignore ├── task ├── access │ └── access.go └── app │ └── app.go ├── types ├── CommaArray.go ├── JsonArray.go ├── JsonMap.go └── Time.go ├── uploads └── .gitignore └── util └── online └── online.go /.env.example: -------------------------------------------------------------------------------- 1 | #应用名称 2 | APP_NAME=gin_web 3 | #应用key 4 | KEY=xxxx 5 | #web监听端口 6 | PORT=8777 7 | #debug模式 8 | APP_DEBUG=false 9 | #启动用户,仅限于Linux 10 | RUN_USER=nobody 11 | 12 | #mysql相关 13 | DB_HOST=127.0.0.1 14 | DB_PORT=3306 15 | DB_DATABASE=game 16 | DB_USERNAME=root 17 | DB_PASSWORD=root 18 | #最大打开连接 19 | DB_MAX_OPEN_CONNS=50 20 | #最大空闲连接 21 | DB_MAX_IDLE_CONNS=10 22 | 23 | #session存活 24 | SESSION_LIFETIME=120 25 | #验证码存活时间 26 | CAPTCHA_LIFETIME=300 27 | #分布式锁前缀 28 | LOCK_PREFIX=_lock: 29 | #缓存驱动 30 | CACHE_DRIVER=file 31 | 32 | #redis相关 33 | REDIS_HOST=127.0.0.1 34 | REDIS_PASSWORD=null 35 | REDIS_PORT=6379 36 | #连接池最大连接数 37 | REDIS_POOL_SIZE=50 38 | #最小的空闲连接数 39 | REDIS_MIN_IDLE_CONNS=10 40 | 41 | #允许上传的文件类型 42 | ALLOW_UPLOAD_TYPE=txt,png,jpg,jpeg,gif,xls,xlsx 43 | #默认队列名称 44 | DEFAULT_QUEUE=default 45 | #队列列表 46 | QUEUES=high,default,low 47 | #队列前缀 48 | QUEUE_PREFIX=queue: 49 | #任务队列前缀 50 | QUEUE_NUM=3 51 | 52 | #登录模式,single(单点登录),multi(多点登录) 53 | LOGIN_MODE=single|multi 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Example user template template 2 | ### Example user template 3 | 4 | # IntelliJ project files 5 | .idea 6 | *.iml 7 | out 8 | gen 9 | /.env 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2021] [peter] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # superAdmin 2 | 3 | 开箱即用的后台框架 4 | 5 | [中文文档](https://www.kancloud.cn/peter_yang/v001/2401726) 6 | 7 | [相关博客](https://www.zhihu.com/column/c_1394224785514082304) 8 | 9 | ### 在线demo 10 | 11 | https://www.peterdemo.net 12 | 13 | 账号:test 14 | 15 | 密码:Aa123456 16 | 17 | ### 前端仓库 18 | https://github.com/PeterYangs/superAdminPage20 19 | 20 | ### 环境要求 21 | 22 | redis 23 | 24 | mysql 25 | 26 | 27 | ### 开发模式 28 | ```shell 29 | go run main.go 30 | ``` 31 | 32 | ### 编译&部署 33 | 编译二进制文件 34 | ``` 35 | go build main.go 36 | ``` 37 | 38 | 直接运行 39 | ``` 40 | ./main 41 | 42 | 或者 43 | 44 | ./main start 45 | ``` 46 | 47 | 后台运行 48 | ``` 49 | ./main start -d 50 | ``` 51 | 52 | 安全停止 53 | ``` 54 | ./main stop 55 | ``` 56 | 57 | 58 | 59 | 运行内置命令 60 | ```shell 61 | ./main artisan 62 | ``` 63 | 64 | 65 | 66 | 67 | ### Quick start 68 | 69 | **controller** 70 | 71 | ```go 72 | package controller 73 | 74 | import ( 75 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 76 | "github.com/PeterYangs/superAdminCore/v2/response" 77 | ) 78 | 79 | func Index(c *contextPlus.Context) *response.Response { 80 | 81 | return response.Resp().Api(1, "success", "index") 82 | } 83 | ``` 84 | 85 | **route** 86 | 87 | routes/routes.go 88 | 89 | ```go 90 | func Routes(_r route.Group) { 91 | 92 | _r.Registered(route.GET, "/index", controller.Index).Bind() 93 | 94 | _r.Group("/login", func (_login route.Group) { 95 | 96 | _login.Registered(route.POST, "/login", login.Login, loginLimiter.LoginLimiter).Bind() 97 | 98 | _login.Registered(route.POST, "/logout", login.Logout).Bind() 99 | 100 | }) 101 | } 102 | ``` 103 | 104 | **session** 105 | 106 | ```go 107 | func Session(c *contextPlus.Context) *response.Response { 108 | 109 | c.Session.Set("key", "value") 110 | 111 | c.Session.Get("key") 112 | 113 | return nil 114 | } 115 | ``` 116 | 117 | **全局中间件** 118 | 119 | middleware/global.go 120 | ```go 121 | package middleware 122 | 123 | import ( 124 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 125 | "github.com/PeterYangs/superAdminCore/v2/middleware/session" 126 | "superadmin/middleware/accessLog" 127 | ) 128 | 129 | func Load() []contextPlus.HandlerFunc { 130 | 131 | return []contextPlus.HandlerFunc{ 132 | 133 | session.StartSession, 134 | accessLog.AccessLog, 135 | } 136 | } 137 | 138 | ``` 139 | 140 | 141 | 142 | **验证码** 143 | 144 | 获取验证码 145 | 146 | ```go 147 | func Captcha(c *contextPlus.Context) *response.Response { 148 | 149 | b := c.GetCaptcha() 150 | 151 | c.Header("content-type", "image/png") 152 | 153 | return response.Resp().Byte(b) 154 | } 155 | ``` 156 | 157 | 检查验证码 158 | 159 | ```go 160 | func CheckCaptcha(c *contextPlus.Context) *response.Response { 161 | 162 | code := c.Query("code") 163 | 164 | bool:= c.CheckCaptcha(code) 165 | 166 | return response.Resp().Json(gin.H{"bool":code}) 167 | 168 | } 169 | ``` 170 | 171 | **参数验证** 172 | 173 | ```go 174 | package regex 175 | 176 | import ( 177 | "gin-web/contextPlus" 178 | "github.com/gin-gonic/gin" 179 | ) 180 | 181 | // Regex 参数规则验证示例,路由为 /regex/:name ,请求为 /regex/1sds?test[]=1&test[]=2,regex标记只支持string和[]string两个类型 182 | func Regex(c *contextPlus.Context) *response.Response { 183 | 184 | type regex struct { 185 | Test []string `form:"test[]" json:"test" regex:"[0-9a-z/]+"` 186 | Name string `uri:"name" json:"name" regex:"[0-9a-z]+"` 187 | } 188 | 189 | var t regex 190 | 191 | err := c.ShouldBindPlus(&t) 192 | 193 | if err != nil { 194 | 195 | return response.Resp().Json(gin.H{"code": 2, "msg": err.Error()}) 196 | 197 | } 198 | 199 | return response.Resp().Json(gin.H{"code": 1, "msg": "hello world"}) 200 | } 201 | 202 | ``` 203 | 204 | **数据库迁移** 205 | 206 | ```shell 207 | 208 | [root@localhost ~]# ./main artisan 209 | Use the arrow keys to navigate: ↓ ↑ → ← 210 | ? 选择类型: 211 | > 数据库迁移 212 | 数据填充 213 | 生成key 214 | 生成任务类 215 | 216 | ``` 217 | 218 | 迁移文件 219 | 220 | ```go 221 | package migrate_2019_08_12_055619_create_admin_table 222 | 223 | import "gin-web/migrate" 224 | 225 | func Up() { 226 | 227 | migrate.Create("admin", func(createMigrate *migrate.Migrate) { 228 | 229 | createMigrate.Name = "migrate_2019_08_12_055619_create_admin_table" 230 | 231 | //主键 232 | createMigrate.BigIncrements("id") 233 | 234 | //int 235 | createMigrate.Integer("user_id").Unsigned().Nullable().Default(0).Unique().Comment("用户id") 236 | 237 | //varchar 238 | createMigrate.String("title", 255).Default("").Comment("标题") 239 | 240 | //text 241 | createMigrate.Text("content").Default(migrate.Null).Comment("内容") 242 | 243 | //索引 244 | createMigrate.Unique("user_id", "title") 245 | 246 | }) 247 | 248 | } 249 | 250 | func Down() { 251 | 252 | migrate.DropIfExists("admin") 253 | 254 | } 255 | 256 | ``` 257 | 258 | **限流器** 259 | 260 | ```go 261 | package loginLimiter 262 | 263 | import ( 264 | "gin-web/component/limiter" 265 | "gin-web/contextPlus" 266 | "golang.org/x/time/rate" 267 | "time" 268 | ) 269 | 270 | func LoginLimiter(c *contextPlus.Context) { 271 | 272 | //每秒生成一个令牌,桶的大小是10,第三个参数是自定义key,根据自定义的key寻找限流器(默认是每1分钟清理一次过期的限流器) 273 | if !limiter.NewLimiter(rate.Every(1*time.Second), 10, c.ClientIP()).Allow() { 274 | 275 | c.String(500, "访问频率过高") 276 | 277 | c.Abort() 278 | 279 | } 280 | 281 | } 282 | 283 | ``` 284 | 285 | **分布式锁** 286 | 287 | 非阻塞 288 | 289 | ```go 290 | func Index(c *contextPlus.Context) *response.Response { 291 | 292 | //申请一个锁,过期时间是10秒 293 | lock := redis.GetClient().Lock("lock", 10*time.Second) 294 | 295 | //释放锁 296 | defer lock.Release() 297 | 298 | //是否拿到锁 299 | if lock.Get() { 300 | 301 | return response.Resp().Json(gin.H{"res": true}) 302 | 303 | } 304 | 305 | return response.Resp().Json(gin.H{"res": false}) 306 | 307 | } 308 | ``` 309 | 310 | 阻塞 311 | 312 | ```go 313 | func Index(c *contextPlus.Context) *response.Response { 314 | 315 | //申请一个锁,过期时间是10秒 316 | lock := redis.GetClient().Lock("lock", 10*time.Second) 317 | 318 | defer lock.Release() 319 | 320 | //是否拿到锁 321 | if lock.Block(time.Second * 3) { 322 | 323 | time.Sleep(4 * time.Second) 324 | 325 | return response.Resp().Json(gin.H{"res": true}) 326 | } 327 | 328 | return response.Resp().Json(gin.H{"res": false}) 329 | 330 | } 331 | ``` 332 | 333 | **消息队列** 334 | 335 | 生成任务类 336 | ```shell 337 | [root@localhost superAdminApi20]# ./main artisan 338 | Use the arrow keys to navigate: ↓ ↑ → ← 339 | ? 选择类型: 340 | 数据库迁移 341 | 数据填充 342 | 生成key 343 | > 生成任务类 344 | ``` 345 | 346 | 任务类 347 | ```go 348 | package access 349 | 350 | import ( 351 | "gin-web/database" 352 | "gin-web/model" 353 | "gin-web/task" 354 | ) 355 | 356 | type TaskAccess struct { 357 | task.BaseTask 358 | Parameters *Parameter 359 | } 360 | 361 | type Parameter struct { 362 | task.Parameter 363 | Ip string 364 | Url string 365 | Params string 366 | AdminId float64 367 | } 368 | 369 | func NewTask(ip string, url string, params string, adminId float64) *TaskAccess { 370 | 371 | return &TaskAccess{ 372 | 373 | BaseTask: task.BaseTask{ 374 | TaskName: "access", 375 | }, 376 | Parameters: &Parameter{ 377 | Ip: ip, 378 | Url: url, 379 | Params: params, 380 | AdminId: adminId, 381 | }, 382 | } 383 | } 384 | 385 | func (t *TaskAccess) Run() error { 386 | 387 | database.GetDb().Create(&model.Access{ 388 | Ip: t.Parameters.Ip, 389 | Url: t.Parameters.Url, 390 | Params: t.Parameters.Params, 391 | AdminId: t.Parameters.AdminId, 392 | }) 393 | 394 | return nil 395 | 396 | } 397 | 398 | func (t *TaskAccess) BindParameters(p map[string]interface{}) { 399 | 400 | t.BaseTask.Bind(t.Parameters, p) 401 | 402 | } 403 | 404 | ``` 405 | 406 | 407 | 即时任务 408 | 409 | ```go 410 | package controller 411 | 412 | import ( 413 | "gin-web/contextPlus" 414 | "gin-web/queue" 415 | "gin-web/response" 416 | "gin-web/task/email" 417 | "gin-web/task/sms" 418 | ) 419 | 420 | func Task(c *contextPlus.Context) *response.Response { 421 | 422 | queue.Dispatch(email.NewTask("904801074@qq.com", "title", "content")).Queue("low").Run() 423 | 424 | return response.Resp().Api(1, "123", "") 425 | 426 | } 427 | ``` 428 | 429 | 延迟队列 430 | 431 | ```go 432 | package controller 433 | 434 | import ( 435 | "gin-web/contextPlus" 436 | "gin-web/queue" 437 | "gin-web/response" 438 | "gin-web/task/email" 439 | "gin-web/task/sms" 440 | "time" 441 | ) 442 | 443 | func Task(c *contextPlus.Context) *response.Response { 444 | 445 | queue.Dispatch(email.NewTask("904801074@qq.com", "title", "content")).Queue("low").Delay(100 * time.Second).Run() 446 | 447 | return response.Resp().Api(1, "123", "") 448 | 449 | } 450 | ``` 451 | 452 | 453 | 重试次数 454 | ```go 455 | package controller 456 | 457 | import ( 458 | "gin-web/contextPlus" 459 | "gin-web/queue" 460 | "gin-web/response" 461 | "gin-web/task/email" 462 | "gin-web/task/sms" 463 | ) 464 | 465 | func Task(c *contextPlus.Context) *response.Response { 466 | 467 | queue.Dispatch(email.NewTask("904801074@qq.com", "title", "content")).SetTryTime(3).Run() 468 | 469 | return response.Resp().Api(1, "123", "") 470 | 471 | } 472 | ``` 473 | 474 | 475 | **任务调度** 476 | 477 | crontab/crontabs.go 478 | ```go 479 | package crontab 480 | 481 | import "fmt" 482 | 483 | func Crontab(c *crontab) { 484 | 485 | c.newSchedule().everyHour().function(func() { 486 | 487 | fmt.Println("每小时") 488 | 489 | }) 490 | 491 | c.newSchedule().hourlyAt(16).everyMinute().function(func() { 492 | 493 | fmt.Println("每个16点的每分钟") 494 | 495 | }) 496 | 497 | c.newSchedule().minuteAt(18).function(func() { 498 | 499 | fmt.Println("每小时的第18分钟") 500 | 501 | }) 502 | 503 | c.newSchedule().everyMinute().function(func() { 504 | 505 | //panic("模拟报错") 506 | 507 | fmt.Println("每分钟") 508 | 509 | }) 510 | 511 | c.newSchedule().everyMinuteAt(2).function(func() { 512 | 513 | fmt.Println("每2分钟") 514 | 515 | }) 516 | 517 | c.newSchedule().everyDay().hourlyAt(16).minuteAt(36).function(func() { 518 | 519 | fmt.Println("每天16点36分") 520 | 521 | }) 522 | 523 | c.newSchedule().dayAt(23).hourlyAt(16).minuteAt(50).function(func() { 524 | 525 | fmt.Println("23号16点50分") 526 | 527 | }) 528 | 529 | c.newSchedule().dayAt(24).hourBetween(8, 10).function(func() { 530 | 531 | fmt.Println("24号8点-10点") 532 | 533 | }) 534 | 535 | c.newSchedule().hourBetween(8, 9).everyMinute().function(func() { 536 | 537 | fmt.Println("每天8点-9点每分钟") 538 | 539 | }) 540 | 541 | c.newSchedule().dayBetween(22, 24).everyHour().everyMinute().function(func() { 542 | 543 | fmt.Println("22号-24号每分钟") 544 | 545 | }) 546 | 547 | } 548 | 549 | ``` 550 | 551 | 552 | **命令行** 553 | 554 | 需要实现 555 | ```go 556 | type Artisan interface { 557 | ArtisanRun() 558 | GetName() string 559 | } 560 | ``` 561 | 一个例子 562 | ```go 563 | package demo 564 | 565 | import "github.com/PeterYangs/superAdminCore/v2/component/logs" 566 | 567 | type Demo struct { 568 | } 569 | 570 | func (d Demo) GetName() string { 571 | 572 | return "demo" 573 | } 574 | 575 | func (d Demo) ArtisanRun() { 576 | 577 | logs.NewLogs().Debug("demo") 578 | } 579 | 580 | ``` 581 | 运行 582 | ```shell 583 | ./main artisan 584 | ``` 585 | **缓存** 586 | 587 | 缓存支持两种驱动`file`和`redis`,在`.env`文件中设置`CACHE_DRIVER=redis` 588 | ```go 589 | package test 590 | 591 | import ( 592 | "fmt" 593 | "github.com/PeterYangs/superAdminCore/v2/cache" 594 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 595 | "github.com/PeterYangs/superAdminCore/v2/response" 596 | ) 597 | 598 | func Cache(c *contextPlus.Context) *response.Response { 599 | 600 | cache.Cache().Put("nice", "0", 0) 601 | 602 | fmt.Println(cache.Cache().Get("nice")) 603 | 604 | return response.Resp().Api(1, "success", "") 605 | } 606 | 607 | ``` 608 | 609 | **管理员登录模式** 610 | 611 | 在.env文件中设置 612 | ```dotenv 613 | #登录模式,single(单点登录),multi(多点登录) 614 | LOGIN_MODE=single|multi 615 | ``` 616 | 617 | 618 | 619 | 620 | -------------------------------------------------------------------------------- /artisan/artisan.go: -------------------------------------------------------------------------------- 1 | package artisan 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/artisan" 5 | "superadmin/artisan/seeds" 6 | ) 7 | 8 | func Load() []artisan.Artisan { 9 | 10 | return []artisan.Artisan{ 11 | new(seeds.Seeds), 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /artisan/seeds/seeds.go: -------------------------------------------------------------------------------- 1 | package seeds 2 | 3 | import ( 4 | "fmt" 5 | "github.com/PeterYangs/superAdminCore/v2/database" 6 | "github.com/PeterYangs/tools" 7 | "github.com/manifoldco/promptui" 8 | "gorm.io/gorm" 9 | "superadmin/common" 10 | "superadmin/model" 11 | ) 12 | 13 | type Seeds struct { 14 | } 15 | 16 | func (s Seeds) GetName() string { 17 | 18 | return "数据填充" 19 | } 20 | 21 | func (s Seeds) ArtisanRun() { 22 | 23 | prompt := promptui.Select{ 24 | Label: "选择类型", 25 | Items: []string{"重置管理员", "生成基础菜单"}, 26 | } 27 | 28 | _, result, err := prompt.Run() 29 | 30 | if err != nil { 31 | fmt.Printf("Prompt failed %v\n", err) 32 | return 33 | } 34 | 35 | switch result { 36 | 37 | case "重置管理员": 38 | 39 | s.resetRoot() 40 | 41 | case "生成基础菜单": 42 | 43 | s.addMenu() 44 | 45 | } 46 | 47 | } 48 | 49 | func (s Seeds) resetRoot() { 50 | 51 | var admin model.Admin 52 | 53 | re := database.GetDb().Model(&model.Admin{}).Where("username=?", "root").First(&admin) 54 | 55 | password := s.createPassword(16) 56 | 57 | if re.Error == gorm.ErrRecordNotFound { 58 | 59 | admin.Username = "root" 60 | 61 | admin.Email = "root@superadmin.com" 62 | 63 | admin.Password, _ = common.HashPassword(password) 64 | 65 | admin.Status = 1 66 | 67 | database.GetDb().Create(&admin) 68 | 69 | common.UpdateOrCreateOne(database.GetDb(), &model.RoleDetail{}, map[string]interface{}{"admin_id": admin.Id}, &model.RoleDetail{AdminId: int(admin.Id), RoleId: 0}) 70 | 71 | } else { 72 | 73 | admin.Password, _ = common.HashPassword(password) 74 | 75 | admin.Status = 1 76 | 77 | database.GetDb().Updates(&admin) 78 | 79 | common.UpdateOrCreateOne(database.GetDb(), &model.RoleDetail{}, map[string]interface{}{"admin_id": admin.Id}, &model.RoleDetail{AdminId: int(admin.Id), RoleId: 0}) 80 | 81 | } 82 | 83 | fmt.Println("root密码为:", password) 84 | 85 | } 86 | 87 | func (s Seeds) addMenu() { 88 | 89 | menu := []model.Menu{ 90 | {Id: 1, Title: "管理员模块", Path: "", Sort: 100, Pid: 0}, 91 | {Id: 3, Title: "管理员列表", Path: "/main/admin_list", Sort: 100, Pid: 1}, 92 | {Id: 4, Title: "分类管理", Path: "", Sort: 100, Pid: 0}, 93 | {Id: 5, Title: "分类列表", Path: "/main/category_list", Sort: 100, Pid: 4}, 94 | {Id: 6, Title: "菜单管理", Path: "", Sort: 100, Pid: 0}, 95 | {Id: 7, Title: "菜单列表", Path: "/main/menu_list", Sort: 100, Pid: 6}, 96 | {Id: 8, Title: "角色列表", Path: "/main/role_list", Sort: 100, Pid: 1}, 97 | {Id: 9, Title: "规则列表", Path: "/main/rule_list", Sort: 100, Pid: 1}, 98 | {Id: 10, Title: "消息队列", Path: "", Sort: 100, Pid: 0}, 99 | {Id: 11, Title: "即时队列", Path: "/main/queue_list", Sort: 100, Pid: 10}, 100 | {Id: 12, Title: "延迟队列", Path: "/main/queue_delay_list", Sort: 100, Pid: 10}, 101 | {Id: 13, Title: "文件管理", Path: "", Sort: 100, Pid: 0}, 102 | {Id: 14, Title: "文件列表", Path: "/main/file_list", Sort: 100, Pid: 13}, 103 | {Id: 15, Title: "日志管理", Path: "", Sort: 100, Pid: 0}, 104 | {Id: 16, Title: "日志列表", Path: "/main/access_list", Sort: 100, Pid: 15}, 105 | } 106 | 107 | re := database.GetDb().Create(&menu) 108 | 109 | if re.Error != nil { 110 | 111 | fmt.Println(re.Error) 112 | } else { 113 | 114 | fmt.Println("生成成功") 115 | } 116 | 117 | } 118 | 119 | func (s Seeds) createPassword(length int) string { 120 | 121 | password := "" 122 | 123 | c := []string{"q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "a", "s", "d", "f", "g", "h", "j", "k", "l", "z", "x", "c", "v", "b", "n", "m", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "!", "@", "#", "$", "%", "^", "&", "*"} 124 | 125 | for i := 0; i < length; i++ { 126 | 127 | password += c[tools.MtRand(0, int64(len(c)-1))] 128 | } 129 | 130 | return password 131 | } 132 | -------------------------------------------------------------------------------- /common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "crypto/hmac" 6 | "crypto/rand" 7 | "crypto/sha256" 8 | "encoding/hex" 9 | "fmt" 10 | "github.com/PeterYangs/tools" 11 | "golang.org/x/crypto/bcrypt" 12 | "gorm.io/gorm" 13 | "math/big" 14 | "os" 15 | "reflect" 16 | ) 17 | 18 | // HmacSha256 加密 19 | func HmacSha256(data string) string { 20 | hash := hmac.New(sha256.New, []byte(os.Getenv("KEY"))) //创建对应的sha256哈希加密算法 21 | hash.Write([]byte(data)) 22 | return hex.EncodeToString(hash.Sum([]byte(""))) 23 | } 24 | 25 | func Paginate(tx *gorm.DB, dest interface{}, page int, size int) map[string]interface{} { 26 | 27 | if page <= 0 { 28 | 29 | page = 1 30 | } 31 | 32 | if size <= 0 { 33 | 34 | size = 10 35 | } 36 | 37 | offset := (page - 1) * size 38 | 39 | var count int64 40 | 41 | c := tx.WithContext(context.TODO()) 42 | 43 | tx.Offset(offset).Limit(size).Find(dest) 44 | 45 | c.Count(&count) 46 | 47 | //return gin.H{"total": count, "data": dest, "page": page, "size": size} 48 | return map[string]interface{}{"total": count, "data": dest, "page": page, "size": size} 49 | 50 | } 51 | 52 | // UpdateOrCreateOne 更新或修改 53 | /** 54 | Omits 是忽略的字段 55 | */ 56 | func UpdateOrCreateOne(tx *gorm.DB, model interface{}, where map[string]interface{}, modelData interface{}, Omits ...string) error { 57 | 58 | tt := tx.Model(model) 59 | 60 | for s, i := range where { 61 | 62 | tt.Where(s+"=?", i) 63 | 64 | } 65 | 66 | re := tt.First(map[string]interface{}{}) 67 | 68 | //id := reflect.ValueOf(model).Elem().FieldByName("Id").Interface() 69 | 70 | if re.Error == gorm.ErrRecordNotFound { 71 | 72 | //fmt.Println("你xx") 73 | 74 | cRe := tx.Create(modelData) 75 | 76 | if cRe.Error != nil { 77 | 78 | return cRe.Error 79 | } 80 | 81 | return nil 82 | 83 | } 84 | 85 | if re.Error == nil { 86 | 87 | up := tx.Model(model) 88 | 89 | for s, i := range where { 90 | 91 | up.Where(s+"=?", i) 92 | } 93 | 94 | if len(Omits) > 0 { 95 | 96 | up.Omit(Omits...) 97 | 98 | } 99 | 100 | s := reflect.TypeOf(modelData).Elem() 101 | 102 | ss, b := s.FieldByName("Id") 103 | 104 | //需要更新的字段 105 | if b { 106 | 107 | fillable := ss.Tag.Get("fillable") 108 | 109 | if fillable != "" { 110 | 111 | f := tools.Explode(",", fillable) 112 | 113 | up.Select(f) 114 | } 115 | 116 | } 117 | 118 | uRe := up.Updates(modelData) 119 | 120 | if uRe.Error != nil { 121 | 122 | return uRe.Error 123 | } 124 | 125 | return nil 126 | } 127 | 128 | return re.Error 129 | 130 | } 131 | 132 | func MtRand(min, max int64) int64 { 133 | 134 | //rand.Seed(time.Now().UnixNano()) 135 | 136 | //return rand.Intn(max-min+1) + min 137 | 138 | n, _ := rand.Int(rand.Reader, big.NewInt(max-min+1)) 139 | 140 | return n.Int64() + min 141 | } 142 | 143 | // Capitalize 字符首字母大写转换 144 | func Capitalize(str string) string { 145 | var upperStr string 146 | vv := []rune(str) // 后文有介绍 147 | for i := 0; i < len(vv); i++ { 148 | if i == 0 { 149 | if vv[i] >= 97 && vv[i] <= 122 { // 后文有介绍 150 | vv[i] -= 32 // string的码表相差32位 151 | upperStr += string(vv[i]) 152 | } else { 153 | fmt.Println("Not begins with lowercase letter,") 154 | return str 155 | } 156 | } else { 157 | upperStr += string(vv[i]) 158 | } 159 | } 160 | return upperStr 161 | } 162 | 163 | func PathExists(path string) (bool, error) { 164 | _, err := os.Stat(path) 165 | if err == nil { 166 | return true, nil 167 | } 168 | if os.IsNotExist(err) { 169 | return false, nil 170 | } 171 | return false, err 172 | } 173 | 174 | func HashPassword(password string) (string, error) { 175 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), 8) 176 | return string(bytes), err 177 | } 178 | 179 | func CheckPasswordHash(password, hash string) bool { 180 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 181 | return err == nil 182 | } 183 | -------------------------------------------------------------------------------- /conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import "os" 4 | 5 | func Conf() map[string]interface{} { 6 | 7 | return map[string]interface{}{ 8 | 9 | "cookie_name": os.Getenv("APP_NAME") + "_session", //浏览器cookie名称 10 | "cookie_key": "cookie_key", //context中cookie的值的name 11 | "redis_prefix": os.Getenv("APP_NAME") + ":", //redis前缀 12 | "redis_session_key": "session:{cookie}", //session在redis中的key(带redis前缀) 13 | "captcha_key": "_captcha", //验证码的key 14 | "captcha_lifetime": os.Getenv("CAPTCHA_LIFETIME"), //验证码过期时间 15 | "lock_prefix": os.Getenv("LOCK_PREFIX"), //锁前缀 16 | "queue_register_path": "queue/queue.go", //消息队列配置路径 17 | "file_cache_path": "storage", 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /controller/Index.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 5 | "github.com/PeterYangs/superAdminCore/v2/response" 6 | ) 7 | 8 | func Index(c *contextPlus.Context) *response.Response { 9 | 10 | return response.Resp().Api(1, "success", "index") 11 | } 12 | -------------------------------------------------------------------------------- /controller/access/access.go: -------------------------------------------------------------------------------- 1 | package access 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 5 | "github.com/PeterYangs/superAdminCore/v2/database" 6 | "github.com/PeterYangs/superAdminCore/v2/response" 7 | "github.com/spf13/cast" 8 | "superadmin/common" 9 | "superadmin/model" 10 | ) 11 | 12 | func List(c *contextPlus.Context) *response.Response { 13 | 14 | roles := make([]*model.Access, 0) 15 | 16 | tx := database.GetDb().Debug().Model(&model.Access{}).Order("id desc") 17 | 18 | data := common.Paginate(tx, &roles, cast.ToInt(c.DefaultQuery("p", "1")), 10) 19 | 20 | return response.Resp().Api(1, "success", data) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /controller/admin/admin.go: -------------------------------------------------------------------------------- 1 | package admin 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 6 | "github.com/PeterYangs/superAdminCore/v2/database" 7 | "github.com/PeterYangs/superAdminCore/v2/response" 8 | "github.com/PeterYangs/superAdminCore/v2/route/allUrl" 9 | "github.com/PeterYangs/tools" 10 | "github.com/spf13/cast" 11 | "superadmin/common" 12 | "superadmin/controller/menu" 13 | "superadmin/model" 14 | "superadmin/search" 15 | ) 16 | 17 | func GetRoleList(c *contextPlus.Context) *response.Response { 18 | 19 | roles := make([]*model.Role, 0) 20 | 21 | database.GetDb().Order("id desc").Find(&roles) 22 | 23 | return response.Resp().Api(1, "success", roles) 24 | } 25 | 26 | func List(c *contextPlus.Context) *response.Response { 27 | 28 | params := c.DefaultQuery("params", "") 29 | 30 | paramsMap := make(map[string]interface{}) 31 | 32 | json.Unmarshal([]byte(params), ¶msMap) 33 | 34 | //fmt.Println(paramsMap) 35 | 36 | roles := make([]*model.Admin, 0) 37 | 38 | tx := database.GetDb().Model(&model.Admin{}).Order("id desc").Preload("RoleDetail.Role") 39 | 40 | if paramsMap["role_id"] != "" && len(paramsMap) > 0 { 41 | 42 | tx.Where("EXISTS( select * from role_detail where role_id = ? and role_detail.admin_id = admin.id )", paramsMap["role_id"]) 43 | } 44 | 45 | search.NewSearch(tx, paramsMap, []search.Field{{Key: "username", Condition: "like"}}) 46 | 47 | data := common.Paginate(tx, &roles, cast.ToInt(c.DefaultQuery("p", "1")), 10) 48 | 49 | return response.Resp().Api(1, "success", data) 50 | 51 | } 52 | 53 | func Detail(c *contextPlus.Context) *response.Response { 54 | 55 | type Form struct { 56 | Id int `json:"id" uri:"id"` 57 | } 58 | 59 | var form Form 60 | 61 | err := c.ShouldBindPlus(&form) 62 | 63 | if err != nil { 64 | 65 | return response.Resp().Api(2, err.Error(), "") 66 | 67 | } 68 | 69 | var r model.Admin 70 | 71 | database.GetDb().Where("id = ?", form.Id).Preload("RoleDetail").First(&r) 72 | 73 | type res struct { 74 | model.Admin 75 | RoleId int `json:"role_id"` 76 | } 77 | 78 | rr := res{ 79 | Admin: r, 80 | RoleId: r.RoleDetail.RoleId, 81 | } 82 | 83 | return response.Resp().Api(1, "success", rr) 84 | } 85 | 86 | func Info(c *contextPlus.Context) *response.Response { 87 | 88 | admin, _ := c.Session().Get("admin") 89 | 90 | return response.Resp().Api(1, "success", admin) 91 | } 92 | 93 | func SearchRule(c *contextPlus.Context) *response.Response { 94 | 95 | type Form struct { 96 | Keyword string `json:"keyword" form:"keyword" regex:"/admin.*"` 97 | } 98 | 99 | var form Form 100 | 101 | err := c.ShouldBindPlus(&form) 102 | 103 | if err != nil { 104 | 105 | return response.Resp().Api(1, "规则不匹配", []string{}) 106 | 107 | } 108 | 109 | all := allUrl.NewAllUrl() 110 | 111 | list := all.Search(form.Keyword) 112 | 113 | return response.Resp().Api(1, "success", list) 114 | 115 | } 116 | 117 | func Destroy(c *contextPlus.Context) *response.Response { 118 | 119 | type Form struct { 120 | Id int `json:"id" uri:"id"` 121 | } 122 | 123 | var form Form 124 | 125 | err := c.ShouldBindPlus(&form) 126 | 127 | if err != nil { 128 | 129 | return response.Resp().Api(2, err.Error(), "") 130 | } 131 | 132 | database.GetDb().Delete(&model.Admin{}, form.Id) 133 | 134 | return response.Resp().Api(1, "success", "") 135 | 136 | } 137 | 138 | func GetMyMenu(c *contextPlus.Context) *response.Response { 139 | 140 | menus := make([]*model.Menu, 0) 141 | 142 | list := menu.GetMenu(0, &menus) 143 | 144 | admin, _ := c.Session().Get("admin") 145 | 146 | id := admin.(map[string]interface{})["id"].(float64) 147 | 148 | var r model.Admin 149 | 150 | database.GetDb().Where("id = ?", id).Preload("RoleDetail.Role").First(&r) 151 | 152 | //超级管理员显示所有菜单 153 | if r.RoleDetail.RoleId == 0 { 154 | 155 | return response.Resp().Api(1, "success", list) 156 | } 157 | 158 | rules := make([]*model.Rule, 0) 159 | 160 | var ids []string 161 | 162 | for _, rule := range r.RoleDetail.Role.Rules { 163 | 164 | ids = append(ids, cast.ToString(rule)) 165 | } 166 | 167 | database.GetDb().Model(&model.Rule{}).Where("id in ?", ids).Find(&rules) 168 | 169 | var rulesArray []string 170 | 171 | for _, rule := range rules { 172 | 173 | rulesArray = append(rulesArray, rule.Rule) 174 | } 175 | 176 | var temp []*model.Menu 177 | 178 | for _, m := range *list { 179 | 180 | if m.Pid == 0 { 181 | 182 | temp = append(temp, m) 183 | 184 | continue 185 | } 186 | 187 | if tools.InArray(rulesArray, m.Rule) { 188 | 189 | temp = append(temp, m) 190 | } 191 | 192 | } 193 | 194 | return response.Resp().Api(1, "success", temp) 195 | } 196 | 197 | func RoleList(c *contextPlus.Context) *response.Response { 198 | 199 | roles := make([]*model.Role, 0) 200 | 201 | database.GetDb().Model(&model.Role{}).Find(&roles) 202 | 203 | return response.Resp().Api(1, "success", roles) 204 | 205 | } 206 | 207 | func GetAllRule(c *contextPlus.Context) *response.Response { 208 | 209 | admin, _ := c.Session().Get("admin") 210 | 211 | id := admin.(map[string]interface{})["id"].(float64) 212 | 213 | var r model.Admin 214 | 215 | database.GetDb().Where("id = ?", id).Preload("RoleDetail.Role").First(&r) 216 | 217 | if r.RoleDetail.RoleId == 0 { 218 | 219 | return response.Resp().Api(1, "success", true) 220 | } 221 | 222 | rules := make([]*model.Rule, 0) 223 | 224 | ids := make([]string, len(r.RoleDetail.Role.Rules)) 225 | 226 | for i, rule := range r.RoleDetail.Role.Rules { 227 | 228 | ids[i] = cast.ToString(rule) 229 | } 230 | 231 | database.GetDb().Where("id in ?", ids).Find(&rules) 232 | 233 | list := make([]string, len(r.RoleDetail.Role.Rules)) 234 | 235 | for i, rule := range rules { 236 | 237 | list[i] = rule.Rule 238 | 239 | } 240 | 241 | return response.Resp().Api(1, "success", list) 242 | 243 | } 244 | -------------------------------------------------------------------------------- /controller/broadcast/broadcast.go: -------------------------------------------------------------------------------- 1 | package broadcast 2 | 3 | import ( 4 | "fmt" 5 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 6 | "github.com/PeterYangs/superAdminCore/v2/response" 7 | "github.com/gorilla/websocket" 8 | "net/http" 9 | "superadmin/util/online" 10 | ) 11 | 12 | var broadcast = websocket.Upgrader{ 13 | ReadBufferSize: 1024, 14 | WriteBufferSize: 1024, 15 | CheckOrigin: checkOrigin, 16 | } 17 | 18 | func Broadcast(c *contextPlus.Context) *response.Response { 19 | 20 | conn, err := broadcast.Upgrade(c.Writer, c.Request, nil) 21 | 22 | if err != nil { 23 | 24 | fmt.Println(err) 25 | 26 | return response.Resp().Api(1, err.Error(), "") 27 | } 28 | 29 | on := online.NewOnline() 30 | 31 | //实例化一个新连接对象 32 | onlineConn := online.NewConn(conn) 33 | 34 | onlineConn.SetAdminId(c.GetAdminId()) 35 | 36 | //添加一个新连接 37 | id := on.Add(onlineConn) 38 | 39 | //上线后发送一个在线人数数据 40 | onlineConn.SendJson(1, "total", "success", on.GetTotal()) 41 | 42 | go func() { 43 | 44 | //defer 45 | 46 | defer func() { 47 | 48 | on.Del(id) 49 | 50 | conn.Close() 51 | }() 52 | 53 | for { 54 | 55 | _, msg, err := conn.ReadMessage() 56 | 57 | //msg. 58 | 59 | if err != nil { 60 | 61 | fmt.Println(err) 62 | 63 | return 64 | } 65 | 66 | //重置上次访问时间 67 | onlineConn.SetReplyTime() 68 | 69 | message := online.NewMessage(msg) 70 | 71 | switch message.Types { 72 | 73 | case "ping": 74 | 75 | onlineConn.SendJson(1, "ping", "", []string{}) 76 | 77 | } 78 | 79 | //conn.WriteJSON(map[string]interface{}{"data": msg}) 80 | 81 | } 82 | 83 | }() 84 | 85 | return response.Resp().Nil() 86 | } 87 | 88 | // GroupMessage 群发测试 89 | func GroupMessage(c *contextPlus.Context) *response.Response { 90 | 91 | on := online.NewOnline() 92 | 93 | on.SendAllMessage(online.Message{Code: 1, Types: "group_test", Message: "群发测试"}) 94 | 95 | return response.Resp().Api(1, "success", []string{}) 96 | } 97 | 98 | func checkOrigin(r *http.Request) bool { 99 | 100 | return true 101 | } 102 | -------------------------------------------------------------------------------- /controller/captcha/captcha.go: -------------------------------------------------------------------------------- 1 | package captcha 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 5 | "github.com/PeterYangs/superAdminCore/v2/response" 6 | ) 7 | 8 | func Captcha(c *contextPlus.Context) *response.Response { 9 | 10 | b := c.GetCaptcha() 11 | 12 | c.Header("content-type", "image/png") 13 | 14 | return response.Resp().Byte(b) 15 | } 16 | -------------------------------------------------------------------------------- /controller/category/category.go: -------------------------------------------------------------------------------- 1 | package category 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 5 | "github.com/PeterYangs/superAdminCore/v2/database" 6 | "github.com/PeterYangs/superAdminCore/v2/response" 7 | "github.com/spf13/cast" 8 | "gorm.io/gorm" 9 | "superadmin/common" 10 | "superadmin/model" 11 | ) 12 | 13 | func List(c *contextPlus.Context) *response.Response { 14 | 15 | category := make([]*model.Category, 0) 16 | 17 | return response.Resp().Api(1, "success", getMenu(0, &category)) 18 | 19 | } 20 | 21 | //递归查询 22 | func getMenu(pid int, m *[]*model.Category) *[]*model.Category { 23 | 24 | menus := make([]*model.Category, 0) 25 | 26 | err := database.GetDb().Model(&model.Category{}).Where("pid=?", pid).Order("sort asc").Find(&menus) 27 | 28 | if err.Error == gorm.ErrRecordNotFound { 29 | 30 | return nil 31 | } 32 | 33 | for _, menu := range menus { 34 | 35 | *m = append(*m, menu) 36 | 37 | getMenu(int(menu.Id), m) 38 | 39 | } 40 | 41 | return m 42 | } 43 | 44 | func Update(c *contextPlus.Context) *response.Response { 45 | 46 | type Form struct { 47 | Id int `json:"id" form:"id"` 48 | Pid int `json:"pid" form:"pid"` 49 | Lv int `json:"lv" form:"lv"` 50 | Title string `json:"title" form:"title" binding:"required"` 51 | Img string `json:"img" form:"img" ` 52 | Sort int `json:"sort" form:"sort" binding:"required"` 53 | //Path string `json:"path"` 54 | } 55 | 56 | var form Form 57 | 58 | err := c.ShouldBindPlus(&form) 59 | 60 | if err != nil { 61 | 62 | return response.Resp().Api(2, err.Error(), "") 63 | } 64 | 65 | category := model.Category{ 66 | Id: uint(form.Id), 67 | Pid: form.Pid, 68 | Lv: form.Lv, 69 | Title: form.Title, 70 | Img: form.Img, 71 | Sort: form.Sort, 72 | } 73 | 74 | if category.Id == 0 { 75 | 76 | category.Lv++ 77 | } 78 | 79 | err = common.UpdateOrCreateOne(database.GetDb(), &model.Category{}, map[string]interface{}{"id": form.Id}, &category) 80 | 81 | if err != nil { 82 | 83 | return response.Resp().Api(2, err.Error(), "") 84 | } 85 | 86 | //fmt.Println(category.Base.CreatedAt) 87 | 88 | //if category.Id == 0 { 89 | 90 | if category.Pid == 0 { 91 | 92 | category.Path = cast.ToString(category.Id) 93 | 94 | //database.GetDb().Omit("CreatedAt").Save(&category) 95 | 96 | database.GetDb().Updates(&category) 97 | 98 | } else { 99 | 100 | var pCategory model.Category 101 | 102 | database.GetDb().Model(&model.Category{}).Where("id=?", category.Pid).First(&pCategory) 103 | 104 | category.Path = pCategory.Path + "," + cast.ToString(category.Id) 105 | 106 | database.GetDb().Save(&category) 107 | 108 | } 109 | 110 | //} 111 | 112 | return response.Resp().Api(1, "success", form) 113 | } 114 | -------------------------------------------------------------------------------- /controller/file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 5 | "github.com/PeterYangs/superAdminCore/v2/database" 6 | "github.com/PeterYangs/superAdminCore/v2/response" 7 | "github.com/spf13/cast" 8 | "os" 9 | "superadmin/common" 10 | "superadmin/model" 11 | ) 12 | 13 | func Update(c *contextPlus.Context) *response.Response { 14 | 15 | type Form struct { 16 | Id int `json:"id" form:"id"` 17 | Path string `json:"path" form:"path" binding:"required"` 18 | Name string `json:"name" form:"name" binding:"required"` 19 | Size int64 `json:"size" form:"size" binding:"required"` 20 | } 21 | 22 | var form Form 23 | 24 | err := c.ShouldBindPlus(&form) 25 | 26 | if err != nil { 27 | 28 | return response.Resp().Api(2, err.Error(), "") 29 | } 30 | 31 | file := model.File{ 32 | Id: uint(form.Id), 33 | Path: form.Path, 34 | Name: form.Name, 35 | Size: form.Size, 36 | AdminId: c.GetAdminId(), 37 | } 38 | 39 | err = common.UpdateOrCreateOne(database.GetDb(), &model.File{}, map[string]interface{}{"id": form.Id}, &file) 40 | 41 | if err != nil { 42 | 43 | return response.Resp().Api(2, err.Error(), "") 44 | } 45 | 46 | return response.Resp().Api(1, "success", "") 47 | } 48 | 49 | func List(c *contextPlus.Context) *response.Response { 50 | 51 | files := make([]*model.File, 0) 52 | 53 | tx := database.GetDb().Preload("Admin").Model(&model.File{}).Order("id desc") 54 | 55 | data := common.Paginate(tx, &files, cast.ToInt(c.DefaultQuery("p", "1")), 10) 56 | 57 | return response.Resp().Api(1, "success", data) 58 | } 59 | 60 | func Destroy(c *contextPlus.Context) *response.Response { 61 | 62 | type Form struct { 63 | Id int `json:"id" uri:"id"` 64 | } 65 | 66 | var form Form 67 | 68 | err := c.ShouldBindPlus(&form) 69 | 70 | if err != nil { 71 | 72 | return response.Resp().Api(2, err.Error(), "") 73 | 74 | } 75 | 76 | var file model.File 77 | 78 | database.GetDb().First(&file, form.Id) 79 | 80 | database.GetDb().Delete(&model.File{}, form.Id) 81 | 82 | //删除对应文件 83 | os.Remove("uploads/" + file.Path) 84 | 85 | return response.Resp().Api(1, "success", "") 86 | 87 | } 88 | -------------------------------------------------------------------------------- /controller/login/login.go: -------------------------------------------------------------------------------- 1 | package login 2 | 3 | import ( 4 | "context" 5 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 6 | "github.com/PeterYangs/superAdminCore/v2/database" 7 | "github.com/PeterYangs/superAdminCore/v2/redis" 8 | "github.com/PeterYangs/superAdminCore/v2/response" 9 | regexp "github.com/dlclark/regexp2" 10 | "github.com/spf13/cast" 11 | "os" 12 | "strings" 13 | "superadmin/common" 14 | "superadmin/model" 15 | ) 16 | 17 | func Login(c *contextPlus.Context) *response.Response { 18 | 19 | type Form struct { 20 | Username string `json:"username" form:"username" binding:"required"` 21 | Password string `json:"password" form:"password" binding:"required"` 22 | Captcha string `json:"captcha" form:"captcha" binding:"required"` 23 | } 24 | 25 | var form Form 26 | 27 | err := c.ShouldBindPlus(&form) 28 | 29 | if err != nil { 30 | 31 | return response.Resp().Api(2, err.Error(), "") 32 | 33 | } 34 | 35 | if !c.CheckCaptcha(form.Captcha) { 36 | 37 | return response.Resp().Api(2, "验证码错误", "") 38 | } 39 | 40 | var admin model.Admin 41 | 42 | if err != nil { 43 | 44 | return response.Resp().Api(2, err.Error(), "") 45 | } 46 | 47 | re := database.GetDb().Where("username = ?", strings.TrimSpace(form.Username)).First(&admin) 48 | 49 | if re.Error != nil { 50 | 51 | return response.Resp().Api(2, "用户不存在", "") 52 | } 53 | 54 | hash := admin.Password 55 | 56 | ok := common.CheckPasswordHash(form.Password, hash) 57 | 58 | if !ok { 59 | 60 | return response.Resp().Api(2, "密码错误", "") 61 | } 62 | 63 | c.Session().Set("admin", admin) 64 | 65 | //单点登录模式 66 | if os.Getenv("LOGIN_MODE") == "single" { 67 | 68 | //获取该id下的所有session key 69 | list, err := redis.GetClient().LRange(context.Background(), "_id:"+cast.ToString(admin.Id), 0, 10).Result() 70 | 71 | if err != nil { 72 | 73 | goto SUCCESS 74 | } 75 | 76 | //删除所有该id下的所有session 77 | redis.GetClient().Del(context.Background(), list...) 78 | 79 | //删除id记录 80 | redis.GetClient().Del(context.Background(), "_id:"+cast.ToString(admin.Id)) 81 | 82 | //记录id的session key 83 | redis.GetClient().LPush(context.Background(), "_id:"+cast.ToString(admin.Id), c.Session().Key()) 84 | } 85 | 86 | SUCCESS: 87 | 88 | return response.Resp().Api(1, "success", "") 89 | } 90 | 91 | // Registered 后台管理员添加 92 | func Registered(c *contextPlus.Context) *response.Response { 93 | 94 | type Validator struct { 95 | Username string `json:"username" form:"username" binding:"required"` 96 | Password string `json:"password" form:"password" ` 97 | RePassword string `form:"repassword"` 98 | Email string `json:"email" form:"email" binding:"required"` 99 | RoleId int `json:"role_id" form:"role_id"` 100 | Id int `json:"id" form:"id"` 101 | } 102 | 103 | var form Validator 104 | 105 | err := c.ShouldBindPlus(&form) 106 | 107 | if err != nil { 108 | 109 | return response.Resp().Api(2, err.Error(), "") 110 | 111 | } 112 | 113 | //新增 114 | if form.Id == 0 { 115 | 116 | if form.Password == "" { 117 | 118 | return response.Resp().Api(2, "密码不能为空", "") 119 | 120 | } 121 | 122 | if form.Password != form.RePassword { 123 | 124 | return response.Resp().Api(2, "两次密码不一致", "") 125 | } 126 | 127 | ok := database.GetDb().Where("username = ?", form.Username).Or("email = ?", form.Email).First(&model.Admin{}) 128 | 129 | if ok.Error == nil { 130 | 131 | return response.Resp().Api(2, "用户名已被注册", "") 132 | 133 | } 134 | 135 | } else { 136 | 137 | if form.Password != form.RePassword { 138 | 139 | return response.Resp().Api(2, "两次密码不一致", "") 140 | } 141 | 142 | } 143 | 144 | if form.Password != "" { 145 | 146 | reg := regexp.MustCompile(`^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,100}$`, 0) 147 | 148 | ok, _ := reg.MatchString(form.Password) 149 | 150 | if !ok { 151 | 152 | return response.Resp().Api(2, "密码必须由数字和大小写字母组成", "") 153 | } 154 | 155 | } 156 | 157 | tx := database.GetDb().Begin() 158 | 159 | hash, err := common.HashPassword(form.Password) 160 | 161 | if err != nil { 162 | 163 | return response.Resp().Api(2, err.Error(), "") 164 | } 165 | 166 | admin := model.Admin{ 167 | Username: form.Username, 168 | Password: hash, 169 | Email: form.Email, 170 | Id: uint(form.Id), 171 | } 172 | 173 | var omits []string 174 | 175 | //密码为空则忽略字段更新 176 | if form.Password == "" { 177 | 178 | omits = append(omits, "password") 179 | } 180 | 181 | //不能更新用户名 182 | if form.Id != 0 { 183 | 184 | omits = append(omits, "username") 185 | } 186 | 187 | err = common.UpdateOrCreateOne(tx, &model.Admin{}, map[string]interface{}{"id": admin.Id}, &admin, omits...) 188 | 189 | if err != nil { 190 | 191 | tx.Rollback() 192 | 193 | return response.Resp().Api(2, err.Error(), "") 194 | } 195 | 196 | err = common.UpdateOrCreateOne(tx, &model.RoleDetail{}, map[string]interface{}{"admin_id": admin.Id}, &model.RoleDetail{AdminId: int(admin.Id), RoleId: form.RoleId}) 197 | 198 | if err != nil { 199 | 200 | tx.Rollback() 201 | 202 | return response.Resp().Api(2, err.Error(), "") 203 | } 204 | 205 | tx.Commit() 206 | 207 | return response.Resp().Api(1, "success", "") 208 | 209 | } 210 | 211 | func Logout(c *contextPlus.Context) *response.Response { 212 | 213 | c.Session().Remove("admin") 214 | 215 | return response.Resp().Api(1, "success", "") 216 | } 217 | -------------------------------------------------------------------------------- /controller/menu/menu.go: -------------------------------------------------------------------------------- 1 | package menu 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 5 | "github.com/PeterYangs/superAdminCore/v2/database" 6 | "github.com/PeterYangs/superAdminCore/v2/response" 7 | "gorm.io/gorm" 8 | "superadmin/common" 9 | "superadmin/model" 10 | ) 11 | 12 | func GetFatherMenu(c *contextPlus.Context) *response.Response { 13 | 14 | menus := make([]*model.Menu, 0) 15 | 16 | database.GetDb().Model(&model.Menu{}).Where("pid = ?", 0).Find(&menus) 17 | 18 | return response.Resp().Api(1, "success", menus) 19 | 20 | } 21 | 22 | func Update(c *contextPlus.Context) *response.Response { 23 | 24 | type Form struct { 25 | Title string `json:"title" form:"title" binding:"required"` 26 | Pid int `json:"pid" form:"pid"` 27 | Id int `json:"id" form:"id"` 28 | Path string `json:"path" form:"path"` 29 | Sort int `json:"sort" form:"sort" binding:"required"` 30 | Rule string `json:"rule" form:"rule"` 31 | } 32 | 33 | var form Form 34 | 35 | err := c.ShouldBindPlus(&form) 36 | 37 | if err != nil { 38 | 39 | return response.Resp().Api(2, err.Error(), "") 40 | } 41 | 42 | menu := model.Menu{ 43 | Id: uint(form.Id), 44 | Pid: form.Pid, 45 | Title: form.Title, 46 | Path: form.Path, 47 | Sort: form.Sort, 48 | Rule: form.Rule, 49 | } 50 | 51 | err = common.UpdateOrCreateOne(database.GetDb(), &model.Menu{}, map[string]interface{}{"id": form.Id}, &menu) 52 | 53 | if err != nil { 54 | 55 | return response.Resp().Api(2, err.Error(), "") 56 | 57 | } 58 | 59 | return response.Resp().Api(1, "success", "") 60 | 61 | } 62 | 63 | func List(c *contextPlus.Context) *response.Response { 64 | 65 | menus := make([]*model.Menu, 0) 66 | 67 | return response.Resp().Api(1, "success", GetMenu(0, &menus)) 68 | 69 | } 70 | 71 | func GetMenu(pid int, m *[]*model.Menu) *[]*model.Menu { 72 | 73 | menus := make([]*model.Menu, 0) 74 | 75 | err := database.GetDb().Model(&model.Menu{}).Where("pid=?", pid).Order("sort asc").Find(&menus) 76 | 77 | if err.Error == gorm.ErrRecordNotFound { 78 | 79 | return nil 80 | } 81 | 82 | for _, menu := range menus { 83 | 84 | *m = append(*m, menu) 85 | 86 | GetMenu(int(menu.Id), m) 87 | 88 | } 89 | 90 | return m 91 | } 92 | 93 | func Detail(c *contextPlus.Context) *response.Response { 94 | 95 | type Form struct { 96 | Id int `json:"id" uri:"id"` 97 | } 98 | 99 | var form Form 100 | 101 | err := c.ShouldBindPlus(&form) 102 | 103 | if err != nil { 104 | 105 | return response.Resp().Api(2, err.Error(), "") 106 | } 107 | 108 | var r model.Menu 109 | 110 | database.GetDb().Where("id = ?", form.Id).First(&r) 111 | 112 | return response.Resp().Api(1, "success", r) 113 | 114 | } 115 | -------------------------------------------------------------------------------- /controller/queue/queue.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 7 | "github.com/PeterYangs/superAdminCore/v2/redis" 8 | "github.com/PeterYangs/superAdminCore/v2/response" 9 | "github.com/spf13/cast" 10 | "os" 11 | ) 12 | 13 | func List(c *contextPlus.Context) *response.Response { 14 | 15 | p := cast.ToInt(c.DefaultQuery("p", "1")) 16 | 17 | queue := c.DefaultQuery("queue", os.Getenv("DEFAULT_QUEUE")) 18 | 19 | size := 10 20 | 21 | start := (p - 1) * size 22 | 23 | stop := start + size 24 | 25 | list, _ := redis.GetClient().LRange(context.TODO(), os.Getenv("QUEUE_PREFIX")+queue, int64(start), int64(stop)).Result() 26 | 27 | count, _ := redis.GetClient().LLen(context.TODO(), os.Getenv("QUEUE_PREFIX")+queue).Result() 28 | 29 | if len(list) <= 0 { 30 | 31 | return response.Resp().Api(1, "success", map[string]interface{}{"total": 0, "data": []string{}, "page": 1, "size": size}) 32 | } 33 | 34 | jList := make([]map[string]interface{}, len(list)) 35 | 36 | for i, s := range list { 37 | 38 | temp := make(map[string]interface{}) 39 | 40 | json.Unmarshal([]byte(s), &temp) 41 | 42 | jList[i] = temp 43 | } 44 | 45 | return response.Resp().Api(1, "success", map[string]interface{}{"total": count, "data": jList, "page": p, "size": size}) 46 | } 47 | 48 | func DelayList(c *contextPlus.Context) *response.Response { 49 | 50 | p := cast.ToInt(c.DefaultQuery("p", "1")) 51 | 52 | //queue := c.DefaultQuery("queue", os.Getenv("DEFAULT_QUEUE")) 53 | 54 | size := 10 55 | 56 | start := (p - 1) * size 57 | 58 | stop := start + size 59 | 60 | list, _ := redis.GetClient().ZRange(context.TODO(), os.Getenv("QUEUE_PREFIX")+"delay", int64(start), int64(stop)).Result() 61 | 62 | count, _ := redis.GetClient().ZCard(context.TODO(), os.Getenv("QUEUE_PREFIX")+"delay").Result() 63 | 64 | if len(list) <= 0 { 65 | 66 | return response.Resp().Api(1, "success", map[string]interface{}{"total": 0, "data": []string{}, "page": 1, "size": size}) 67 | } 68 | 69 | jList := make([]map[string]interface{}, len(list)) 70 | 71 | for i, s := range list { 72 | 73 | temp := make(map[string]interface{}) 74 | 75 | json.Unmarshal([]byte(s), &temp) 76 | 77 | jList[i] = temp 78 | } 79 | 80 | return response.Resp().Api(1, "success", map[string]interface{}{"total": count, "data": jList, "page": p, "size": size}) 81 | } 82 | -------------------------------------------------------------------------------- /controller/role/role.go: -------------------------------------------------------------------------------- 1 | package role 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 5 | "github.com/PeterYangs/superAdminCore/v2/database" 6 | "github.com/PeterYangs/superAdminCore/v2/response" 7 | "github.com/spf13/cast" 8 | "superadmin/common" 9 | "superadmin/model" 10 | ) 11 | 12 | func GetRuleList(c *contextPlus.Context) *response.Response { 13 | 14 | rules := make([]*model.Rule, 0) 15 | 16 | database.GetDb().Find(&rules) 17 | 18 | group := make(map[string][]*model.Rule) 19 | 20 | for _, rule := range rules { 21 | 22 | group[rule.GroupName] = append(group[rule.GroupName], rule) 23 | 24 | } 25 | 26 | return response.Resp().Api(1, "success", group) 27 | } 28 | 29 | func Update(c *contextPlus.Context) *response.Response { 30 | 31 | type Form struct { 32 | Title string `json:"title" form:"title" binding:"required"` 33 | Rules []int `json:"rules" form:"rules" binding:"required"` 34 | Id int `json:"id" form:"id"` 35 | } 36 | 37 | var form Form 38 | 39 | err := c.ShouldBindPlus(&form) 40 | 41 | if err != nil { 42 | 43 | return response.Resp().Api(2, err.Error(), "") 44 | 45 | } 46 | 47 | r := model.Role{ 48 | 49 | Title: form.Title, 50 | Rules: form.Rules, 51 | Id: uint(form.Id), 52 | } 53 | 54 | if r.Id == 0 { 55 | 56 | database.GetDb().Create(&r) 57 | 58 | } else { 59 | 60 | database.GetDb().Model(&r).Updates(&r) 61 | 62 | } 63 | 64 | return response.Resp().Api(1, "success", form) 65 | } 66 | 67 | func List(c *contextPlus.Context) *response.Response { 68 | 69 | roles := make([]*model.Role, 0) 70 | 71 | tx := database.GetDb().Model(&model.Role{}) 72 | 73 | data := common.Paginate(tx, &roles, cast.ToInt(c.DefaultQuery("p", "1")), 10) 74 | 75 | return response.Resp().Api(1, "success", data) 76 | 77 | } 78 | 79 | func Detail(c *contextPlus.Context) *response.Response { 80 | 81 | type Form struct { 82 | Id int `json:"id" uri:"id"` 83 | } 84 | 85 | var form Form 86 | 87 | err := c.ShouldBindPlus(&form) 88 | 89 | if err != nil { 90 | 91 | return response.Resp().Api(2, err.Error(), "") 92 | 93 | } 94 | 95 | var r model.Role 96 | 97 | database.GetDb().Where("id = ?", form.Id).First(&r) 98 | 99 | return response.Resp().Api(1, "success", r) 100 | } 101 | 102 | func Destroy(c *contextPlus.Context) *response.Response { 103 | 104 | type Form struct { 105 | Id int `json:"id" uri:"id"` 106 | } 107 | 108 | var form Form 109 | 110 | err := c.ShouldBindPlus(&form) 111 | 112 | if err != nil { 113 | 114 | return response.Resp().Api(2, err.Error(), "") 115 | 116 | } 117 | 118 | database.GetDb().Delete(&model.Role{}, form.Id) 119 | 120 | return response.Resp().Api(1, "success", "") 121 | 122 | } 123 | -------------------------------------------------------------------------------- /controller/rule/rule.go: -------------------------------------------------------------------------------- 1 | package rule 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 5 | "github.com/PeterYangs/superAdminCore/v2/database" 6 | "github.com/PeterYangs/superAdminCore/v2/response" 7 | "github.com/spf13/cast" 8 | "superadmin/common" 9 | "superadmin/model" 10 | ) 11 | 12 | func Update(c *contextPlus.Context) *response.Response { 13 | 14 | type Form struct { 15 | Title string `json:"title" form:"title" binding:"required"` 16 | Rule string `json:"rule" form:"rule" binding:"required"` 17 | GroupName string `json:"group_name" form:"group_name" ` 18 | Id uint `json:"id" form:"id"` 19 | } 20 | 21 | var form Form 22 | 23 | err := c.ShouldBind(&form) 24 | 25 | if err != nil { 26 | 27 | return response.Resp().Api(2, err.Error(), "") 28 | } 29 | 30 | r := model.Rule{ 31 | GroupName: form.GroupName, 32 | Title: form.Title, 33 | Rule: form.Rule, 34 | Id: form.Id, 35 | } 36 | 37 | if r.Id == 0 { 38 | 39 | database.GetDb().Create(&r) 40 | 41 | } else { 42 | 43 | database.GetDb().Model(&r).Updates(&r) 44 | 45 | } 46 | 47 | return response.Resp().Api(1, "测试消息", r) 48 | 49 | } 50 | 51 | func List(c *contextPlus.Context) *response.Response { 52 | 53 | rules := make([]*model.Rule, 0) 54 | 55 | tx := database.GetDb().Model(&model.Rule{}) 56 | 57 | data := common.Paginate(tx, &rules, cast.ToInt(c.DefaultQuery("p", "1")), 10) 58 | 59 | return response.Resp().Api(1, "success", data) 60 | 61 | } 62 | 63 | func Detail(c *contextPlus.Context) *response.Response { 64 | 65 | type Form struct { 66 | Id int `json:"id" uri:"id"` 67 | } 68 | 69 | var form Form 70 | 71 | err := c.ShouldBindPlus(&form) 72 | 73 | if err != nil { 74 | 75 | return response.Resp().Api(2, err.Error(), "") 76 | } 77 | 78 | var r model.Rule 79 | 80 | database.GetDb().Where("id = ?", form.Id).First(&r) 81 | 82 | return response.Resp().Api(1, "success", r) 83 | } 84 | 85 | func Destroy(c *contextPlus.Context) *response.Response { 86 | 87 | type Form struct { 88 | Id int `json:"id" uri:"id"` 89 | } 90 | 91 | var form Form 92 | 93 | err := c.ShouldBindPlus(&form) 94 | 95 | if err != nil { 96 | 97 | return response.Resp().Api(2, err.Error(), "") 98 | } 99 | 100 | database.GetDb().Delete(&model.Rule{}, form.Id) 101 | 102 | return response.Resp().Api(1, "success", "") 103 | 104 | } 105 | -------------------------------------------------------------------------------- /controller/test/test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/PeterYangs/superAdminCore/v2/cache" 6 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 7 | "github.com/PeterYangs/superAdminCore/v2/response" 8 | ) 9 | 10 | func Cache(c *contextPlus.Context) *response.Response { 11 | 12 | cache.Cache().Put("nice", "0", 0) 13 | 14 | fmt.Println(cache.Cache().Get("nice")) 15 | 16 | return response.Resp().Api(1, "success", "") 17 | } 18 | -------------------------------------------------------------------------------- /controller/upload/upload.go: -------------------------------------------------------------------------------- 1 | package upload 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 7 | "github.com/PeterYangs/superAdminCore/v2/response" 8 | "github.com/PeterYangs/tools" 9 | "github.com/PeterYangs/tools/file/read" 10 | "github.com/gorilla/websocket" 11 | uuid "github.com/satori/go.uuid" 12 | "net/http" 13 | "os" 14 | "time" 15 | ) 16 | 17 | var upload = websocket.Upgrader{ 18 | ReadBufferSize: 1024, 19 | WriteBufferSize: 1024, 20 | CheckOrigin: checkOrigin, 21 | } 22 | 23 | type info struct { 24 | Name string `json:"name"` 25 | Size int `json:"size"` 26 | Type string `json:"type"` 27 | Nums int `json:"nums"` 28 | } 29 | 30 | func Upload(c *contextPlus.Context) *response.Response { 31 | 32 | form, _ := c.MultipartForm() 33 | //files := form.File["upload[]"] 34 | files := form.File["file[]"] 35 | 36 | if len(files) <= 0 { 37 | 38 | return response.Resp().Api(2, "上传文件为空!", "") 39 | } 40 | 41 | path := make([]string, len(files)) 42 | 43 | for i, file := range files { 44 | 45 | //log.Println() 46 | 47 | ex, err := tools.GetExtensionName(file.Filename) 48 | 49 | if err != nil { 50 | 51 | return response.Resp().Api(2, err.Error(), "") 52 | } 53 | 54 | if !tools.InArray(tools.Explode(",", os.Getenv("ALLOW_UPLOAD_TYPE")), ex) { 55 | 56 | return response.Resp().Api(2, "该拓展类型不允许上传", "") 57 | } 58 | 59 | date := tools.Date("Ymd", time.Now().Unix()) 60 | 61 | os.MkdirAll("uploads/"+date, 0755) 62 | 63 | name := date + "/" + uuid.NewV4().String() + "." + ex 64 | 65 | // 上传文件至指定目录 66 | c.SaveUploadedFile(file, "uploads/"+name) 67 | 68 | path[i] = name 69 | } 70 | 71 | if len(path) > 1 { 72 | 73 | return response.Resp().Api(1, "success", path) 74 | } 75 | 76 | return response.Resp().Api(1, "success", path[0]) 77 | } 78 | 79 | // BigFile 大文件上传 80 | func BigFile(c *contextPlus.Context) *response.Response { 81 | 82 | conn, err := upload.Upgrade(c.Writer, c.Request, nil) 83 | 84 | if err != nil { 85 | 86 | fmt.Println(err) 87 | 88 | return response.Resp().Api(1, err.Error(), "") 89 | } 90 | 91 | go func() { 92 | 93 | defer conn.Close() 94 | 95 | tempDir := "" 96 | 97 | var tempListName []string 98 | 99 | var info info 100 | 101 | currentNum := 0 102 | 103 | //清理临时文件 104 | defer func() { 105 | 106 | for _, s := range tempListName { 107 | 108 | os.Remove("uploads/temp/" + tempDir + "/" + s) 109 | 110 | } 111 | 112 | os.Remove("uploads/temp/" + tempDir) 113 | 114 | }() 115 | 116 | for { 117 | // Read message from browser 118 | msgType, msg, err := conn.ReadMessage() 119 | 120 | if err != nil { 121 | 122 | fmt.Println(err) 123 | 124 | return 125 | } 126 | 127 | if msgType == 1 { 128 | 129 | //设置文件信息 130 | err := json.Unmarshal(msg, &info) 131 | 132 | if err != nil { 133 | 134 | fmt.Println(err) 135 | 136 | return 137 | } 138 | 139 | tempDir = uuid.NewV4().String() 140 | 141 | //生成临时文件夹 142 | os.MkdirAll("uploads/temp/"+tempDir, 0755) 143 | 144 | } 145 | 146 | if msgType == 2 { 147 | 148 | //获取文件拓展名 149 | exName, err := tools.GetExtensionName(info.Name) 150 | 151 | if err != nil { 152 | 153 | fmt.Println(err) 154 | 155 | return 156 | } 157 | 158 | //过滤非法文件 159 | if !tools.InArray(tools.Explode(",", os.Getenv("ALLOW_UPLOAD_TYPE")), exName) { 160 | 161 | conn.WriteJSON(map[string]interface{}{"code": 3, "msg": "不允许上传该类型", "data": ""}) 162 | 163 | return 164 | } 165 | 166 | //临时文件名称 167 | tempName := uuid.NewV4().String() + ".temp" 168 | 169 | f, err := os.OpenFile("uploads/temp/"+tempDir+"/"+tempName, os.O_CREATE|os.O_RDWR, 0644) 170 | 171 | if err != nil { 172 | 173 | fmt.Println(err) 174 | 175 | return 176 | } 177 | 178 | f.Write(msg) 179 | 180 | f.Close() 181 | 182 | currentNum++ 183 | 184 | //发送给客户端进度 185 | conn.WriteJSON(map[string]interface{}{"code": 2, "msg": "success", "data": currentNum}) 186 | 187 | //将临时文件写入到数组 188 | tempListName = append(tempListName, tempName) 189 | 190 | //当发送到最后一块时 191 | if currentNum == info.Nums { 192 | 193 | date := tools.Date("Ymd", time.Now().Unix()) 194 | 195 | dir := "uploads/" + date 196 | 197 | //生成上传文件夹 198 | os.MkdirAll(dir, 0775) 199 | 200 | //生成文件名称 201 | fileName := uuid.NewV4().String() + "." + exName 202 | 203 | //文件完整路径 204 | path := dir + "/" + fileName 205 | 206 | ff, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0664) 207 | 208 | if err != nil { 209 | 210 | fmt.Println(err) 211 | 212 | return 213 | } 214 | 215 | //拼接临时文件到目标文件 216 | for _, s := range tempListName { 217 | 218 | data, err := read.Open("uploads/temp/" + tempDir + "/" + s).Read() 219 | 220 | if err != nil { 221 | 222 | fmt.Println(err) 223 | 224 | ff.Close() 225 | 226 | return 227 | } 228 | 229 | ff.Write(data) 230 | 231 | } 232 | 233 | //发送文件上传完毕通知 234 | conn.WriteJSON(map[string]interface{}{"code": 1, "msg": "success", "data": map[string]interface{}{"path": date + "/" + fileName, "name": info.Name, "size": info.Size}}) 235 | 236 | //关闭文件流 237 | ff.Close() 238 | 239 | } 240 | 241 | } 242 | 243 | } 244 | 245 | }() 246 | 247 | return response.Resp().Nil() 248 | } 249 | 250 | func checkOrigin(r *http.Request) bool { 251 | 252 | return true 253 | } 254 | -------------------------------------------------------------------------------- /crontab/crontabs.go: -------------------------------------------------------------------------------- 1 | package crontab 2 | 3 | import ( 4 | crontab2 "github.com/PeterYangs/superAdminCore/v2/crontab" 5 | ) 6 | 7 | func Crontab(crontab *crontab2.Crontab) { 8 | 9 | //crontab.NewSchedule().EveryMinute().Function(func() { 10 | // 11 | // fmt.Println("每分钟") 12 | // 13 | //}) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module superadmin 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/PeterYangs/superAdminCore/v2 v2.0.2 7 | github.com/PeterYangs/tools v0.2.40 8 | github.com/dlclark/regexp2 v1.4.0 9 | github.com/gorilla/websocket v1.4.2 10 | github.com/manifoldco/promptui v0.8.0 11 | github.com/satori/go.uuid v1.2.0 12 | github.com/spf13/cast v1.5.0 13 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 14 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac 15 | gorm.io/gorm v1.21.15 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PeterYangs/gcmd2 v0.0.8 h1:FUmhqsT4qsU4dION+whwRo932b0kswo7gOPunA3jZ2Y= 2 | github.com/PeterYangs/gcmd2 v0.0.8/go.mod h1:xuuql5mIrLbEJ3iiF0qNRy5lJj1C16xR+ENylaMTv24= 3 | github.com/PeterYangs/gostage v0.0.12 h1:QZu8YpzXL/7e8mqr7H10q9R45+s4L6wC+aURI3PwXKQ= 4 | github.com/PeterYangs/gostage v0.0.12/go.mod h1:xviwGbHD8TqXjmx+zjzoXH1AZXWV0/zKbOS0K5VC8MU= 5 | github.com/PeterYangs/superAdminCore/v2 v2.0.2 h1:00vUok/IsExJfZ4/MfY9rOqjACplgnWSm2Am6LTB/H0= 6 | github.com/PeterYangs/superAdminCore/v2 v2.0.2/go.mod h1:utqwKLzLbYbf1YzJHj0U+Se3GC2765lZ8tpThNhWoVs= 7 | github.com/PeterYangs/tools v0.2.40 h1:AP3ZijnFzCJw7sfzoZEaJkHPka+Xgoj+3JO76v8oW5g= 8 | github.com/PeterYangs/tools v0.2.40/go.mod h1:DyUXMnXUCN5PUgroBmKnbr11ZC8kYqY60WdnOoMnc+o= 9 | github.com/PeterYangs/waitTree v0.0.6 h1:r9DQqKqA5jooI0mMKMQu2Q32aOMhAJqrZoG//i2sZ3o= 10 | github.com/PeterYangs/waitTree v0.0.6/go.mod h1:a8HynWCCwns3y11AI5ywTZa2vCRQcH0DUuvJeGoiIPs= 11 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 12 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 13 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 14 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 15 | github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394 h1:OYA+5W64v3OgClL+IrOD63t4i/RW7RqrAVl9LTZ9UqQ= 16 | github.com/axgle/mahonia v0.0.0-20180208002826-3358181d7394/go.mod h1:Q8n74mJTIgjX4RBBcHnJ05h//6/k6foqmgE45jTQtxg= 17 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 18 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 19 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 20 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 21 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 22 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 23 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 24 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 25 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 26 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 27 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 28 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 30 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 31 | github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= 32 | github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= 33 | github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= 34 | github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= 35 | github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= 36 | github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= 37 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 38 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 39 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 40 | github.com/gin-contrib/pprof v1.3.0 h1:G9eK6HnbkSqDZBYbzG4wrjCsA4e+cvYAHUZw6W+W9K0= 41 | github.com/gin-contrib/pprof v1.3.0/go.mod h1:waMjT1H9b179t3CxuG1cV3DHpga6ybizwfBaM5OXaB0= 42 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 43 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 44 | github.com/gin-gonic/gin v1.6.2/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 45 | github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= 46 | github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= 47 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 48 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 49 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 50 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 51 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 52 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 53 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 54 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 55 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 56 | github.com/go-redis/redis/v8 v8.11.3 h1:GCjoYp8c+yQTJfc0n69iwSiHjvuAdruxl7elnZCxgt8= 57 | github.com/go-redis/redis/v8 v8.11.3/go.mod h1:xNJ9xDG09FsIPwh3bWdk+0oDWHbtF9rPN0F/oD9XeKc= 58 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 59 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 60 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 61 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= 62 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= 63 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 64 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 65 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 66 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 67 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 68 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 69 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 70 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 71 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 72 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 73 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 74 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 75 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 76 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 77 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 78 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 79 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 80 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 81 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 82 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 83 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 84 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 85 | github.com/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U= 86 | github.com/huandu/go-clone v1.3.2/go.mod h1:bPJ9bAG8fjyAEBRFt6toaGUZcGFGL3f6g5u6yW+9W14= 87 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 88 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 89 | github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= 90 | github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 91 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 92 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 93 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 94 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 95 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= 96 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= 97 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 98 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 99 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 100 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 101 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 102 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 103 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 104 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 105 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 106 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 107 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= 108 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 109 | github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo= 110 | github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= 111 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 112 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 113 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 114 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 115 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 116 | github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= 117 | github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 118 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 119 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 120 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 121 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 122 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 123 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 124 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 125 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 126 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 127 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 128 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 129 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 130 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 131 | github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU= 132 | github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0= 133 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 134 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 135 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 136 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 137 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 138 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 139 | github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= 140 | github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= 141 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 142 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 143 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 144 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 145 | github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= 146 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 147 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 148 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 149 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 150 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 151 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 152 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 153 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 154 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 155 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= 156 | golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 157 | golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs= 158 | golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 159 | golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= 160 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 161 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 162 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 163 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 164 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 165 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 166 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 167 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= 168 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 169 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 170 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 171 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 172 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 173 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 174 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 175 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 177 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 178 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 179 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 180 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 181 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 182 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 183 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 184 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 185 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= 186 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 187 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 188 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 189 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 190 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 191 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 192 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 193 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 194 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= 195 | golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 196 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 197 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 198 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 199 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 200 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 201 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 202 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 203 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 204 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 205 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 206 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 207 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 208 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 209 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 210 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 211 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 212 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 213 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 214 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 215 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= 216 | gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= 217 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 218 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 219 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 220 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 221 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 222 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 223 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= 224 | gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= 225 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 226 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 227 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 228 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 229 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 230 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 231 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 232 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 233 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 234 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 235 | gorm.io/driver/mysql v1.1.2 h1:OofcyE2lga734MxwcCW9uB4mWNXMr50uaGRVwQL2B0M= 236 | gorm.io/driver/mysql v1.1.2/go.mod h1:4P/X9vSc3WTrhTLZ259cpFd6xKNYiSSdSZngkSBGIMM= 237 | gorm.io/gorm v1.21.12/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= 238 | gorm.io/gorm v1.21.15 h1:gAyaDoPw0lCyrSFWhBlahbUA1U4P5RViC1uIqoB+1Rk= 239 | gorm.io/gorm v1.21.15/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= 240 | -------------------------------------------------------------------------------- /logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "github.com/PeterYangs/superAdminCore/v2/core" 6 | "superadmin/artisan" 7 | "superadmin/conf" 8 | "superadmin/crontab" 9 | "superadmin/middleware" 10 | "superadmin/queue" 11 | "superadmin/routes" 12 | ) 13 | 14 | func main() { 15 | 16 | c := core.NewCore(context.Background()) 17 | 18 | //加载配置 19 | c.LoadConf(conf.Conf) 20 | 21 | c.LoadMiddleware(middleware.Load) 22 | 23 | //加载路由 24 | c.LoadRoute(routes.Routes) 25 | 26 | //加载任务调度 27 | c.LoadCrontab(crontab.Crontab) 28 | 29 | //加载消息队列 30 | c.LoadQueues(queue.Queues) 31 | 32 | //加载自定义命令 33 | c.LoadArtisan(artisan.Load) 34 | 35 | //启动 36 | c.Start() 37 | 38 | } 39 | -------------------------------------------------------------------------------- /middleware/accessLog/accessLog.go: -------------------------------------------------------------------------------- 1 | package accessLog 2 | 3 | import ( 4 | "bytes" 5 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 6 | "github.com/PeterYangs/superAdminCore/v2/queue" 7 | "superadmin/task/access" 8 | 9 | "io/ioutil" 10 | ) 11 | 12 | func AccessLog(c *contextPlus.Context) { 13 | 14 | b, _ := ioutil.ReadAll(c.Request.Body) 15 | c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(b)) 16 | 17 | var id float64 18 | 19 | if c.Session().Exist("admin") { 20 | 21 | admin, _ := c.Session().Get("admin") 22 | 23 | id = admin.(map[string]interface{})["id"].(float64) 24 | 25 | } 26 | 27 | queue.Dispatch(access.NewAccessTask(c.ClientIP(), c.Request.URL.String(), string(b), id)).Run() 28 | 29 | } 30 | -------------------------------------------------------------------------------- /middleware/authCheck/authCheck.go: -------------------------------------------------------------------------------- 1 | package authCheck 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 5 | "github.com/PeterYangs/superAdminCore/v2/database" 6 | "github.com/PeterYangs/superAdminCore/v2/response" 7 | "github.com/spf13/cast" 8 | "superadmin/model" 9 | ) 10 | 11 | func AuthCheck(c *contextPlus.Context) { 12 | 13 | //fmt.Println(c.Handler.Tag,"----------") 14 | 15 | //跳过权限检查 16 | if c.Handler.Tag == "skip_auth" { 17 | 18 | return 19 | } 20 | 21 | admin_, err := c.Session().Get("admin") 22 | 23 | if err != nil { 24 | 25 | c.JSON(200, response.Resp().Api(11, "session异常", "").GetData()) 26 | 27 | c.Abort() 28 | 29 | return 30 | } 31 | 32 | admin := admin_.(map[string]interface{}) 33 | 34 | adminId := admin["id"] 35 | 36 | var info model.Admin 37 | 38 | database.GetDb().Model(&model.Admin{}).Where("id = ?", adminId).Preload("RoleDetail.Role").First(&info) 39 | 40 | //超级管理员 41 | if info.RoleDetail.RoleId == 0 { 42 | 43 | return 44 | } 45 | 46 | rules := make([]*model.Rule, 0) 47 | 48 | var str []string 49 | 50 | for _, rule := range info.RoleDetail.Role.Rules { 51 | 52 | str = append(str, cast.ToString(rule)) 53 | } 54 | 55 | database.GetDb().Model(&model.Rule{}).Where("id in ? ", str).Find(&rules) 56 | 57 | url := c.FullPath() 58 | 59 | for _, rule := range rules { 60 | 61 | if url == rule.Rule { 62 | 63 | return 64 | } 65 | } 66 | 67 | c.JSON(200, response.Resp().Api(51, "你没有这个权限", "").GetData()) 68 | 69 | c.Abort() 70 | 71 | } 72 | -------------------------------------------------------------------------------- /middleware/global.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 5 | "github.com/PeterYangs/superAdminCore/v2/middleware/session" 6 | "superadmin/middleware/accessLog" 7 | ) 8 | 9 | func Load() []contextPlus.HandlerFunc { 10 | 11 | return []contextPlus.HandlerFunc{ 12 | 13 | session.StartSession, 14 | accessLog.AccessLog, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /middleware/loginCheck/loginCheck.go: -------------------------------------------------------------------------------- 1 | package loginCheck 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 5 | "github.com/PeterYangs/superAdminCore/v2/response" 6 | ) 7 | 8 | func LoginCheck(c *contextPlus.Context) { 9 | 10 | if !c.Session().Exist("admin") { 11 | 12 | c.JSON(200, response.Resp().Api(10, "请登录", "").GetData()) 13 | 14 | c.Abort() 15 | 16 | return 17 | } 18 | 19 | //fmt.Println(c.FullPath()) 20 | 21 | } 22 | -------------------------------------------------------------------------------- /middleware/loginLimiter/loginLimiter.go: -------------------------------------------------------------------------------- 1 | package loginLimiter 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/component/limiter" 5 | "github.com/PeterYangs/superAdminCore/v2/contextPlus" 6 | "golang.org/x/time/rate" 7 | "time" 8 | ) 9 | 10 | func LoginLimiter(c *contextPlus.Context) { 11 | 12 | if !limiter.NewLimiter(rate.Every(1*time.Second), 10, c.ClientIP()).Allow() { 13 | 14 | c.String(500, "访问频率过高") 15 | 16 | c.Abort() 17 | 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /migrations/bin/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /migrations/migrate_2021_06_28_111049_create_admin_table/migrate.go: -------------------------------------------------------------------------------- 1 | package migrate_2021_06_28_111049_create_admin_table 2 | 3 | import "github.com/PeterYangs/superAdminCore/v2/migrate" 4 | 5 | func Up() { 6 | 7 | migrate.Create("admin", func(createMigrate *migrate.Migrate) { 8 | 9 | createMigrate.Name = "migrate_2021_06_28_111049_create_admin_table" 10 | 11 | createMigrate.BigIncrements("id") 12 | 13 | createMigrate.Timestamp("created_at").Nullable() 14 | 15 | createMigrate.Timestamp("updated_at").Nullable() 16 | 17 | createMigrate.String("username", 255).Unique().Comment("用户名") 18 | 19 | createMigrate.String("password", 255).Comment("密码") 20 | 21 | createMigrate.String("email", 255).Unique().Comment("邮箱") 22 | 23 | createMigrate.Integer("status").Default(1).Comment("状态") 24 | 25 | createMigrate.Timestamp("deleted_at").Nullable() 26 | 27 | }) 28 | 29 | } 30 | 31 | func Down() { 32 | 33 | migrate.DropIfExists("admin") 34 | 35 | } 36 | -------------------------------------------------------------------------------- /migrations/migrate_2021_07_31_152020_create_rule_table/migrate.go: -------------------------------------------------------------------------------- 1 | package migrate_2021_07_31_152020_create_rule_table 2 | 3 | import "github.com/PeterYangs/superAdminCore/v2/migrate" 4 | 5 | func Up() { 6 | 7 | migrate.Create("rule", func(createMigrate *migrate.Migrate) { 8 | 9 | createMigrate.Name = "migrate_2021_07_31_152020_create_rule_table" 10 | 11 | createMigrate.BigIncrements("id") 12 | 13 | createMigrate.Timestamp("created_at").Nullable() 14 | 15 | createMigrate.Timestamp("updated_at").Nullable() 16 | 17 | createMigrate.Timestamp("deleted_at").Nullable() 18 | 19 | createMigrate.String("title", 255).Comment("规则描述") 20 | 21 | createMigrate.String("rule", 255).Comment("规则") 22 | 23 | createMigrate.String("group_name", 255).Comment("分组名称") 24 | 25 | //createMigrate.String("test", 255).Default(migrate.Null).Comment("测试") 26 | 27 | }) 28 | 29 | } 30 | 31 | func Down() { 32 | 33 | migrate.DropIfExists("rule") 34 | 35 | } 36 | -------------------------------------------------------------------------------- /migrations/migrate_2021_08_04_112919_create_role_table/migrate.go: -------------------------------------------------------------------------------- 1 | package migrate_2021_08_04_112919_create_role_table 2 | 3 | import "github.com/PeterYangs/superAdminCore/v2/migrate" 4 | 5 | func Up() { 6 | 7 | migrate.Create("role", func(createMigrate *migrate.Migrate) { 8 | 9 | createMigrate.Name = "migrate_2021_08_04_112919_create_role_table" 10 | 11 | createMigrate.BigIncrements("id") 12 | 13 | createMigrate.Timestamp("created_at").Nullable() 14 | 15 | createMigrate.Timestamp("updated_at").Nullable() 16 | 17 | // createMigrate.Timestamp("deleted_at").Nullable() 18 | 19 | createMigrate.String("title", 255).Comment("描述") 20 | 21 | createMigrate.String("rules", 1000).Comment("规则id") 22 | 23 | }) 24 | 25 | } 26 | 27 | func Down() { 28 | 29 | migrate.DropIfExists("role") 30 | 31 | } 32 | -------------------------------------------------------------------------------- /migrations/migrate_2021_08_04_143652_create_role_detail_table/migrate.go: -------------------------------------------------------------------------------- 1 | package migrate_2021_08_04_143652_create_role_detail_table 2 | 3 | import "github.com/PeterYangs/superAdminCore/v2/migrate" 4 | 5 | func Up() { 6 | 7 | migrate.Create("role_detail", func(createMigrate *migrate.Migrate) { 8 | 9 | createMigrate.Name = "migrate_2021_08_04_143652_create_role_detail_table" 10 | 11 | createMigrate.BigIncrements("id") 12 | 13 | createMigrate.Timestamp("created_at").Nullable() 14 | 15 | createMigrate.Timestamp("updated_at").Nullable() 16 | 17 | // createMigrate.Timestamp("deleted_at").Nullable() 18 | 19 | createMigrate.Integer("admin_id").Unsigned().Comment("管理员id") 20 | 21 | createMigrate.Integer("role_id").Unsigned().Comment("角色id") 22 | 23 | createMigrate.Unique("admin_id", "role_id") 24 | 25 | }) 26 | 27 | } 28 | 29 | func Down() { 30 | 31 | migrate.DropIfExists("role_detail") 32 | 33 | } 34 | -------------------------------------------------------------------------------- /migrations/migrate_2021_08_05_174801_update_role_detail_table/migrate.go: -------------------------------------------------------------------------------- 1 | package migrate_2021_08_05_174801_update_role_detail_table 2 | 3 | import "github.com/PeterYangs/superAdminCore/v2/migrate" 4 | 5 | func Up() { 6 | 7 | migrate.Table("role_detail", func(createMigrate *migrate.Migrate) { 8 | 9 | createMigrate.Name = "migrate_2021_08_05_174801_update_role_detail_table" 10 | 11 | createMigrate.Integer("role_id").Comment("角色id,-1是超级管理员").Change() 12 | 13 | }) 14 | 15 | } 16 | 17 | func Down() { 18 | 19 | } 20 | -------------------------------------------------------------------------------- /migrations/migrate_2021_08_07_112213_update_rule_table/migrate.go: -------------------------------------------------------------------------------- 1 | package migrate_2021_08_07_112213_update_rule_table 2 | 3 | import "github.com/PeterYangs/superAdminCore/v2/migrate" 4 | 5 | func Up() { 6 | 7 | migrate.Table("rule", func(createMigrate *migrate.Migrate) { 8 | 9 | createMigrate.Name = "migrate_2021_08_07_112213_update_rule_table" 10 | 11 | createMigrate.Unique("rule") 12 | }) 13 | 14 | } 15 | 16 | func Down() { 17 | 18 | } 19 | -------------------------------------------------------------------------------- /migrations/migrate_2021_08_07_150304_create_menu_table/migrate.go: -------------------------------------------------------------------------------- 1 | package migrate_2021_08_07_150304_create_menu_table 2 | 3 | import "github.com/PeterYangs/superAdminCore/v2/migrate" 4 | 5 | func Up() { 6 | 7 | migrate.Create("menu", func(createMigrate *migrate.Migrate) { 8 | 9 | createMigrate.Name = "migrate_2021_08_07_150304_create_menu_table" 10 | 11 | createMigrate.BigIncrements("id") 12 | 13 | createMigrate.Timestamp("created_at").Nullable() 14 | 15 | createMigrate.Timestamp("updated_at").Nullable() 16 | 17 | // createMigrate.Timestamp("deleted_at").Nullable() 18 | 19 | createMigrate.Integer("pid").Unsigned().Comment("父级菜单id") 20 | 21 | createMigrate.String("title", 255).Comment("菜单名称") 22 | 23 | createMigrate.String("path", 255).Comment("路径") 24 | 25 | createMigrate.Integer("sort").Unsigned().Default(100).Comment("排序,越小越靠前") 26 | 27 | }) 28 | 29 | } 30 | 31 | func Down() { 32 | 33 | migrate.DropIfExists("menu") 34 | 35 | } 36 | -------------------------------------------------------------------------------- /migrations/migrate_2021_08_09_153916_create_category_table/migrate.go: -------------------------------------------------------------------------------- 1 | package migrate_2021_08_09_153916_create_category_table 2 | 3 | import "github.com/PeterYangs/superAdminCore/v2/migrate" 4 | 5 | func Up() { 6 | 7 | migrate.Create("category", func(createMigrate *migrate.Migrate) { 8 | 9 | createMigrate.Name = "migrate_2021_08_09_153916_create_category_table" 10 | 11 | createMigrate.BigIncrements("id") 12 | 13 | createMigrate.Timestamp("created_at").Nullable() 14 | 15 | createMigrate.Timestamp("updated_at").Nullable() 16 | 17 | // createMigrate.Timestamp("deleted_at").Nullable() 18 | 19 | createMigrate.Integer("pid").Unsigned().Comment("父类id") 20 | 21 | createMigrate.Integer("lv").Unsigned().Default(1).Comment("层级") 22 | 23 | createMigrate.String("title", 255).Comment("标题") 24 | 25 | createMigrate.String("img", 255).Default("").Comment("图片") 26 | 27 | createMigrate.Integer("sort").Comment("排序,越小越靠前") 28 | 29 | createMigrate.String("path", 255).Comment("层级路径,逗号分隔") 30 | 31 | createMigrate.Unique("title", "pid") 32 | 33 | }) 34 | 35 | } 36 | 37 | func Down() { 38 | 39 | migrate.DropIfExists("category") 40 | 41 | } 42 | -------------------------------------------------------------------------------- /migrations/migrate_2021_08_17_165541_update_menu_table/migrate.go: -------------------------------------------------------------------------------- 1 | package migrate_2021_08_17_165541_update_menu_table 2 | 3 | import "github.com/PeterYangs/superAdminCore/v2/migrate" 4 | 5 | func Up() { 6 | 7 | migrate.Table("menu", func(createMigrate *migrate.Migrate) { 8 | 9 | createMigrate.Name = "migrate_2021_08_17_165541_update_menu_table" 10 | 11 | createMigrate.String("rule", 255).Default("").Comment("对应的权限,用于显示和隐藏不同角色的菜单显示") 12 | 13 | }) 14 | 15 | } 16 | 17 | func Down() { 18 | 19 | migrate.Table("menu", func(createMigrate *migrate.Migrate) { 20 | 21 | createMigrate.DropColumn("rule") 22 | 23 | }) 24 | 25 | } 26 | -------------------------------------------------------------------------------- /migrations/migrate_2021_08_18_143020_create_access_table/migrate.go: -------------------------------------------------------------------------------- 1 | package migrate_2021_08_18_143020_create_access_table 2 | 3 | import "github.com/PeterYangs/superAdminCore/v2/migrate" 4 | 5 | func Up() { 6 | 7 | migrate.Create("access", func(createMigrate *migrate.Migrate) { 8 | 9 | createMigrate.Name = "migrate_2021_08_18_143020_create_access_table" 10 | 11 | createMigrate.BigIncrements("id") 12 | 13 | createMigrate.Timestamp("created_at").Nullable() 14 | 15 | createMigrate.Timestamp("updated_at").Nullable() 16 | 17 | createMigrate.String("ip", 255).Comment("ip") 18 | 19 | createMigrate.Text("url").Comment("请求路径") 20 | 21 | createMigrate.Text("params").Comment("参数") 22 | 23 | // createMigrate.Timestamp("deleted_at").Nullable() 24 | 25 | }) 26 | 27 | } 28 | 29 | func Down() { 30 | 31 | migrate.DropIfExists("access") 32 | 33 | } 34 | -------------------------------------------------------------------------------- /migrations/migrate_2021_08_18_155238_update_access_table/migrate.go: -------------------------------------------------------------------------------- 1 | package migrate_2021_08_18_155238_update_access_table 2 | 3 | import "github.com/PeterYangs/superAdminCore/v2/migrate" 4 | 5 | func Up() { 6 | 7 | migrate.Table("access", func(createMigrate *migrate.Migrate) { 8 | 9 | createMigrate.Name = "migrate_2021_08_18_155238_update_access_table" 10 | 11 | createMigrate.Integer("admin_id").Unsigned().Default(0).Comment("管理员id") 12 | 13 | }) 14 | 15 | } 16 | 17 | func Down() { 18 | 19 | migrate.Table("access", func(createMigrate *migrate.Migrate) { 20 | 21 | }) 22 | 23 | } 24 | -------------------------------------------------------------------------------- /migrations/migrate_2021_09_09_094754_create_file_table/migrate.go: -------------------------------------------------------------------------------- 1 | package migrate_2021_09_09_094754_create_file_table 2 | 3 | import "github.com/PeterYangs/superAdminCore/v2/migrate" 4 | 5 | func Up() { 6 | 7 | migrate.Create("file", func(createMigrate *migrate.Migrate) { 8 | 9 | createMigrate.Name = "migrate_2021_09_09_094754_create_file_table" 10 | 11 | createMigrate.BigIncrements("id") 12 | 13 | createMigrate.Timestamp("created_at").Nullable() 14 | 15 | createMigrate.Timestamp("updated_at").Nullable() 16 | 17 | // createMigrate.Timestamp("deleted_at").Nullable() 18 | 19 | createMigrate.String("path", 255).Comment("文件地址") 20 | 21 | createMigrate.String("name", 255).Comment("文件名称") 22 | 23 | createMigrate.BigInteger("size").Unsigned().Comment("文件大小,单位字节") 24 | 25 | createMigrate.Integer("admin_id").Unsigned().Comment("上传管理员") 26 | 27 | }) 28 | 29 | } 30 | 31 | func Down() { 32 | 33 | migrate.DropIfExists("file") 34 | 35 | } 36 | -------------------------------------------------------------------------------- /model/Access.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Access struct { 4 | Base 5 | Id uint `json:"id" fillable:"Ip,Url,Params,AdminId"` 6 | Ip string `json:"ip"` 7 | Url string `json:"url"` 8 | Params string `json:"params"` 9 | AdminId float64 `json:"admin_id"` 10 | } 11 | -------------------------------------------------------------------------------- /model/Admin.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "superadmin/types" 6 | ) 7 | 8 | type Admin struct { 9 | Id uint `json:"id" fillable:"Username,Password,Email,Status"` 10 | CreatedAt types.Time `json:"created_at"` 11 | UpdatedAt types.Time `json:"updated_at"` 12 | Username string `json:"username" form:"username"` 13 | Password string `json:"-" form:"password" ` 14 | Email string `json:"email" form:"email" ` 15 | Status int `json:"status" form:"status" gorm:"default:1"` 16 | DeletedAt gorm.DeletedAt `json:"deleted_at"` 17 | RoleDetail RoleDetail `json:"role_detail" gorm:"foreignKey:admin_id"` 18 | } 19 | -------------------------------------------------------------------------------- /model/Base.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "superadmin/types" 4 | 5 | type Base struct { 6 | CreatedAt types.Time `json:"created_at"` 7 | UpdatedAt types.Time `json:"updated_at"` 8 | } 9 | -------------------------------------------------------------------------------- /model/Category.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Category struct { 4 | Base 5 | Id uint `json:"id" fillable:"Pid,Lv,Title,Img,Sort,Path"` 6 | Pid int `json:"pid"` 7 | Lv int `json:"lv"` 8 | Title string `json:"title"` 9 | Img string `json:"img"` 10 | Sort int `json:"sort"` 11 | Path string `json:"path"` 12 | } 13 | 14 | //func (c *Category)BeforeCreate(tx *gorm.DB) (err error) { 15 | // 16 | // 17 | // 18 | // 19 | // return 20 | //} 21 | -------------------------------------------------------------------------------- /model/File.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type File struct { 4 | Base 5 | Id uint `json:"id" fillable:"Path,Name,Size,AdminId"` 6 | Path string `json:"path"` 7 | Name string `json:"name"` 8 | Size int64 `json:"size"` 9 | AdminId int `json:"admin_id"` 10 | Admin Admin `json:"admin"` 11 | } 12 | -------------------------------------------------------------------------------- /model/Menu.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Menu struct { 4 | Base 5 | Id uint `json:"id" fillable:"Pid,Title,Path,Sort,Rule"` 6 | Pid int `json:"pid"` 7 | Title string `json:"title"` 8 | Path string `json:"path"` 9 | Sort int `json:"sort"` 10 | Rule string `json:"rule"` 11 | } 12 | -------------------------------------------------------------------------------- /model/Role.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "superadmin/types" 4 | 5 | type Role struct { 6 | Base 7 | Id uint `json:"id"` 8 | Title string `json:"title"` 9 | Rules types.CommaArray `json:"rules"` 10 | } 11 | -------------------------------------------------------------------------------- /model/RoleDetail.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type RoleDetail struct { 4 | Base 5 | Id uint `json:"id" fillable:"AdminId,RoleId"` 6 | AdminId int `json:"admin_id"` 7 | RoleId int `json:"role_id"` 8 | Role Role `json:"role"` 9 | } 10 | -------------------------------------------------------------------------------- /model/Rule.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Rule struct { 4 | Base 5 | Id uint `json:"id" fillable:"Title,Rule,GroupName"` 6 | Title string `json:"title"` 7 | Rule string `json:"rule"` 8 | GroupName string `json:"group_name"` 9 | } 10 | -------------------------------------------------------------------------------- /queue/queue.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/queue/template" 5 | "superadmin/task/access" 6 | "superadmin/task/app" 7 | //namespace 8 | ) 9 | 10 | var Queues = map[string]template.Task{ 11 | "access": &access.TaskAccess{Parameters: &access.Parameter{}}, 12 | "app": &app.AppTask{Parameters: &app.Parameter{}}, 13 | //taskRegister 14 | } 15 | -------------------------------------------------------------------------------- /routes/routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/route" 5 | "superadmin/controller" 6 | "superadmin/controller/access" 7 | "superadmin/controller/admin" 8 | "superadmin/controller/broadcast" 9 | "superadmin/controller/captcha" 10 | "superadmin/controller/category" 11 | "superadmin/controller/file" 12 | "superadmin/controller/login" 13 | "superadmin/controller/menu" 14 | "superadmin/controller/queue" 15 | "superadmin/controller/role" 16 | "superadmin/controller/rule" 17 | "superadmin/controller/test" 18 | "superadmin/controller/upload" 19 | "superadmin/middleware/authCheck" 20 | "superadmin/middleware/loginCheck" 21 | "superadmin/middleware/loginLimiter" 22 | ) 23 | 24 | func Routes(_r route.Group) { 25 | 26 | _r.Registered(route.GET, "/index", controller.Index).Bind() 27 | 28 | _r.Group("/login", func(_login route.Group) { 29 | 30 | _login.Registered(route.POST, "/login", login.Login, loginLimiter.LoginLimiter).Bind() 31 | 32 | _login.Registered(route.POST, "/logout", login.Logout).Bind() 33 | 34 | }) 35 | 36 | _r.Group("/admin", func(_admin route.Group) { 37 | 38 | _admin.Group("/rule", func(_rule route.Group) { 39 | 40 | _rule.Registered(route.POST, "/update", rule.Update).Bind() 41 | _rule.Registered(route.GET, "/list", rule.List).Bind() 42 | _rule.Registered(route.GET, "/detail/:id", rule.Detail).Bind() 43 | _rule.Registered(route.POST, "/destroy/:id", rule.Destroy).Bind() 44 | 45 | }) 46 | 47 | _admin.Group("/role", func(_role route.Group) { 48 | 49 | _role.Registered(route.GET, "/GetRuleList", role.GetRuleList).SetTag("skip_auth").Bind() 50 | _role.Registered(route.POST, "/update", role.Update).Bind() 51 | _role.Registered(route.GET, "/list", role.List).Bind() 52 | _role.Registered(route.GET, "/detail/:id", role.Detail).Bind() 53 | _role.Registered(route.POST, "/destroy/:id", role.Destroy).Bind() 54 | 55 | }) 56 | 57 | _admin.Group("/admin", func(_admins route.Group) { 58 | 59 | _admins.Registered(route.GET, "/getRoleList", admin.GetRoleList).Bind() 60 | _admins.Registered(route.POST, "/registered", login.Registered).Bind() 61 | _admins.Registered(route.POST, "/list", admin.List).Bind() 62 | _admins.Registered(route.POST, "/detail/:id", admin.Detail).Bind() 63 | _admins.Registered(route.GET, "/info", admin.Info).SetTag("skip_auth").Bind() 64 | _admins.Registered(route.GET, "/SearchRule", admin.SearchRule).SetTag("skip_auth").Bind() 65 | _admins.Registered(route.POST, "/destroy/:id", admin.Destroy).Bind() 66 | _admins.Registered(route.POST, "/getMyMenu", admin.GetMyMenu).SetTag("skip_auth").Bind() 67 | _admins.Registered(route.POST, "/roleList", admin.RoleList).Bind() 68 | _admins.Registered(route.GET, "/getAllRule", admin.GetAllRule).SetTag("skip_auth").Bind() 69 | 70 | }) 71 | 72 | _admin.Group("/menu", func(_menu route.Group) { 73 | 74 | _menu.Registered(route.GET, "/getFatherMenu", menu.GetFatherMenu).SetTag("skip_auth").Bind() 75 | _menu.Registered(route.POST, "/update", menu.Update).Bind() 76 | _menu.Registered(route.POST, "/list", menu.List).Bind() 77 | _menu.Registered(route.GET, "/detail/:id", menu.Detail).Bind() 78 | 79 | }) 80 | 81 | _admin.Group("/category", func(_category route.Group) { 82 | 83 | _category.Registered(route.GET, "/list", category.List).Bind() 84 | _category.Registered(route.POST, "/update", category.Update).Bind() 85 | 86 | }) 87 | 88 | _admin.Group("/upload", func(_upload route.Group) { 89 | 90 | _upload.Registered(route.POST, "/upload", upload.Upload).Bind() 91 | _upload.Registered(route.ANY, "/big_file", upload.BigFile).Bind() 92 | }) 93 | 94 | _admin.Group("/queue", func(_queue route.Group) { 95 | 96 | _queue.Registered(route.POST, "/list", queue.List).Bind() 97 | _queue.Registered(route.POST, "/delay_list", queue.DelayList).Bind() 98 | 99 | }) 100 | 101 | _admin.Group("/access", func(_access route.Group) { 102 | 103 | _access.Registered(route.POST, "/list", access.List).Bind() 104 | 105 | }) 106 | 107 | _admin.Group("/file", func(_file route.Group) { 108 | 109 | _file.Registered(route.POST, "/update", file.Update).Bind() 110 | _file.Registered(route.POST, "/list", file.List).Bind() 111 | _file.Registered(route.POST, "/destroy/:id", file.Destroy).Bind() 112 | 113 | }) 114 | 115 | _admin.Group("/broadcast", func(_broadcast route.Group) { 116 | 117 | _broadcast.Registered(route.ANY, "/broadcast", broadcast.Broadcast).SetTag("skip_auth").Bind() 118 | _broadcast.Registered(route.POST, "/groupMessage", broadcast.GroupMessage).SetTag("skip_auth").Bind() 119 | 120 | }) 121 | 122 | }, loginCheck.LoginCheck, authCheck.AuthCheck) 123 | 124 | _r.Group("/captcha", func(g route.Group) { 125 | 126 | g.Registered(route.GET, "/captcha", captcha.Captcha).Bind() 127 | }) 128 | 129 | _r.Group("/test", func(_test route.Group) { 130 | 131 | _test.Registered(route.GET, "/cache", test.Cache).Bind() 132 | 133 | }) 134 | 135 | } 136 | -------------------------------------------------------------------------------- /search/search.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | type Field struct { 8 | Key string 9 | Condition string 10 | } 11 | 12 | type search struct { 13 | } 14 | 15 | func NewSearch(tx *gorm.DB, maps map[string]interface{}, fields []Field) *search { 16 | 17 | if maps == nil || len(maps) <= 0 { 18 | 19 | return &search{} 20 | } 21 | 22 | for _, field := range fields { 23 | 24 | if maps[field.Key] != "" { 25 | 26 | switch field.Condition { 27 | 28 | case "like": 29 | tx.Where(field.Key+" "+field.Condition+"?", "%"+maps[field.Key].(string)+"%") 30 | default: 31 | tx.Where(field.Key+" "+field.Condition+"?", maps[field.Key]) 32 | } 33 | 34 | } 35 | 36 | } 37 | 38 | return &search{} 39 | 40 | } 41 | -------------------------------------------------------------------------------- /storage/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /task/access/access.go: -------------------------------------------------------------------------------- 1 | package access 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/database" 5 | "github.com/PeterYangs/superAdminCore/v2/queue/task" 6 | "superadmin/model" 7 | ) 8 | 9 | type TaskAccess struct { 10 | task.BaseTask 11 | Parameters *Parameter 12 | } 13 | 14 | type Parameter struct { 15 | task.Parameter 16 | Ip string 17 | Url string 18 | Params string 19 | AdminId float64 20 | } 21 | 22 | func NewAccessTask(ip string, url string, params string, adminId float64) *TaskAccess { 23 | 24 | return &TaskAccess{ 25 | 26 | BaseTask: task.BaseTask{ 27 | TaskName: "access", 28 | }, 29 | Parameters: &Parameter{ 30 | Ip: ip, 31 | Url: url, 32 | Params: params, 33 | AdminId: adminId, 34 | }, 35 | } 36 | } 37 | 38 | func (t *TaskAccess) Run() error { 39 | 40 | database.GetDb().Create(&model.Access{ 41 | Ip: t.Parameters.Ip, 42 | Url: t.Parameters.Url, 43 | Params: t.Parameters.Params, 44 | AdminId: t.Parameters.AdminId, 45 | }) 46 | 47 | return nil 48 | } 49 | 50 | func (t *TaskAccess) BindParameters(p map[string]interface{}) { 51 | 52 | t.BaseTask.Bind(t.Parameters, p) 53 | 54 | } 55 | -------------------------------------------------------------------------------- /task/app/app.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "github.com/PeterYangs/superAdminCore/v2/queue/task" 5 | ) 6 | 7 | type AppTask struct { 8 | task.BaseTask 9 | Parameters *Parameter 10 | } 11 | 12 | type Parameter struct { 13 | task.Parameter 14 | } 15 | 16 | func NewAppTask() *AppTask { 17 | 18 | return &AppTask{ 19 | 20 | BaseTask: task.BaseTask{ 21 | TaskName: "app", 22 | }, 23 | Parameters: &Parameter{}, 24 | } 25 | } 26 | 27 | func (t *AppTask) Run() error { 28 | 29 | return nil 30 | } 31 | 32 | func (t *AppTask) BindParameters(p map[string]interface{}) { 33 | 34 | t.BaseTask.Bind(t.Parameters, p) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /types/CommaArray.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "github.com/PeterYangs/tools" 6 | "github.com/spf13/cast" 7 | ) 8 | 9 | type CommaArray []int 10 | 11 | //获取器(读取结果) 12 | func (m *CommaArray) Scan(value interface{}) error { 13 | 14 | bytes, ok := value.([]byte) 15 | 16 | if !ok { 17 | //return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value)) 18 | } 19 | 20 | //err := json.Unmarshal(bytes, &r) 21 | 22 | s := tools.Explode(",", string(bytes)) 23 | 24 | r := make([]int, len(s)) 25 | 26 | for i, s2 := range s { 27 | 28 | r[i] = cast.ToInt(s2) 29 | 30 | } 31 | 32 | //if err != nil { 33 | // 34 | // *m = []interface{}{} 35 | // 36 | // return nil 37 | //} 38 | 39 | *m = r 40 | 41 | return nil 42 | 43 | } 44 | 45 | //写入数据库(修改器) 46 | func (m CommaArray) Value() (driver.Value, error) { 47 | 48 | r := make([]string, len(m)) 49 | 50 | for i, i2 := range m { 51 | 52 | r[i] = cast.ToString(i2) 53 | } 54 | 55 | return tools.Join(",", r), nil 56 | 57 | } 58 | -------------------------------------------------------------------------------- /types/JsonArray.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | type JsonArray []interface{} 10 | 11 | func (m *JsonArray) Scan(value interface{}) error { 12 | 13 | bytes, ok := value.([]byte) 14 | 15 | if !ok { 16 | //return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value)) 17 | } 18 | 19 | var r []interface{} 20 | 21 | err := json.Unmarshal(bytes, &r) 22 | 23 | fmt.Println() 24 | 25 | if err != nil { 26 | 27 | *m = []interface{}{} 28 | 29 | return nil 30 | } 31 | 32 | *m = r 33 | 34 | return nil 35 | 36 | } 37 | 38 | //将插入内容转成数据库(修改器) 39 | func (m JsonArray) Value() (driver.Value, error) { 40 | 41 | return json.Marshal(m) 42 | 43 | } 44 | -------------------------------------------------------------------------------- /types/JsonMap.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | type JsonMap map[string]interface{} 10 | 11 | //将数据库内容转成对应类型(获取器) 12 | func (m *JsonMap) Scan(value interface{}) error { 13 | 14 | bytes, ok := value.([]byte) 15 | 16 | if !ok { 17 | //return errors.New(fmt.Sprint("Failed to unmarshal JSONB value:", value)) 18 | } 19 | 20 | var r map[string]interface{} 21 | 22 | err := json.Unmarshal(bytes, &r) 23 | 24 | fmt.Println() 25 | 26 | if err != nil { 27 | 28 | *m = map[string]interface{}{} 29 | 30 | return nil 31 | } 32 | 33 | *m = r 34 | 35 | return nil 36 | 37 | } 38 | 39 | //将插入内容转成数据库(修改器) 40 | func (m JsonMap) Value() (driver.Value, error) { 41 | 42 | return json.Marshal(m) 43 | 44 | } 45 | -------------------------------------------------------------------------------- /types/Time.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "database/sql/driver" 5 | "time" 6 | ) 7 | 8 | type Time time.Time 9 | 10 | func (t Time) Value() (driver.Value, error) { 11 | 12 | return time.Time(t), nil 13 | } 14 | 15 | func (t Time) MarshalJSON() ([]byte, error) { 16 | tTime := time.Time(t) 17 | tStr := tTime.Format("2006-01-02 15:04:05") // 设置格式 18 | 19 | if tStr == "0001-01-01 00:00:00" { 20 | 21 | return []byte("\"" + " " + "\""), nil 22 | } 23 | 24 | //// 注意 json 字符串风格要求 25 | return []byte("\"" + tStr + "\""), nil 26 | 27 | } 28 | -------------------------------------------------------------------------------- /uploads/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /util/online/online.go: -------------------------------------------------------------------------------- 1 | package online 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/gorilla/websocket" 7 | uuid "github.com/satori/go.uuid" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | //所有连接处理结构体 13 | type online struct { 14 | total int64 //在线连接数 15 | list sync.Map //所有连接存放 16 | lock sync.Mutex 17 | } 18 | 19 | var Online *online 20 | 21 | var once sync.Once 22 | 23 | // Conn 单个连接结构体 24 | type Conn struct { 25 | id string //连接id 26 | conn *websocket.Conn //websocket连接对象 27 | lastReply time.Time //上一次回复时间 28 | adminId int //账号id 29 | lock sync.Mutex 30 | } 31 | 32 | type Message struct { 33 | Code int `json:"code"` 34 | Types string `json:"types"` 35 | Message string `json:"message"` 36 | Data interface{} `json:"data"` 37 | } 38 | 39 | func NewOnline() *online { 40 | 41 | once.Do(func() { 42 | 43 | Online = &online{ 44 | total: 0, 45 | list: sync.Map{}, 46 | lock: sync.Mutex{}, 47 | } 48 | 49 | go Online.checkTime() 50 | 51 | go Online.pushTotal() 52 | }) 53 | 54 | return Online 55 | } 56 | 57 | // Add 添加一个连接 58 | func (o *online) Add(c *Conn) string { 59 | 60 | o.total++ 61 | 62 | o.list.Store(c.id, c) 63 | 64 | return c.id 65 | 66 | } 67 | 68 | // Del 删除一个连接 69 | func (o *online) Del(id string) { 70 | 71 | o.total-- 72 | 73 | o.list.Delete(id) 74 | 75 | } 76 | 77 | // Total 当前在线人数 78 | func (o *online) Total() int64 { 79 | 80 | return o.total 81 | } 82 | 83 | // GetConnById 根据id获取连接 84 | func (o *online) GetConnById(id string) (bool, *Conn) { 85 | 86 | c, ok := o.list.Load(id) 87 | 88 | if ok { 89 | 90 | con := c.(*Conn) 91 | 92 | return ok, con 93 | 94 | } 95 | 96 | return ok, nil 97 | } 98 | 99 | // SendAllMessage 群发 100 | func (o *online) SendAllMessage(message Message) { 101 | 102 | o.list.Range(func(key, value interface{}) bool { 103 | 104 | con := value.(*Conn) 105 | 106 | con.SendMessage(message) 107 | 108 | return true 109 | }) 110 | } 111 | 112 | //心跳检测 113 | func (o *online) checkTime() { 114 | 115 | for { 116 | 117 | time.Sleep(30 * time.Second) 118 | 119 | o.list.Range(func(key, value interface{}) bool { 120 | 121 | con := value.(*Conn) 122 | 123 | if time.Now().Sub(con.lastReply).Seconds() > 60 { 124 | 125 | fmt.Println("超时") 126 | 127 | con.conn.Close() 128 | } 129 | 130 | return true 131 | }) 132 | 133 | } 134 | 135 | } 136 | 137 | //群发在线连接数 138 | func (o *online) pushTotal() { 139 | 140 | for { 141 | 142 | time.Sleep(15 * time.Second) 143 | 144 | o.list.Range(func(key, value interface{}) bool { 145 | 146 | con := value.(*Conn) 147 | 148 | con.SendJson(1, "total", "success", o.total) 149 | 150 | return true 151 | }) 152 | 153 | } 154 | 155 | } 156 | 157 | // GetTotal 获取在线人数 158 | func (o *online) GetTotal() int64 { 159 | 160 | return o.total 161 | } 162 | 163 | //------------------------------------------------------------------------------------ 164 | 165 | func NewConn(conn *websocket.Conn) *Conn { 166 | 167 | return &Conn{id: uuid.NewV4().String(), conn: conn, lastReply: time.Now(), lock: sync.Mutex{}} 168 | } 169 | 170 | // SetAdminId 设置账号id 171 | func (c *Conn) SetAdminId(adminId int) { 172 | 173 | c.adminId = adminId 174 | 175 | } 176 | 177 | // SendMessage 结构体式 178 | func (c *Conn) SendMessage(message Message) error { 179 | 180 | c.lock.Lock() 181 | 182 | defer c.lock.Unlock() 183 | 184 | return c.conn.WriteJSON(message) 185 | } 186 | 187 | // SendJson 函数式 188 | func (c *Conn) SendJson(code int, types string, message string, data interface{}) error { 189 | 190 | c.lock.Lock() 191 | 192 | defer c.lock.Unlock() 193 | 194 | return c.conn.WriteJSON(Message{Code: code, Types: types, Message: message, Data: data}) 195 | //return c.conn.WriteJSON(map[string]interface{}{"code": code, "type": types, "message": message, "data": data}) 196 | } 197 | 198 | // SetReplyTime 设置上一次回复时间 199 | func (c *Conn) SetReplyTime() { 200 | 201 | c.lastReply = time.Now() 202 | } 203 | 204 | //--------------------------------------------------------------------------------------- 205 | 206 | func NewMessage(message []byte) Message { 207 | 208 | var m Message 209 | 210 | _ = json.Unmarshal(message, &m) 211 | 212 | return m 213 | 214 | } 215 | --------------------------------------------------------------------------------