add ginskeleton but change nothing

master
joefalmko 1 month ago
parent 8f83981e08
commit 6e30eb041d

@ -0,0 +1,6 @@
/.idea/
.idea/
.idea
/storage/logs/*
/storage/uploaded/*
/public/storage

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 张奇峰
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -0,0 +1,30 @@
## 这是什么?
- 1.这是一个基于go语言gin框架的web项目骨架专注于前后端分离的业务场景,其目的主要在于将web项目主线逻辑梳理清晰最基础的东西封装完善开发者更多关注属于自己的的业务即可。
- 2.本项目骨架封装了以`tb_users`表为核心的全部功能主要包括用户相关的接口参数验证器、注册、登录获取token、刷新token、CURD以及token鉴权等开发者拉取本项目骨架在此基础上就可以快速开发自己的项目。
- 3.<font color=#FF4500>本项目骨架请使用 `master` 分支版本即可, 该分支是最新稳定分支 </font>.
- 4.<font color=#FF4500>本项目骨架从V1.4.00开始要求go语言版本必须 >=1.15才能稳定地使用gorm v2读写分离方案,go1.15下载地址https://studygolang.com/dl </font>
- 5.该版本定位为主线版本,总体追求简洁、无界面,没有太多的业务逻辑,适合开发者自己随意扩展.
### [GinSkeleton 新版在线文档](https://www.yuque.com/xiaofensinixidaouxiang/bkfhct/mar1g7)
- 1.我们花费了极大的精力编写了非常完整、高质量的文档,初学者优先从如何使用学起, 成熟的开发者可以与我们一起研究 gin 内核源码,成为 gin 框架的高级开发.
- 2.学习 GinSkeleton 您只需要关注主线即可,我们没有创造太多新的语法,只要您会使用 gin 就可以迅速上手 Ginskeleton .
- 3.<font color=#FF4500>QQ群129885228 </font>
[旧文档入口](./ReadMEBak.md)
### ginskeleton 路由跳转插件
- ginskeleton 的路由是基于容器加载的,需要安装插件才能实现快速跳转.
- 安装步骤https://www.yuque.com/xiaofensinixidaouxiang/bkfhct/ngfzv1
### [点击进入 GinSkeleton-Admin2 系统](https://www.yuque.com/xiaofensinixidaouxiang/qmanaq/qmucb4)
- admin 系统集成界面, 定位快速开发业务方向, 可以在不需要修改一行代码的情况下,快速进入业务开发模式.
#### V 1.5.64 2024-08-03最新版本
**更新**
- 1.`websocket` 修复断电、直接拔网线导致服务端检测的终端在线状态不准确的bug, 因为直接断电、拔网线客户端的回调事件(onClose、onError)根本无法传递出去,服务端对应的socket文件状态无法及时变化.
- 2.项目依赖包全部更新至最新版.

@ -0,0 +1,312 @@
## 这是什么?
> 1.这是一个基于go语言gin框架的web项目骨架专注于前后端分离的业务场景,其目的主要在于将web项目主线逻辑梳理清晰最基础的东西封装完善开发者更多关注属于自己的的业务即可。
> 2.本项目骨架封装了以`tb_users`表为核心的全部功能主要包括用户相关的接口参数验证器、注册、登录获取token、刷新token、CURD以及token鉴权等开发者拉取本项目骨架在此基础上就可以快速开发自己的项目。
> 3.<font color=#FF4500>本项目骨架请使用 `master` 分支版本即可, 该分支是最新稳定分支 </font>.
> 4.<font color=#FF4500>本项目骨架从V1.4.00开始要求go语言版本必须 >=1.15才能稳定地使用gorm v2读写分离方案,go1.15下载地址https://studygolang.com/dl </font>
### 问题反馈
> 1.提交问题请在项目顶栏的`issue`直接添加问题,基本上都是每天处理当天上报的问题。
> 2.本项目优先关注 [Gitee Issue](https://gitee.com/daitougege/GinSkeleton/issues) 仓库的所有问题, github 太卡严重影响效率。
### 本项目主线逻辑图
> ![业务主线图](https://www.ginskeleton.com/GinSkeleton.jpg)
### 快速上手
- 1.go语言环境配置
```code
// 1.安装的go语言版本必须>=1.15 .
// 2.配置go包的代理打开你的终端(cmd黑窗口)并执行以下命令windwos系统
// 其他操作系统自行参见https://goproxy.cn
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
// 3.下载本项目依赖库
使用 goland(>=2019.3版本) 打开本项目,打开 goland 底部的 Terminal ,执行 go mod tidy 下载本项目依赖库
```
- 2.选择自己正在使用的数据库进行配置
```code
// 1.Mysql 数据库用户
// mysql数据库是默认数据库使用相关的客户端还原即可
找到`database/db_demo_mysql.sql`导入数据库,
// 2.SqlServer 数据库用户
1.找到`database/db_demo_sqlserver.sql`,复制内容,在相关的客户端窗口界面一次性执行即可,
2.在 app/model 目录内,使用 users_for_sqlserver.txt 的内容覆盖同目录的 users.go 已有内容
3.在 config/gorm_v2.yml 中,修改 UseDbTypesqlserver
// 3.PostgreSql 数据库用户
1.首先使用相关的客户端软件,手动创建数据 db_goskeleton选择该数据库.
2.找到`database/db_demo_postgre.sql`,复制内容,在相关的客户端窗口界面一次性执行即可,
3.在 app/model 目录内,使用 users_for_postgres.txt 的内容覆盖同目录的 users.go 已有内容
4.在 config/gorm_v2.yml 中,修改 UseDbTypepostgresql
// 4.完成以上三者中的其中一个之后,
在 config/gorm_v2.yml 选择您需要使用的数据库类型、配置账号、密码、端口等。
```
- 3.启动项目
```code
// 1.启动项目
使用goland打开本项目在根目录手动更新项目依赖执行命令 go mod tidy
双击`cmd/(web|api|cli)/main.go`,进入代码界面,找到 `main` 函数左侧,鼠标点击 `run`即可启动,此外鼠标右键`run`也可以启动.
```
### 项目目录结构介绍
>[核心结构](./docs/project_struct.md)
### 交叉编译(windows直接编译出linux可执行文件)
```code
// goland 终端底栏打开`terminal`, 依次执行以下命令,设置编译前的参数
// 特别注意: 以下三个命令执行时,前后不要有空格,否则最后编译可能会报错,无法编译出最终可执行文件
# 追加 env -w 表示将值写入环境变量,否则每次只是临时生效,
# 对于运行在linux服务器的程序后续编译就不需要重复设置编译前的参数如果程序最终运行在windows则编译参数 GOOS=windows
go env -w GOARCH=amd64 // cpu架构
go env -w GOOS=linux // 程序运行的最终系统linux、windows、darwin(苹果macos系统)
go env -w CGO_ENABLED=0 // window编译设置Cgo模块关闭因为windows上做cgo开发太麻烦如果引用了Cgo库库那么请在linux环境开发、编译
// 编译出最终可执行文件进入根目录GinSkeleton所在目录也就是 go.mod 所在的目录)
// 编译时建议追加参数:-ldflags "-w -s" -w 表示去除调试信息禁止gdb调试-s 表示去除符号表(符号表在链接时起着按符号寻址的作用,静态编译后用不到)
// 追加参数编译后的程序体积也会比原来减少25%左右.
// web|api|cli 三个目录选择其一即可,表示编译的入口目录
go build -o demo_goskeleton -ldflags "-w -s" cmd/(web|api|cli)/main.go
```
### <font color="red">项目骨架主线、核心逻辑</font>
> 这部分主要介绍了`项目初始化流程`、`路由`、`表单参数验证器`、`控制器`、`model`、`service` 以及 `websocket` 为核心的主线逻辑.
[进入主线逻辑文档](docs/document.md)
### 测试用例路由
[进入Api接口测试用例文档](docs/api_doc.md)
### 开发常用模块
> 随着项目不断完善以下列表模块会陆续增加, 虽然数目可能看起来会比较多,但是您只需要选择自己所需要的搭配主线使用即可.
> 只要掌握主线逻辑,结合以下模块,会让整个项目的操作更加流畅、简洁.
序号|功能模块 | 文档地址
---|---|---
1| 全局变量(日志、gorm、配置模块、雪花算法)| [清单一览](docs/global_variable.md)
2 | 表单参数验证器语法| [validator](docs/validator.md)
3 | 复杂表单参数提交| [复杂表单参数提交文档](docs/formparams.md)
4 | 消息队列| [rabbitmq文档](docs/rabbitmq.md)
5 | cli命令| [cobra文档](docs/cobra.md)
6 | goCurl、httpClient|[httpClient客户端](https://gitee.com/daitougege/goCurl)
7|[websocket js客户端](docs/ws_js_client.md)| [websocket服务端](./docs/websocket.md)
8|控制器aop切面编程| [Aop切面编程](docs/aop.md)
9|redis| [redis使用示例](test/redis_test.go)
10|gorm_v2 CURD 操作精华版| [ gorm+ginskeleton 增删改查精华](docs/concise.md)
11|gorm_v2操作(mysql、sqlserver、postgreSql)| [gorm v2 更多测试用例](test/gormv2_test.go)
12|多源数据库的操作| [同时连接多台服务器的mysql、sqlserver、postgresql操作](docs/many_db_operate.md)
13|gorm_v2 Scan Find函数查询结果一键树形化| [sql结果树形化反射扫描器](https://gitee.com/daitougege/sql_res_to_tree)
14|日志记录| [zap高性能日志](docs/zap_log.md)
15|ELK 项目日志顶级解决方案| [elk 7.13.3 推荐使用](https://gitee.com/daitougege/elk-docker-compose) <br/> <s>[elk 7.9.1 旧版本](docs/elk_log.md)</s>
16| 验证码(captcha)以及验证码中间件| [验证码使用详情](docs/captcha.md)
17| nginx配置(https、负载均衡)|[nginx配置详情](docs/nginx.md)
18|主线解耦| [对验证器与控制器进行解耦](docs/low_coupling.md)
19|Casbin 接口访问权限管控| [Casbin使用介绍](docs/casbin.md)
20|Mysql主从同步(旨在实现读写分离)| [使用docker-compose快速搭建](https://gitee.com/daitougege/mysql-master-slave-docker-compose)
### 项目部署方案
序号|部署办法 | 文档地址
---|---|---
1 | 开发、调试环境| [最简单的 nohup](docs/deploy_nohup.md)
2 | 生产环境之supervisor进程守护 | [稳定可靠的进程守护方案](docs/supervisor.md)
3 | 生产环境之docker部署方案 | [稳定可靠、版本回滚、扩容非常灵活的方案](docs/deploy_docker.md)
### 项目上线后,运维方案(基于docker)
序号|运维模块 | 文档地址
---|---|---
1 | linux服务器| [性能指标监控](http://gitee.com/daitougege/grafana-prometheus-nodeexpoter) <br/> <s>[旧版本](docs/deploy_linux.md)</s>
### 并发测试
[点击查看详情](docs/bench_cpu_memory.md)
### 性能分析报告
> 1.开发之初,我们的目标就是追求极致的高性能,因此在项目整体功能越来越趋于完善之时,我们现将进行一次全面的性能分析评测.
> 2.通过执行相关代码, 跟踪 cpu 耗时 和 内存占用 来分析各个部分的性能,CPU耗时越短性、内存占用越低能越优秀,反之就比较垃圾.
#### 通过CPU的耗时来分析相关代码段的性能
序号|分析对象 | 文档地址
---|---|---
1| 项目骨架主线逻辑| [主线分析报告](./docs/project_analysis_1.md)
2| 操作数据库代码段| [操作数据库代码段分析报告](./docs/project_analysis_2.md)
#### 通过内存占用来分析相关代码段的性能
序号|分析对象 | 文档地址
---|---|---
1| 操作数据库代码段| [操作数据库代码段](./docs/project_analysis_3.md)
### <font color='red'>FAQ 常见问题汇总 </font>
[点击查看详情](./docs/faq.md)
## GinSkeleton-Admin 后台系统
> 1.本系统是基于 GinSkeleton(v1.5.10) + Iview(v4.5.0) 开发而成的企业级项目后台骨架.
> 2.在线演示系统相比本地运行的版本收缩了修改、删除 数据的权限.
![预览图](https://www.ginskeleton.com/images/home_page1.png)
### [在线演示系统: GinSkeleton-Admin](http://139.196.101.31:20202/)
### [admin 后端仓库](https://gitee.com/daitougege/gin-skeleton-admin-backend)
### [admin 前端仓库](https://gitee.com/daitougege/gin-skeleton-admin-frontend)
#### 主线版本更新日志
#### V 1.5.30 2021-11-28
* 新增
1.引入表单参数验证器全局自动翻译器,简化代码书写,提升开发效率.
* 更新
1.按照gin官方提示,当程序切换到生产模式时,对gin的路由进行二次封装、异常恢复中间件自定义重写,release模式经过并发测试可以获得5%的性能提升.
1.1 当配置文件(config/config.yml)中的键 `AppDebug` 设置为 `false` 时,gin 路由默认启用 `release` 模式,并且不会记录接口访问日志,生产环境请使用 `nginx` 代理,也方便实现负载均衡.
2.其他更新主要是一些细节:文档、程序注释方面.
#### V 1.5.29 2021-11-15
* 新增
1.多源数据库操作文档.
2.在 `cli` 模式执行操作数据库命令时支持 `created_at``updated_at` 字段自动赋值.
3.`gorm v2` 接入层 `utils` 增加 `Create` 函数的参数类型非指针时拦截检查逻辑, 避免发生 `panic` ,该函数官方没有针对数据类型做安全检查.
4.`gorm v2` 接入层 `utils` 增加 `Save、Update` 函数的参数类型非指针时拦截检查逻辑,以便支持 `gorm` 的所有回调函数.
5.为了完美支持第4条功能今后开发者使用 `gorm` 函数 `Create 、Save、Update ` 时请统一传递指针类型的参数, 如果老项目直接合并 `ginskeleton` 的代码, 原来调用 `Save、Update` 函数的参数需要手动修改为指针类型.
* 更新
1.验证码控制器文件单词拼写错误修正.
2.路由中的一些注释更新.
3.所有依赖包更新至最新版,与 `gorm` 包相关的接入层(utils)日志部分也同步更新.
#### V 1.5.28 2021-10-07
* 更新
1.文档更新,增加复杂表单参数提交的处理示例文档,文档其他完善更新.
2.解决项目在 `linux` 环境启动时,如果 `public` 目录内有从 `windows` 环境复制过来的软连接无法删除的问题.
3.`token` 刷新路由与其他路由逻辑分离.
* 漏洞修复:
1.` ≤ V1.5.24 ` 包括此版本 `token` 认证中间件存在被恶意构造特殊 `token` 绕过的风险,请尽快升级到最新版.
1.1 升级方法:使用最新的 `app/http/middleware/authorization/auth.go` 替换 `V1.5.24`以及之前的版本同位置代码即可.
#### V 1.5.27 2021-09-18
* 更新
1.`app/model/users.go` 中,操作数据库的函数参数,个别使用了 `float64` ,全部统一为 `int` 系列,避免给开发者带来不必要的困扰.
#### V 1.5.26 2021-09-13
* 更新
1.精简合并代码.
#### V 1.5.25 2021-09-13
* 新增
1.cli命令模式增加简单示例,方便新用户快速上手,相关位置:./command/demo_simple/.
* 更新
1.过期token刷新逻辑增加延期时间范围,方便已经处于过期时间范围内的token刷新换取新token.
2.交叉编译部分完善常用编译参数说明.
#### V 1.5.24 2021-09-03
* 修复
1.图形验证码逻辑:如果没有使用本系统封装的验证码中间件,而是直接调用了自定义验证逻辑部分代码,则一直提示没有获取验证码信息.
* 更新
1.编译部分,增加编译时参数的选项说明.
2.websocket 完善文档使用说明.
3.在安装有360软件的机器上本项目启动失败增加提示原因.
#### V 1.5.23 2021-08-06
* 修复
1.postgresql文件 `app/model/users_for_postgres.txt` 中一处bug登陆后登陆次数+1时sql语句报错.
* 更新
1.为 `http://github.com/casbin/gorm-adapter` 依赖包提交pr,由于官方已经合并,此包更新至最新版,解决postgresql创建索引报错的bug.
#### V 1.5.22 2021-08-04
* 新增
1.项目部署方案.
2.mysql主从同步快速部署方案.
3.新增redis执行结果常用转换函数.
4.新增postgresql数据库demo,至此,主线版本已经全面支持 mysql、sqlserver、postgresql数据库.
* 更新
1.项目依赖的所有包更新至最新版.
2.项目使用文档.
#### V 1.5.21 2021-07-16
* 更新
1.项目依赖的所有包更新至最新版.
2.项目日志对接到 elk 日志管理中心,增加 `docker-compose.yml` 集成环境快速部署脚本,详情参见常用开发模块第 13 项.
3.增加项目部署文档.
#### V 1.5.20 2021-06-18
* 更新
1.表单参数验证器示例代码更新,提供了更加紧凑的书写示例代码,相关示例文档同步更新.
2.一个用户同时允许最大在线的token, 查询时优先按照 expires_at 倒序排列,便于不同系统间对接时,那种长久有效的token不会被"踢"下线.
3.command 命令示例 demo 调整为按照子目录创建 cli 命令,方便更清晰地组织更多的 command 命令代码.
4.nginx 部署文档优化在nginx处理请求时,相关的静态资源直接由nginx拦截响应提升响应速度,这样 go 程序将更专注于处于api接口请求.
5.自带的 mysql 数据库创建脚本字段 last_login_ip , 设置默认值为 '' .
#### V 1.5.17 2021-06-06
* 新增、更新
1.sqlserver 数据库对应的用户模型,参见 app/model/users_for_sqlserver.txt.
2.更新 database/db_demo_sqlserver.sql 数据库、表创建命令.
修复
1.修正常量定义处日期格式单词书写错误问题.
#### V 1.5.16 2021-05-28
* 新增
1.增加验证码中间件以及使用介绍.
#### V 1.5.15 2021-05-11
* 完善
1.文件上传后自动创建目录时,目录权限由(0666)调整为os.ModePerm,解决可能遇到的权限问题 .
2.cobra 文档增加创建子命令的示例链接.
#### V 1.5.14 2021-04-28
* 完善
1.更新 rabbitMq 排版
2.更新 websocket 文档
#### V 1.5.13 2021-04-27
* 完善
1.表单参数验证器注册文件拆分为api、web,当项目较大时,尽可能保持逻辑清晰、简洁.
3.完善细节,避免mysql 函数 FROM_UNIXTIME 参数最大只能支持21亿的局限.
3.核心依赖包升级至最新版.
#### V 1.5.12 2021-04-20
* 完善
1.app/model/users 增加注释主要是主线版本操作数据库大量使用了原生sql注释主要增加了 gorm_v2 自带语法操作数据库的链接地址.
2.代码中涉及到的分页语法(limit offset,limit),参数 offset,limit 统一调整为 int 型,解决mysql8.x系列高版本的数据库不支持浮点型的问题.
#### V 1.5.11 2021-04-02
* 变更
1.app/model/BaseModel 文件中,UseDbConn 函数名首字符调整为大写,方便创建更多的子级目录.
* 更新
1.日志(nginx 的access.log)对接到 ELK 日志管理中心,相关文档更新,增加了ip转 经纬度功能,方便展示用户在世界地图的分布.
2.针对上一条,补充了日志展示的整体[效果图](docs/elk_log.md)
#### V 1.5.10 2021-03-23
* 完善
1.form表单参数验证器完成验证后, 自动为上下文绑定三个键created_at、updated_at、deleted_at ,相关值均为请求时的日期时间.
2.baseModel 中 created_at、updated_at 修改为 string 类型,方便从上下文自动绑定对应的键值到 model .
3.用户每次登录后tb_users 表,登陆次数字段+1 .
4.nginx 部署文档修正一处缺少单引号的错误.
5.gorm 操作数据库精华版文档更新.
6.删除其他小部分无关代码.
7.增加自动创建连接功能,只为更好地处理静态资源.
8.文件上传代码配置项增加部分参数,代码同步升级.
9.GinSkeleton-Admin 系统同步发布.
#### V 1.5.00 2021-03-10
* 新增
1.为即将发布的 GinSkeleton-Admin 系统增加了基础支撑模块casbin模块、gorm_v2 操作精华版文档,参见**常用开发模块**列表.
2.token模块引用的部分常量值调整到配置文件.
3.调整token校验中间件和casbin中间件名称.
4.主线版本本次更新并不是很多,今后主线版本将依然保持简洁,后续的新功能模块都将以包的形式引入和调用.
5.更多企业级的功能将在后续推出的 GinSkeleton-Admin 展现,欢迎关注本项目,反馈使用意见.
V 1.1.xx - 1.4.xx 版本日志
> 1.[历史日志](docs/history_log.md)
### 感谢 jetbrains 为本项目提供的 goland 激活码
![https://www.jetbrains.com/](https://www.ginskeleton.com/images/jetbrains.jpg)

@ -0,0 +1,19 @@
package users
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/global/variable"
)
// 模拟Aop 实现对某个控制器函数的前置和后置回调
type DestroyAfter struct{}
func (d *DestroyAfter) After(context *gin.Context) {
// 后置函数可以使用异步执行
go func() {
userId := context.GetFloat64(consts.ValidatorPrefix + "id")
variable.ZapLog.Sugar().Infof("模拟 Users 删除操作, After 回调,用户ID%.f\n", userId)
}()
}

@ -0,0 +1,22 @@
package users
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/global/variable"
)
// 模拟Aop 实现对某个控制器函数的前置和后置回调
type DestroyBefore struct{}
// 前置函数必须具有返回值,这样才能控制流程是否继续向下执行
func (d *DestroyBefore) Before(context *gin.Context) bool {
userId := context.GetFloat64(consts.ValidatorPrefix + "id")
variable.ZapLog.Sugar().Infof("模拟 Users 删除操作, Before 回调,用户ID%.f\n", userId)
if userId > 10 {
return true
} else {
return false
}
}

@ -0,0 +1,69 @@
package container
import (
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"log"
"strings"
"sync"
)
// 定义一个全局键值对存储容器
var sMap sync.Map
// CreateContainersFactory 创建一个容器工厂
func CreateContainersFactory() *containers {
return &containers{}
}
// 定义一个容器结构体
type containers struct {
}
// Set 1.以键值对的形式将代码注册到容器
func (c *containers) Set(key string, value interface{}) (res bool) {
if _, exists := c.KeyIsExists(key); exists == false {
sMap.Store(key, value)
res = true
} else {
// 程序启动阶段zaplog 未初始化使用系统log打印启动时候发生的异常日志
if variable.ZapLog == nil {
log.Fatal(my_errors.ErrorsContainerKeyAlreadyExists + ",请解决键名重复问题,相关键:" + key)
} else {
// 程序启动初始化完成
variable.ZapLog.Warn(my_errors.ErrorsContainerKeyAlreadyExists + ", 相关键:" + key)
}
}
return
}
// Delete 2.删除
func (c *containers) Delete(key string) {
sMap.Delete(key)
}
// Get 3.传递键,从容器获取值
func (c *containers) Get(key string) interface{} {
if value, exists := c.KeyIsExists(key); exists {
return value
}
return nil
}
// KeyIsExists 4. 判断键是否被注册
func (c *containers) KeyIsExists(key string) (interface{}, bool) {
return sMap.Load(key)
}
// FuzzyDelete 按照键的前缀模糊删除容器中注册的内容
func (c *containers) FuzzyDelete(keyPre string) {
sMap.Range(func(key, value interface{}) bool {
if keyname, ok := key.(string); ok {
if strings.HasPrefix(keyname, keyPre) {
sMap.Delete(keyname)
}
}
return true
})
}

@ -0,0 +1,25 @@
package destroy
import (
"go.uber.org/zap"
"goskeleton/app/core/event_manage"
"goskeleton/app/global/consts"
"goskeleton/app/global/variable"
"os"
"os/signal"
"syscall"
)
func init() {
// 用于系统信号的监听
go func() {
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, os.Kill, syscall.SIGQUIT, syscall.SIGINT, syscall.SIGTERM) // 监听可能的退出信号
received := <-c //接收信号管道中的值
variable.ZapLog.Warn(consts.ProcessKilled, zap.String("信号值", received.String()))
(event_manage.CreateEventManageFactory()).FuzzyCall(variable.EventDestroyPrefix)
close(c)
os.Exit(1)
}()
}

@ -0,0 +1,73 @@
package event_manage
import (
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"strings"
"sync"
)
// 定义一个全局事件存储变量,本模块只负责存储 键 => 函数 相对容器来说功能稍弱,但是调用更加简单、方便、快捷
var sMap sync.Map
// 创建一个事件管理工厂
func CreateEventManageFactory() *eventManage {
return &eventManage{}
}
// 定义一个事件管理结构体
type eventManage struct {
}
// 1.注册事件
func (e *eventManage) Set(key string, keyFunc func(args ...interface{})) bool {
//判断key下是否已有事件
if _, exists := e.Get(key); exists == false {
sMap.Store(key, keyFunc)
return true
} else {
variable.ZapLog.Info(my_errors.ErrorsFuncEventAlreadyExists + " , 相关键名:" + key)
}
return false
}
// 2.获取事件
func (e *eventManage) Get(key string) (interface{}, bool) {
if value, exists := sMap.Load(key); exists {
return value, exists
}
return nil, false
}
// 3.执行事件
func (e *eventManage) Call(key string, args ...interface{}) {
if valueInterface, exists := e.Get(key); exists {
if fn, ok := valueInterface.(func(args ...interface{})); ok {
fn(args...)
} else {
variable.ZapLog.Error(my_errors.ErrorsFuncEventNotCall + ", 键名:" + key + ", 相关函数无法调用")
}
} else {
variable.ZapLog.Error(my_errors.ErrorsFuncEventNotRegister + ", 键名:" + key)
}
}
// 4.删除事件
func (e *eventManage) Delete(key string) {
sMap.Delete(key)
}
// 5.根据键的前缀,模糊调用. 使用请谨慎.
func (e *eventManage) FuzzyCall(keyPre string) {
sMap.Range(func(key, value interface{}) bool {
if keyName, ok := key.(string); ok {
if strings.HasPrefix(keyName, keyPre) {
e.Call(keyName)
}
}
return true
})
}

@ -0,0 +1,75 @@
package consts
// 这里定义的常量,一般是具有错误代码+错误说明组成,一般用于接口返回
const (
// 进程被结束
ProcessKilled string = "收到信号,进程被结束"
// 表单验证器前缀
ValidatorPrefix string = "Form_Validator_"
ValidatorParamsCheckFailCode int = -400300
ValidatorParamsCheckFailMsg string = "参数校验失败"
//服务器代码发生错误
ServerOccurredErrorCode int = -500100
ServerOccurredErrorMsg string = "服务器内部发生代码执行错误, "
GinSetTrustProxyError string = "Gin 设置信任代理服务器出错"
// token相关
JwtTokenOK int = 200100 //token有效
JwtTokenInvalid int = -400100 //无效的token
JwtTokenExpired int = -400101 //过期的token
JwtTokenFormatErrCode int = -400102 //提交的 token 格式错误
JwtTokenFormatErrMsg string = "提交的 token 格式错误" //提交的 token 格式错误
JwtTokenMustValid string = "token为必填项,请在请求header部分提交!" //提交的 token 格式错误
//SnowFlake 雪花算法
StartTimeStamp = int64(1483228800000) //开始时间截 (2017-01-01)
MachineIdBits = uint(10) //机器id所占的位数
SequenceBits = uint(12) //序列所占的位数
//MachineIdMax = int64(-1 ^ (-1 << MachineIdBits)) //支持的最大机器id数量
SequenceMask = int64(-1 ^ (-1 << SequenceBits)) //
MachineIdShift = SequenceBits //机器id左移位数
TimestampShift = SequenceBits + MachineIdBits //时间戳左移位数
// CURD 常用业务状态码
CurdStatusOkCode int = 200
CurdStatusOkMsg string = "Success"
CurdCreatFailCode int = -400200
CurdCreatFailMsg string = "新增失败"
CurdUpdateFailCode int = -400201
CurdUpdateFailMsg string = "更新失败"
CurdDeleteFailCode int = -400202
CurdDeleteFailMsg string = "删除失败"
CurdSelectFailCode int = -400203
CurdSelectFailMsg string = "查询无数据"
CurdRegisterFailCode int = -400204
CurdRegisterFailMsg string = "注册失败"
CurdLoginFailCode int = -400205
CurdLoginFailMsg string = "登录失败"
CurdRefreshTokenFailCode int = -400206
CurdRefreshTokenFailMsg string = "刷新Token失败"
//文件上传
FilesUploadFailCode int = -400250
FilesUploadFailMsg string = "文件上传失败, 获取上传文件发生错误!"
FilesUploadMoreThanMaxSizeCode int = -400251
FilesUploadMoreThanMaxSizeMsg string = "长传文件超过系统设定的最大值,系统允许的最大值:"
FilesUploadMimeTypeFailCode int = -400252
FilesUploadMimeTypeFailMsg string = "文件mime类型不允许"
FilesUploadIsEmpty string = "不允许上传空文件"
//websocket
WsServerNotStartCode int = -400300
WsServerNotStartMsg string = "websocket 服务没有开启请在配置文件开启相关路径config/config.yml"
WsOpenFailCode int = -400301
WsOpenFailMsg string = "websocket open阶段初始化基本参数失败"
//验证码
CaptchaGetParamsInvalidMsg string = "获取验证码:提交的验证码参数无效,请检查验证码ID以及文件名后缀是否完整"
CaptchaGetParamsInvalidCode int = -400350
CaptchaCheckParamsInvalidMsg string = "校验验证码:提交的参数无效,请检查 【验证码ID、验证码值】 提交时的键名是否与配置项一致"
CaptchaCheckParamsInvalidCode int = -400351
CaptchaCheckOkMsg string = "验证码校验通过"
CaptchaCheckFailCode int = -400355
CaptchaCheckFailMsg string = "验证码校验失败"
)

@ -0,0 +1,70 @@
package my_errors
const (
//系统部分
ErrorsContainerKeyAlreadyExists string = "该键已经注册在容器中了"
ErrorsPublicNotExists string = "public 目录不存在"
ErrorsConfigYamlNotExists string = "config.yml 配置文件不存在"
ErrorsConfigGormNotExists string = "gorm_v2.yml 配置文件不存在"
ErrorsStorageLogsNotExists string = "storage/logs 目录不存在"
ErrorsConfigInitFail string = "初始化配置文件发生错误"
ErrorsSoftLinkCreateFail string = "自动创建软连接失败,请以管理员身份运行客户端(开发环境为goland等生产环境检查命令执行者权限), " +
"最后一个可能如果您是360用户请退出360相关软件才能保证go语言创建软连接函数 os.Symlink() 正常运行"
ErrorsSoftLinkDeleteFail string = "删除软软连接失败"
ErrorsFuncEventAlreadyExists string = "注册函数类事件失败,键名已经被注册"
ErrorsFuncEventNotRegister string = "没有找到键名对应的函数"
ErrorsFuncEventNotCall string = "注册的函数无法正确执行"
ErrorsBasePath string = "初始化项目根目录失败"
ErrorsTokenBaseInfo string = "token最基本的格式错误,请提供一个有效的token!"
ErrorsNoAuthorization string = "token鉴权未通过请通过token授权接口重新获取token,"
ErrorsRefreshTokenFail string = "token不符合刷新条件,请通过登陆接口重新获取token!"
ErrorsParseTokenFail string = "解析token失败"
ErrorsGormInitFail string = "Gorm 数据库驱动、连接初始化失败"
ErrorsCasbinNoAuthorization string = "Casbin 鉴权未通过,请在后台检查 casbin 设置参数"
ErrorsGormNotInitGlobalPointer string = "%s 数据库全局变量指针没有初始化,请在配置文件 config/gorm_v2.yml 设置 Gormv2.%s.IsInitGlobalGormMysql = 1, 并且保证数据库配置正确 \n"
// 数据库部分
ErrorsDbDriverNotExists string = "数据库驱动类型不存在,目前支持的数据库类型mysql、sqlserver、postgresql您提交数据库类型"
ErrorsDialectorDbInitFail string = "gorm dialector 初始化失败,dbType:"
ErrorsGormDBCreateParamsNotPtr string = "gorm Create 函数的参数必须是一个指针"
ErrorsGormDBUpdateParamsNotPtr string = "gorm 的 Update、Save 函数的参数必须是一个指针(GinSkeleton ≥ v1.5.29 版本新增验证,为了完美支持 gorm 的所有回调函数,请在参数前面添加 & )"
//redis部分
ErrorsRedisInitConnFail string = "初始化redis连接池失败"
ErrorsRedisAuthFail string = "Redis Auth 鉴权失败,密码错误"
ErrorsRedisGetConnFail string = "Redis 从连接池获取一个连接失败,超过最大重试次数"
// 表单参数验证器未通过时的错误
ErrorsValidatorNotExists string = "不存在的验证器"
ErrorsValidatorTransInitFail string = "validator的翻译器初始化错误"
ErrorNotAllParamsIsBlank string = "该接口不允许所有参数都为空,请按照接口要求提交必填参数"
ErrorsValidatorBindParamsFail string = "验证器绑定参数失败"
//token部分
ErrorsTokenInvalid string = "无效的token"
ErrorsTokenNotActiveYet string = "token 尚未激活"
ErrorsTokenMalFormed string = "token 格式不正确"
//snowflake
ErrorsSnowflakeGetIdFail string = "获取snowflake唯一ID过程发生错误"
// websocket
ErrorsWebsocketOnOpenFail string = "websocket onopen 发生阶段错误"
ErrorsWebsocketUpgradeFail string = "websocket Upgrade 协议升级, 发生错误"
ErrorsWebsocketReadMessageFail string = "websocket ReadPump(实时读取消息)协程出错"
ErrorsWebsocketBeatHeartFail string = "websocket BeatHeart心跳协程出错"
ErrorsWebsocketBeatHeartsMoreThanMaxTimes string = "websocket BeatHeart 失败次数超过最大值"
ErrorsWebsocketClientOfflineTimeout string = "websocket 客户端响应ping消息超过服务端允许的最长时间(秒):"
ErrorsWebsocketSetWriteDeadlineFail string = "websocket 设置消息写入截止时间出错"
ErrorsWebsocketWriteMgsFail string = "websocket Write Msg(send msg) 失败"
ErrorsWebsocketStateInvalid string = "websocket state 状态已经不可用(掉线、卡死等愿意,造成双方无法进行数据交互)"
// rabbitMq
ErrorsRabbitMqReconnectFail string = "RabbitMq消费者端掉线后重连失败超过尝试最大次数"
//文件上传
ErrorsFilesUploadOpenFail string = "打开文件失败,详情:"
ErrorsFilesUploadReadFail string = "读取文件32字节失败详情"
// casbin 初始化可能的错误
ErrorCasbinCanNotUseDbPtr string = "casbin 的初始化基于gorm 初始化后的数据库连接指针,程序检测到 gorm 连接指针无效,请检查数据库配置!"
ErrorCasbinCreateAdaptFail string = "casbin NewAdapterByDBUseTableName 发生错误:"
ErrorCasbinCreateEnforcerFail string = "casbin NewEnforcer 发生错误:"
ErrorCasbinNewModelFromStringFail string = "NewModelFromString 调用时出错:"
)

@ -0,0 +1,62 @@
package variable
import (
"github.com/casbin/casbin/v2"
"go.uber.org/zap"
"gorm.io/gorm"
"goskeleton/app/global/my_errors"
"goskeleton/app/utils/snow_flake/snowflake_interf"
"goskeleton/app/utils/yml_config/ymlconfig_interf"
"log"
"os"
"strings"
)
// ginskeleton 封装的全局变量全部支持并发安全,请放心使用即可
// 开发者自行封装的全局变量,请做好并发安全检查与确认
var (
BasePath string // 定义项目的根目录
EventDestroyPrefix = "Destroy_" // 程序退出时需要销毁的事件前缀
ConfigKeyPrefix = "Config_" // 配置文件键值缓存时,键的前缀
DateFormat = "2006-01-02 15:04:05" // 设置全局日期时间格式
// 全局日志指针
ZapLog *zap.Logger
// 全局配置文件
ConfigYml ymlconfig_interf.YmlConfigInterf // 全局配置文件指针
ConfigGormv2Yml ymlconfig_interf.YmlConfigInterf // 全局配置文件指针
//gorm 数据库客户端如果您操作数据库使用的是gorm请取消以下注释在 bootstrap>init 文件,进行初始化即可使用
GormDbMysql *gorm.DB // 全局gorm的客户端连接
GormDbSqlserver *gorm.DB // 全局gorm的客户端连接
GormDbPostgreSql *gorm.DB // 全局gorm的客户端连接
//雪花算法全局变量
SnowFlake snowflake_interf.InterfaceSnowFlake
//websocket
WebsocketHub interface{}
WebsocketHandshakeSuccess = `{"code":200,"msg":"ws连接成功","data":""}`
WebsocketServerPingMsg = "Server->Ping->Client"
//casbin 全局操作指针
Enforcer *casbin.SyncedEnforcer
// 用户自行定义其他全局变量 ↓
)
func init() {
// 1.初始化程序根目录
if curPath, err := os.Getwd(); err == nil {
// 路径进行处理,兼容单元测试程序程序启动时的奇怪路径
if len(os.Args) > 1 && strings.HasPrefix(os.Args[1], "-test") {
BasePath = strings.Replace(strings.Replace(curPath, `\test`, "", 1), `/test`, "", 1)
} else {
BasePath = curPath
}
} else {
log.Fatal(my_errors.ErrorsBasePath)
}
}

@ -0,0 +1,33 @@
package api
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/utils/response"
)
type Home struct {
}
// 1.门户类首页新闻
func (u *Home) News(context *gin.Context) {
// 由于本项目骨架已经将表单验证器的字段(成员)绑定在上下文,因此可以按照 GetString()、GetInt64()、GetFloat64等快捷获取需要的数据类型
// 当然也可以通过gin框架的上下文原原始方法获取例如 context.PostForm("name") 获取,这样获取的数据格式为文本,需要自己继续转换
newsType := context.GetString(consts.ValidatorPrefix + "newsType")
page := context.GetFloat64(consts.ValidatorPrefix + "page")
limit := context.GetFloat64(consts.ValidatorPrefix + "limit")
userIp := context.ClientIP()
ref := context.GetHeader("Referer")
// 这里随便模拟一条数据返回
response.Success(context, "ok", gin.H{
"newsType": newsType,
"page": page,
"limit": limit,
"userIp": userIp,
"title": "门户首页公司新闻标题001",
"content": "门户新闻内容001",
"referer": ref,
})
}

@ -0,0 +1,83 @@
package captcha
import (
"bytes"
"github.com/dchest/captcha"
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/global/variable"
"goskeleton/app/utils/response"
"net/http"
"path"
"time"
)
type Captcha struct{}
// 生成验证码ID
func (c *Captcha) GenerateId(context *gin.Context) {
// 设置验证码的数字长度(个数)
var length = variable.ConfigYml.GetInt("Captcha.length")
var captchaId, imgUrl, refresh, verify string
captchaId = captcha.NewLen(length)
imgUrl = "/captcha/" + captchaId + ".png"
refresh = imgUrl + "?reload=1"
verify = "/captcha/" + captchaId + "/这里替换为正确的验证码进行验证"
response.Success(context, "验证码信息", gin.H{
"id": captchaId,
"img_url": imgUrl,
"refresh": refresh,
"verify": verify,
})
}
// 获取验证码图像
func (c *Captcha) GetImg(context *gin.Context) {
captchaIdKey := variable.ConfigYml.GetString("Captcha.captchaId")
captchaId := context.Param(captchaIdKey)
_, file := path.Split(context.Request.URL.Path)
ext := path.Ext(file)
id := file[:len(file)-len(ext)]
if ext == "" || captchaId == "" {
response.Fail(context, consts.CaptchaGetParamsInvalidCode, consts.CaptchaGetParamsInvalidMsg, "")
return
}
if context.Query("reload") != "" {
captcha.Reload(id)
}
context.Header("Cache-Control", "no-cache, no-store, must-revalidate")
context.Header("Pragma", "no-cache")
context.Header("Expires", "0")
var vBytes bytes.Buffer
if ext == ".png" {
context.Header("Content-Type", "image/png")
// 设置实际业务需要的验证码图片尺寸(宽 X 高captcha.StdWidth, captcha.StdHeight 为默认值,请自行修改为具体数字即可
_ = captcha.WriteImage(&vBytes, id, captcha.StdWidth, captcha.StdHeight)
http.ServeContent(context.Writer, context.Request, id+ext, time.Time{}, bytes.NewReader(vBytes.Bytes()))
}
}
// 校验验证码
func (c *Captcha) CheckCode(context *gin.Context) {
captchaIdKey := variable.ConfigYml.GetString("Captcha.captchaId")
captchaValueKey := variable.ConfigYml.GetString("Captcha.captchaValue")
captchaId := context.Param(captchaIdKey)
value := context.Param(captchaValueKey)
if captchaId == "" || value == "" {
response.Fail(context, consts.CaptchaCheckParamsInvalidCode, consts.CaptchaCheckParamsInvalidMsg, "")
return
}
if captcha.VerifyString(captchaId, value) {
response.Success(context, consts.CaptchaCheckOkMsg, "")
} else {
response.Fail(context, consts.CaptchaCheckFailCode, consts.CaptchaCheckFailMsg, "")
}
}

@ -0,0 +1,23 @@
package web
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/global/variable"
"goskeleton/app/service/upload_file"
"goskeleton/app/utils/response"
)
type Upload struct {
}
// 文件上传是一个独立模块,给任何业务返回文件上传后的存储路径即可。
// 开始上传
func (u *Upload) StartUpload(context *gin.Context) {
savePath := variable.BasePath + variable.ConfigYml.GetString("FileUploadSetting.UploadFileSavePath")
if r, finnalSavePath := upload_file.Upload(context, savePath); r == true {
response.Success(context, consts.CurdStatusOkMsg, finnalSavePath)
} else {
response.Fail(context, consts.FilesUploadFailCode, consts.FilesUploadFailMsg, "")
}
}

@ -0,0 +1,144 @@
package web
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/global/variable"
"goskeleton/app/model"
"goskeleton/app/service/users/curd"
userstoken "goskeleton/app/service/users/token"
"goskeleton/app/utils/response"
"time"
)
type Users struct {
}
// 1.用户注册
func (u *Users) Register(context *gin.Context) {
// 由于本项目骨架已经将表单验证器的字段(成员)绑定在上下文,因此可以按照 GetString()、context.GetBool()、GetFloat64等快捷获取需要的数据类型注意相关键名规则 前缀+验证器结构体中的 json 标签
// 注意:在 ginskeleton 中获取表单参数验证器中的数字键(字段),请统一使用 GetFloat64(),其它获取数字键字段的函数无效例如GetInt()、GetInt64()等
// 当然也可以通过gin框架的上下文原始方法获取例如 context.PostForm("user_name") 获取,这样获取的数据格式为文本,需要自己继续转换
userName := context.GetString(consts.ValidatorPrefix + "user_name")
pass := context.GetString(consts.ValidatorPrefix + "pass")
userIp := context.ClientIP()
if curd.CreateUserCurdFactory().Register(userName, pass, userIp) {
response.Success(context, consts.CurdStatusOkMsg, "")
} else {
response.Fail(context, consts.CurdRegisterFailCode, consts.CurdRegisterFailMsg, "")
}
}
// 2.用户登录
func (u *Users) Login(context *gin.Context) {
userName := context.GetString(consts.ValidatorPrefix + "user_name")
pass := context.GetString(consts.ValidatorPrefix + "pass")
phone := context.GetString(consts.ValidatorPrefix + "phone")
userModelFact := model.CreateUserFactory("")
userModel := userModelFact.Login(userName, pass)
if userModel != nil {
userTokenFactory := userstoken.CreateUserFactory()
if userToken, err := userTokenFactory.GenerateToken(userModel.Id, userModel.UserName, userModel.Phone, variable.ConfigYml.GetInt64("Token.JwtTokenCreatedExpireAt")); err == nil {
if userTokenFactory.RecordLoginToken(userToken, context.ClientIP()) {
data := gin.H{
"userId": userModel.Id,
"user_name": userName,
"realName": userModel.RealName,
"phone": phone,
"token": userToken,
"updated_at": time.Now().Format(variable.DateFormat),
}
response.Success(context, consts.CurdStatusOkMsg, data)
go userModel.UpdateUserloginInfo(context.ClientIP(), userModel.Id)
return
}
}
}
response.Fail(context, consts.CurdLoginFailCode, consts.CurdLoginFailMsg, "")
}
// 刷新用户token
func (u *Users) RefreshToken(context *gin.Context) {
oldToken := context.GetString(consts.ValidatorPrefix + "token")
if newToken, ok := userstoken.CreateUserFactory().RefreshToken(oldToken, context.ClientIP()); ok {
res := gin.H{
"token": newToken,
}
response.Success(context, consts.CurdStatusOkMsg, res)
} else {
response.Fail(context, consts.CurdRefreshTokenFailCode, consts.CurdRefreshTokenFailMsg, "")
}
}
// 后面是 curd 部分,自带版本中为了降低初学者学习难度,使用了最简单的方式操作 增、删、改、查
// 在开发企业实际项目中,建议使用我们提供的一整套 curd 快速操作模式
// 参考地址https://gitee.com/daitougege/GinSkeleton/blob/master/docs/concise.md
// 您也可以参考 Admin 项目地址https://gitee.com/daitougege/gin-skeleton-admin-backend/ 中, app/model/ 提供的示例语法
//3.用户查询show
func (u *Users) Show(context *gin.Context) {
userName := context.GetString(consts.ValidatorPrefix + "user_name")
page := context.GetFloat64(consts.ValidatorPrefix + "page")
limit := context.GetFloat64(consts.ValidatorPrefix + "limit")
limitStart := (page - 1) * limit
counts, showlist := model.CreateUserFactory("").Show(userName, int(limitStart), int(limit))
if counts > 0 && showlist != nil {
response.Success(context, consts.CurdStatusOkMsg, gin.H{"counts": counts, "list": showlist})
} else {
response.Fail(context, consts.CurdSelectFailCode, consts.CurdSelectFailMsg, "")
}
}
//4.用户新增(store)
func (u *Users) Store(context *gin.Context) {
userName := context.GetString(consts.ValidatorPrefix + "user_name")
pass := context.GetString(consts.ValidatorPrefix + "pass")
realName := context.GetString(consts.ValidatorPrefix + "real_name")
phone := context.GetString(consts.ValidatorPrefix + "phone")
remark := context.GetString(consts.ValidatorPrefix + "remark")
if curd.CreateUserCurdFactory().Store(userName, pass, realName, phone, remark) {
response.Success(context, consts.CurdStatusOkMsg, "")
} else {
response.Fail(context, consts.CurdCreatFailCode, consts.CurdCreatFailMsg, "")
}
}
//5.用户更新(update)
func (u *Users) Update(context *gin.Context) {
//表单参数验证中的int、int16、int32 、int64、float32、float64等数字键字段请统一使用 GetFloat64() 获取,其他函数无效
userId := context.GetFloat64(consts.ValidatorPrefix + "id")
userName := context.GetString(consts.ValidatorPrefix + "user_name")
pass := context.GetString(consts.ValidatorPrefix + "pass")
realName := context.GetString(consts.ValidatorPrefix + "real_name")
phone := context.GetString(consts.ValidatorPrefix + "phone")
remark := context.GetString(consts.ValidatorPrefix + "remark")
userIp := context.ClientIP()
// 检查正在修改的用户名是否被其他人使用
if model.CreateUserFactory("").UpdateDataCheckUserNameIsUsed(int(userId), userName) > 0 {
response.Fail(context, consts.CurdUpdateFailCode, consts.CurdUpdateFailMsg+", "+userName+" 已经被其他人使用", "")
return
}
//注意这里没有实现更加精细的权限控制逻辑例如超级管理管理员可以更新全部用户数据普通用户只能修改自己的数据。目前只是验证了token有效、合法之后就可以进行后续操作
// 实际使用请根据真是业务实现权限控制逻辑、再进行数据库操作
if curd.CreateUserCurdFactory().Update(int(userId), userName, pass, realName, phone, remark, userIp) {
response.Success(context, consts.CurdStatusOkMsg, "")
} else {
response.Fail(context, consts.CurdUpdateFailCode, consts.CurdUpdateFailMsg, "")
}
}
//6.删除记录
func (u *Users) Destroy(context *gin.Context) {
//表单参数验证中的int、int16、int32 、int64、float32、float64等数字键字段请统一使用 GetFloat64() 获取,其他函数无效
userId := context.GetFloat64(consts.ValidatorPrefix + "id")
if model.CreateUserFactory("").Destroy(int(userId)) {
response.Success(context, consts.CurdStatusOkMsg, "")
} else {
response.Fail(context, consts.CurdDeleteFailCode, consts.CurdDeleteFailMsg, "")
}
}

@ -0,0 +1,24 @@
package websocket
import (
"github.com/gin-gonic/gin"
serviceWs "goskeleton/app/service/websocket"
)
/**
websocket
https://github.com/gorilla/websocket/tree/master/examples
*/
type Ws struct {
}
// OnOpen 主要解决握手+协议升级
func (w *Ws) OnOpen(context *gin.Context) (*serviceWs.Ws, bool) {
return (&serviceWs.Ws{}).OnOpen(context)
}
// OnMessage 处理业务消息
func (w *Ws) OnMessage(serviceWs *serviceWs.Ws, context *gin.Context) {
serviceWs.OnMessage(context)
}

@ -0,0 +1,165 @@
package authorization
import (
"github.com/dchest/captcha"
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/global/variable"
userstoken "goskeleton/app/service/users/token"
"goskeleton/app/utils/response"
"strings"
)
type HeaderParams struct {
Authorization string `header:"Authorization" binding:"required,min=20"`
}
// CheckTokenAuth 检查token完整性、有效性中间件
func CheckTokenAuth() gin.HandlerFunc {
return func(context *gin.Context) {
headerParams := HeaderParams{}
// 推荐使用 ShouldBindHeader 方式获取头参数
if err := context.ShouldBindHeader(&headerParams); err != nil {
response.TokenErrorParam(context, consts.JwtTokenMustValid+err.Error())
return
}
token := strings.Split(headerParams.Authorization, " ")
if len(token) == 2 && len(token[1]) >= 20 {
tokenIsEffective := userstoken.CreateUserFactory().IsEffective(token[1])
if tokenIsEffective {
if customToken, err := userstoken.CreateUserFactory().ParseToken(token[1]); err == nil {
key := variable.ConfigYml.GetString("Token.BindContextKeyName")
// token验证通过同时绑定在请求上下文
context.Set(key, customToken)
}
context.Next()
} else {
response.ErrorTokenAuthFail(context)
}
} else {
response.ErrorTokenBaseInfo(context)
}
}
}
// CheckTokenAuthWithRefresh 检查token完整性、有效性并且自动刷新中间件
func CheckTokenAuthWithRefresh() gin.HandlerFunc {
return func(context *gin.Context) {
headerParams := HeaderParams{}
// 推荐使用 ShouldBindHeader 方式获取头参数
if err := context.ShouldBindHeader(&headerParams); err != nil {
response.TokenErrorParam(context, consts.JwtTokenMustValid+err.Error())
return
}
token := strings.Split(headerParams.Authorization, " ")
if len(token) == 2 && len(token[1]) >= 20 {
tokenIsEffective := userstoken.CreateUserFactory().IsEffective(token[1])
// 判断token是否有效
if tokenIsEffective {
if customToken, err := userstoken.CreateUserFactory().ParseToken(token[1]); err == nil {
key := variable.ConfigYml.GetString("Token.BindContextKeyName")
// token验证通过同时绑定在请求上下文
context.Set(key, customToken)
// 在自动刷新token的中间件中将请求的认证键、值原路返回与后续刷新逻辑格式保持一致
context.Header("Refresh-Token", "")
context.Header("Access-Control-Expose-Headers", "Refresh-Token")
}
context.Next()
} else {
// 判断token是否满足刷新条件
if userstoken.CreateUserFactory().TokenIsMeetRefreshCondition(token[1]) {
// 刷新token
if newToken, ok := userstoken.CreateUserFactory().RefreshToken(token[1], context.ClientIP()); ok {
if customToken, err := userstoken.CreateUserFactory().ParseToken(newToken); err == nil {
key := variable.ConfigYml.GetString("Token.BindContextKeyName")
// token刷新成功同时绑定在请求上下文
context.Set(key, customToken)
}
// 新token放入header返回
context.Header("Refresh-Token", newToken)
context.Header("Access-Control-Expose-Headers", "Refresh-Token")
context.Next()
} else {
response.ErrorTokenRefreshFail(context)
}
} else {
response.ErrorTokenRefreshFail(context)
}
}
} else {
response.ErrorTokenBaseInfo(context)
}
}
}
// RefreshTokenConditionCheck 刷新token条件检查中间件针对已经过期的token要求是token格式以及携带的信息满足配置参数即可
func RefreshTokenConditionCheck() gin.HandlerFunc {
return func(context *gin.Context) {
headerParams := HeaderParams{}
if err := context.ShouldBindHeader(&headerParams); err != nil {
response.TokenErrorParam(context, consts.JwtTokenMustValid+err.Error())
return
}
token := strings.Split(headerParams.Authorization, " ")
if len(token) == 2 && len(token[1]) >= 20 {
// 判断token是否满足刷新条件
if userstoken.CreateUserFactory().TokenIsMeetRefreshCondition(token[1]) {
context.Next()
} else {
response.ErrorTokenRefreshFail(context)
}
} else {
response.ErrorTokenBaseInfo(context)
}
}
}
// CheckCasbinAuth casbin检查用户对应的角色权限是否允许访问接口
func CheckCasbinAuth() gin.HandlerFunc {
return func(c *gin.Context) {
requstUrl := c.Request.URL.Path
method := c.Request.Method
// 模拟请求参数转换后的角色roleId=2
// 主线版本没有深度集成casbin的使用逻辑
// GinSkeleton-Admin 系统则深度集成了casbin接口权限管控
// 详细实现参考地址https://gitee.com/daitougege/gin-skeleton-admin-backend/blob/master/app/http/middleware/authorization/auth.go
role := "2" // 这里模拟某个用户的roleId=2
// 这里将用户的id解析为所拥有的的角色判断是否具有某个权限即可
isPass, err := variable.Enforcer.Enforce(role, requstUrl, method)
if err != nil {
response.ErrorCasbinAuthFail(c, err.Error())
return
} else if !isPass {
response.ErrorCasbinAuthFail(c, "")
return
} else {
c.Next()
}
}
}
// CheckCaptchaAuth 验证码中间件
func CheckCaptchaAuth() gin.HandlerFunc {
return func(c *gin.Context) {
captchaIdKey := variable.ConfigYml.GetString("Captcha.captchaId")
captchaValueKey := variable.ConfigYml.GetString("Captcha.captchaValue")
captchaId := c.PostForm(captchaIdKey)
value := c.PostForm(captchaValueKey)
if captchaId == "" || value == "" {
response.Fail(c, consts.CaptchaCheckParamsInvalidCode, consts.CaptchaCheckParamsInvalidMsg, "")
return
}
if captcha.VerifyString(captchaId, value) {
c.Next()
} else {
response.Fail(c, consts.CaptchaCheckFailCode, consts.CaptchaCheckFailMsg, "")
}
}
}

@ -0,0 +1,24 @@
package cors
import (
"github.com/gin-gonic/gin"
"net/http"
)
// 允许跨域
func Next() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Headers", "Access-Control-Allow-Headers,Authorization,User-Agent, Keep-Alive, Content-Type, X-Requested-With,X-CSRF-Token,AccessToken,Token")
c.Header("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, PATCH, OPTIONS")
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
c.Header("Access-Control-Allow-Credentials", "true")
// 放行所有OPTIONS方法
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusAccepted)
}
c.Next()
}
}

@ -0,0 +1,11 @@
package my_jwt
import "github.com/dgrijalva/jwt-go"
// 自定义jwt的声明字段信息+标准字段参考地址https://blog.csdn.net/codeSquare/article/details/99288718
type CustomClaims struct {
UserId int64 `json:"user_id"`
Name string `json:"user_name"`
Phone string `json:"phone"`
jwt.StandardClaims
}

@ -0,0 +1,73 @@
package my_jwt
import (
"errors"
"github.com/dgrijalva/jwt-go"
"goskeleton/app/global/my_errors"
"time"
)
// 使用工厂创建一个 JWT 结构体
func CreateMyJWT(signKey string) *JwtSign {
if len(signKey) <= 0 {
signKey = "goskeleton"
}
return &JwtSign{
[]byte(signKey),
}
}
// 定义一个 JWT验签 结构体
type JwtSign struct {
SigningKey []byte
}
// CreateToken 生成一个token
func (j *JwtSign) CreateToken(claims CustomClaims) (string, error) {
// 生成jwt格式的header、claims 部分
tokenPartA := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 继续添加秘钥值,生成最后一部分
return tokenPartA.SignedString(j.SigningKey)
}
// 解析Token
func (j *JwtSign) ParseToken(tokenString string) (*CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return j.SigningKey, nil
})
if token == nil {
return nil, errors.New(my_errors.ErrorsTokenInvalid)
}
if err != nil {
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
return nil, errors.New(my_errors.ErrorsTokenMalFormed)
} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
return nil, errors.New(my_errors.ErrorsTokenNotActiveYet)
} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
// 如果 TokenExpired ,只是过期(格式都正确),我们认为他是有效的,接下可以允许刷新操作
token.Valid = true
goto labelHere
} else {
return nil, errors.New(my_errors.ErrorsTokenInvalid)
}
}
}
labelHere:
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
} else {
return nil, errors.New(my_errors.ErrorsTokenInvalid)
}
}
// 更新token
func (j *JwtSign) RefreshToken(tokenString string, extraAddSeconds int64) (string, error) {
if CustomClaims, err := j.ParseToken(tokenString); err == nil {
CustomClaims.ExpiresAt = time.Now().Unix() + extraAddSeconds
return j.CreateToken(*CustomClaims)
} else {
return "", err
}
}

@ -0,0 +1,36 @@
package home
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/http/controller/api"
common_data_type "goskeleton/app/http/validator/common/data_type"
"goskeleton/app/http/validator/core/data_transfer"
"goskeleton/app/utils/response"
)
// 门户类前端接口模拟一个获取新闻的参数验证器
type News struct {
NewsType string `form:"newsType" json:"newsType" binding:"required,min=1"` // 验证规则必填最小长度为1
common_data_type.Page
}
func (n News) CheckParams(context *gin.Context) {
//1.先按照验证器提供的基本语法基本可以校验90%以上的不合格参数
if err := context.ShouldBind(&n); err != nil {
// 将表单参数验证器出现的错误直接交给错误翻译器统一处理即可
response.ValidatorError(context, err)
return
}
// 该函数主要是将绑定的数据以 键=>值 形式直接传递给下一步(控制器)
extraAddBindDataContext := data_transfer.DataAddContext(n, consts.ValidatorPrefix, context)
if extraAddBindDataContext == nil {
response.ErrorSystem(context, "HomeNews表单验证器json化失败", "")
} else {
// 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性
(&api.Home{}).News(extraAddBindDataContext)
}
}

@ -0,0 +1,6 @@
package data_type
type Page struct {
Page float64 `form:"page" json:"page" binding:"min=1"` // 必填,页面值>=1
Limit float64 `form:"limit" json:"limit" binding:"min=1"` // 必填,每页条数值>=1
}

@ -0,0 +1,20 @@
package register_validator
import (
"goskeleton/app/core/container"
"goskeleton/app/global/consts"
"goskeleton/app/http/validator/api/home"
)
// 各个业务模块验证器必须进行注册(初始化),程序启动时会自动加载到容器
func ApiRegisterValidator() {
//创建容器
containers := container.CreateContainersFactory()
// key 按照前缀+模块+验证动作 格式,将各个模块验证注册在容器
var key string
// 注册门户类表单参数验证器
key = consts.ValidatorPrefix + "HomeNews"
containers.Set(key, home.News{})
}

@ -0,0 +1,43 @@
package register_validator
import (
"goskeleton/app/core/container"
"goskeleton/app/global/consts"
"goskeleton/app/http/validator/common/upload_files"
"goskeleton/app/http/validator/common/websocket"
"goskeleton/app/http/validator/web/users"
)
// 各个业务模块验证器必须进行注册(初始化),程序启动时会自动加载到容器
func WebRegisterValidator() {
//创建容器
containers := container.CreateContainersFactory()
// key 按照前缀+模块+验证动作 格式,将各个模块验证注册在容器
var key string
// Users 模块表单验证器按照 key => value 形式注册在容器,方便路由模块中调用
key = consts.ValidatorPrefix + "UsersRegister"
containers.Set(key, users.Register{})
key = consts.ValidatorPrefix + "UsersLogin"
containers.Set(key, users.Login{})
key = consts.ValidatorPrefix + "RefreshToken"
containers.Set(key, users.RefreshToken{})
// Users基本操作CURD
key = consts.ValidatorPrefix + "UsersShow"
containers.Set(key, users.Show{})
key = consts.ValidatorPrefix + "UsersStore"
containers.Set(key, users.Store{})
key = consts.ValidatorPrefix + "UsersUpdate"
containers.Set(key, users.Update{})
key = consts.ValidatorPrefix + "UsersDestroy"
containers.Set(key, users.Destroy{})
// 文件上传
key = consts.ValidatorPrefix + "UploadFiles"
containers.Set(key, upload_files.UpFiles{})
// Websocket 连接验证器
key = consts.ValidatorPrefix + "WebsocketConnect"
containers.Set(key, websocket.Connect{})
}

@ -0,0 +1,58 @@
package upload_files
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/global/variable"
"goskeleton/app/http/controller/web"
"goskeleton/app/utils/files"
"goskeleton/app/utils/response"
"strconv"
"strings"
)
type UpFiles struct {
}
// 文件上传公共模块表单参数验证器
func (u UpFiles) CheckParams(context *gin.Context) {
tmpFile, err := context.FormFile(variable.ConfigYml.GetString("FileUploadSetting.UploadFileField")) // file 是一个文件结构体(文件对象)
var isPass bool
//获取文件发生错误,可能上传了空文件等
if err != nil {
response.Fail(context, consts.FilesUploadFailCode, consts.FilesUploadFailMsg, err.Error())
return
}
if tmpFile.Size == 0 {
response.Fail(context, consts.FilesUploadMoreThanMaxSizeCode, consts.FilesUploadIsEmpty, "")
return
}
//超过系统设定的最大值32MtmpFile.Size 的单位是 bytes 和我们定义的文件单位M 比较,就需要将我们的单位*1024*1024(即2的20次方),一步到位就是 << 20
sizeLimit := variable.ConfigYml.GetInt64("FileUploadSetting.Size")
if tmpFile.Size > sizeLimit<<20 {
response.Fail(context, consts.FilesUploadMoreThanMaxSizeCode, consts.FilesUploadMoreThanMaxSizeMsg+strconv.FormatInt(sizeLimit, 10)+"M", "")
return
}
//不允许的文件mime类型
if fp, err := tmpFile.Open(); err == nil {
mimeType := files.GetFilesMimeByFp(fp)
for _, value := range variable.ConfigYml.GetStringSlice("FileUploadSetting.AllowMimeType") {
if strings.ReplaceAll(value, " ", "") == strings.ReplaceAll(mimeType, " ", "") {
isPass = true
break
}
}
_ = fp.Close()
} else {
response.ErrorSystem(context, consts.ServerOccurredErrorMsg, "")
return
}
//凡是存在相等的类型,通过验证,调用控制器
if !isPass {
response.Fail(context, consts.FilesUploadMimeTypeFailCode, consts.FilesUploadMimeTypeFailMsg, "")
} else {
(&web.Upload{}).StartUpload(context)
}
}

@ -0,0 +1,43 @@
package websocket
import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"goskeleton/app/global/consts"
"goskeleton/app/global/variable"
controllerWs "goskeleton/app/http/controller/websocket"
"goskeleton/app/http/validator/core/data_transfer"
)
type Connect struct {
Token string `form:"token" json:"token" binding:"required,min=10"`
}
// 验证器语法,参见 Register.go文件有详细说明
// 注意websocket 连接建立之前如果有错误只能在服务端同构日志输出方式记录因为使用response.Fail等函数客户端是收不到任何信息的
func (c Connect) CheckParams(context *gin.Context) {
// 1. 首先检查是否开启websocket服务配置在配置项中开启
if variable.ConfigYml.GetInt("Websocket.Start") != 1 {
variable.ZapLog.Error(consts.WsServerNotStartMsg)
return
}
//2.基本的验证规则没有通过
if err := context.ShouldBind(&c); err != nil {
variable.ZapLog.Error("客户端上线参数不合格", zap.Error(err))
return
}
extraAddBindDataContext := data_transfer.DataAddContext(c, consts.ValidatorPrefix, context)
if extraAddBindDataContext == nil {
variable.ZapLog.Error("websocket-Connect 表单验证器json化失败")
context.Abort()
return
} else {
if serviceWs, ok := (&controllerWs.Ws{}).OnOpen(extraAddBindDataContext); ok == false {
variable.ZapLog.Error(consts.WsOpenFailMsg)
} else {
(&controllerWs.Ws{}).OnMessage(serviceWs, extraAddBindDataContext) // 注意这里传递的service_ws必须是调用open返回的必须保证的ws对象的一致性
}
}
}

@ -0,0 +1,37 @@
package data_transfer
import (
"encoding/json"
"github.com/gin-gonic/gin"
"goskeleton/app/global/variable"
"goskeleton/app/http/validator/core/interf"
"time"
)
// 将验证器成员(字段)绑定到数据传输上下文,方便控制器获取
/**
validatorInterface
extra_add_data_prefix
context gin
*/
func DataAddContext(validatorInterface interf.ValidatorInterface, extraAddDataPrefix string, context *gin.Context) *gin.Context {
var tempJson interface{}
if tmpBytes, err1 := json.Marshal(validatorInterface); err1 == nil {
if err2 := json.Unmarshal(tmpBytes, &tempJson); err2 == nil {
if value, ok := tempJson.(map[string]interface{}); ok {
for k, v := range value {
context.Set(extraAddDataPrefix+k, v)
}
// 此外给上下文追加三个键created_at 、 updated_at 、 deleted_at ,实际根据需要自己选择获取相关键值
curDateTime := time.Now().Format(variable.DateFormat)
context.Set(extraAddDataPrefix+"created_at", curDateTime)
context.Set(extraAddDataPrefix+"updated_at", curDateTime)
context.Set(extraAddDataPrefix+"deleted_at", curDateTime)
return context
}
}
}
return nil
}

@ -0,0 +1,21 @@
package factory
import (
"github.com/gin-gonic/gin"
"goskeleton/app/core/container"
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"goskeleton/app/http/validator/core/interf"
)
// 表单参数验证器工厂(请勿修改)
func Create(key string) func(context *gin.Context) {
if value := container.CreateContainersFactory().Get(key); value != nil {
if val, isOk := value.(interf.ValidatorInterface); isOk {
return val.CheckParams
}
}
variable.ZapLog.Error(my_errors.ErrorsValidatorNotExists + ", 验证器模块:" + key)
return nil
}

@ -0,0 +1,8 @@
package interf
import "github.com/gin-gonic/gin"
// 验证器接口,每个验证器必须实现该接口,请勿修改
type ValidatorInterface interface {
CheckParams(context *gin.Context)
}

@ -0,0 +1,10 @@
package users
type BaseField struct {
UserName string `form:"user_name" json:"user_name" binding:"required,min=1"` // 必填、对于文本,表示它的长度>=1
Pass string `form:"pass" json:"pass" binding:"required,min=6,max=20"` // 密码为 必填,长度>=6
}
type Id struct {
Id float64 `form:"id" json:"id" binding:"required,min=1"`
}

@ -0,0 +1,48 @@
package users
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/http/controller/web"
"goskeleton/app/http/validator/core/data_transfer"
"goskeleton/app/utils/response"
)
type Destroy struct {
// 表单参数验证结构体支持匿名结构体嵌套、以及匿名结构体与普通字段组合
Id
}
// 验证器语法,参见 Register.go文件有详细说明
func (d Destroy) CheckParams(context *gin.Context) {
if err := context.ShouldBind(&d); err != nil {
// 将表单参数验证器出现的错误直接交给错误翻译器统一处理即可
response.ValidatorError(context, err)
return
}
// 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值
extraAddBindDataContext := data_transfer.DataAddContext(d, consts.ValidatorPrefix, context)
if extraAddBindDataContext == nil {
response.ErrorSystem(context, "UserShow表单参数验证器json化失败", "")
return
} else {
// 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性
(&web.Users{}).Destroy(extraAddBindDataContext)
// 以下代码为模拟 前置、后置函数的回调代码
/*
func(before_callback_fn func(context *gin.Context) bool, after_callback_fn func(context *gin.Context)) {
if before_callback_fn(extraAddBindDataContext) {
defer after_callback_fn(extraAddBindDataContext)
(&Web.Users{}).Destroy(extraAddBindDataContext)
} else {
// 这里编写前置函数验证不通过的相关返回提示逻辑...
}
}((&Users.DestroyBefore{}).Before, (&Users.DestroyAfter{}).After)
*/
}
}

@ -0,0 +1,35 @@
package users
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/http/controller/web"
"goskeleton/app/http/validator/core/data_transfer"
"goskeleton/app/utils/response"
)
type Login struct {
// 表单参数验证结构体支持匿名结构体嵌套
BaseField
}
// 验证器语法,参见 Register.go文件有详细说明
func (l Login) CheckParams(context *gin.Context) {
//1.基本的验证规则没有通过
if err := context.ShouldBind(&l); err != nil {
response.ValidatorError(context, err)
return
}
// 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值
extraAddBindDataContext := data_transfer.DataAddContext(l, consts.ValidatorPrefix, context)
if extraAddBindDataContext == nil {
response.ErrorSystem(context, "userLogin表单验证器json化失败", "")
} else {
// 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性
(&web.Users{}).Login(extraAddBindDataContext)
}
}

@ -0,0 +1,36 @@
package users
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/http/controller/web"
"goskeleton/app/utils/response"
"strings"
)
type RefreshToken struct {
Authorization string `json:"token" header:"Authorization" binding:"required,min=20"`
}
// 验证器语法,参见 Register.go文件有详细说明
func (r RefreshToken) CheckParams(context *gin.Context) {
//1.基本的验证规则没有通过
if err := context.ShouldBindHeader(&r); err != nil {
// 将表单参数验证器出现的错误直接交给错误翻译器统一处理即可
response.ValidatorError(context, err)
return
}
token := strings.Split(r.Authorization, " ")
if len(token) == 2 {
context.Set(consts.ValidatorPrefix+"token", token[1])
(&web.Users{}).RefreshToken(context)
} else {
errs := gin.H{
"tips": "Token不合法token请放置在header头部分按照按=>键提交例如AuthorizationBearer 你的实际token....",
}
response.Fail(context, consts.JwtTokenFormatErrCode, consts.JwtTokenFormatErrMsg, errs)
}
}

@ -0,0 +1,58 @@
package users
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/http/controller/web"
"goskeleton/app/http/validator/core/data_transfer"
"goskeleton/app/utils/response"
)
// 验证器是本项目骨架的先锋队,必须发挥它的极致优势,具体参考地址:
//https://godoc.org/github.com/go-playground/validator ,该验证器非常强大,强烈建议重点发挥,
//请求正式进入控制器等后面的业务逻辑层之前,参数的校验必须在验证器层完成,后面的控制器等就只管获取各种参数,代码一把梭
// 给出一些最常用的验证规则:
//required 必填;
//len=11 长度=11
//min=3 如果是数字验证的是数据范围最小值为3如果是文本验证的是最小长度为3
//max=6 如果是数字验证的是数字最大值为6如果是文本验证的是最大长度为6
// mail 验证邮箱
//gt=3 对于文本就是长度>=3
//lt=6 对于文本就是长度<=6
type Register struct {
BaseField
// 表单参数验证结构体支持匿名结构体嵌套、以及匿名结构体与普通字段组合
Phone string `form:"phone" json:"phone"` // 手机号, 非必填
CardNo string `form:"card_no" json:"card_no"` //身份证号码,非必填
}
// 特别注意: 表单参数验证器结构体的函数,绝对不能绑定在指针上
// 我们这部分代码项目启动后会加载到容器,如果绑定在指针,一次请求之后,会造成容器中的代码段被污染
func (r Register) CheckParams(context *gin.Context) {
//1.先按照验证器提供的基本语法基本可以校验90%以上的不合格参数
if err := context.ShouldBind(&r); err != nil {
response.ValidatorError(context, err)
return
}
//2.继续验证具有中国特色的参数,例如 身份证号码等基本语法校验了长度18位然后可以自行编写正则表达式等更进一步验证每一部分组成
// r.CardNo 获取身份证号码继续校验,可能需要开发者编写正则表达式,稍微复杂,这里忽略
// r.Phone 获取手机号码,可以根据手机号码开头等等自定义验证,例如 如果不是以138 开头的手机号码,则报错
//if !strings.HasPrefix(r.CardNo, "138") {
// response.ErrorParam(context, gin.H{"tips": "手机号码字段card_no 必须以138开头"})
// return
//}
// 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值
extraAddBindDataContext := data_transfer.DataAddContext(r, consts.ValidatorPrefix, context)
if extraAddBindDataContext == nil {
response.ErrorSystem(context, "UserRegister表单验证器json化失败", "")
} else {
// 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性
(&web.Users{}).Register(extraAddBindDataContext)
}
}

@ -0,0 +1,35 @@
package users
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/http/controller/web"
common_data_type "goskeleton/app/http/validator/common/data_type"
"goskeleton/app/http/validator/core/data_transfer"
"goskeleton/app/utils/response"
)
type Show struct {
// 表单参数验证结构体支持匿名结构体嵌套
UserName string `form:"user_name" json:"user_name" binding:"required,min=1"` // 必填、对于文本,表示它的长度>=1
common_data_type.Page
}
// 验证器语法,参见 Register.go文件有详细说明
func (s Show) CheckParams(context *gin.Context) {
//1.基本的验证规则没有通过
if err := context.ShouldBind(&s); err != nil {
// 将表单参数验证器出现的错误直接交给错误翻译器统一处理即可
response.ValidatorError(context, err)
return
}
// 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值
extraAddBindDataContext := data_transfer.DataAddContext(s, consts.ValidatorPrefix, context)
if extraAddBindDataContext == nil {
response.ErrorSystem(context, "UserShow表单验证器json化失败", "")
} else {
// 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性
(&web.Users{}).Show(extraAddBindDataContext)
}
}

@ -0,0 +1,37 @@
package users
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/http/controller/web"
"goskeleton/app/http/validator/core/data_transfer"
"goskeleton/app/utils/response"
)
type Store struct {
BaseField
// 表单参数验证结构体支持匿名结构体嵌套、以及匿名结构体与普通字段组合
RealName string `form:"real_name" json:"real_name" binding:"required,min=2"`
Phone string `form:"phone" json:"phone" binding:"required,len=11"`
Remark string `form:"remark" json:"remark" `
}
// 验证器语法,参见 Register.go文件有详细说明
func (s Store) CheckParams(context *gin.Context) {
//1.基本的验证规则没有通过
if err := context.ShouldBind(&s); err != nil {
// 将表单参数验证器出现的错误直接交给错误翻译器统一处理即可
response.ValidatorError(context, err)
return
}
// 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值
extraAddBindDataContext := data_transfer.DataAddContext(s, consts.ValidatorPrefix, context)
if extraAddBindDataContext == nil {
response.ErrorSystem(context, "UserStore表单验证器json化失败", "")
} else {
// 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性
(&web.Users{}).Store(extraAddBindDataContext)
}
}

@ -0,0 +1,38 @@
package users
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"goskeleton/app/http/controller/web"
"goskeleton/app/http/validator/core/data_transfer"
"goskeleton/app/utils/response"
)
type Update struct {
BaseField
Id
// 表单参数验证结构体支持匿名结构体嵌套、以及匿名结构体与普通字段组合
RealName string `form:"real_name" json:"real_name" binding:"required,min=2"`
Phone string `form:"phone" json:"phone" binding:"required,len=11"`
Remark string `form:"remark" json:"remark"`
}
// 验证器语法,参见 Register.go文件有详细说明
func (u Update) CheckParams(context *gin.Context) {
//1.基本的验证规则没有通过
if err := context.ShouldBind(&u); err != nil {
// 将表单参数验证器出现的错误直接交给错误翻译器统一处理即可
response.ValidatorError(context, err)
return
}
// 该函数主要是将本结构体的字段(成员)按照 consts.ValidatorPrefix+ json标签对应的 键 => 值 形式绑定在上下文,便于下一步(控制器)可以直接通过 context.Get(键) 获取相关值
extraAddBindDataContext := data_transfer.DataAddContext(u, consts.ValidatorPrefix, context)
if extraAddBindDataContext == nil {
response.ErrorSystem(context, "UserUpdate表单验证器json化失败", "")
} else {
// 验证完成,调用控制器,并将验证器成员(字段)递给控制器,保持上下文数据一致性
(&web.Users{}).Update(extraAddBindDataContext)
}
}

@ -0,0 +1,71 @@
package model
import (
"fmt"
"gorm.io/gorm"
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"strings"
)
type BaseModel struct {
*gorm.DB `gorm:"-" json:"-"`
Id int64 `gorm:"primaryKey" json:"id"`
CreatedAt string `json:"created_at"` //日期时间字段统一设置为字符串即可
UpdatedAt string `json:"updated_at"`
//DeletedAt gorm.DeletedAt `json:"deleted_at"` // 如果开发者需要使用软删除功能打开本行注释掉的代码即可同时需要在数据库的所有表增加字段deleted_at 类型为 datetime
}
func UseDbConn(sqlType string) *gorm.DB {
var db *gorm.DB
sqlType = strings.Trim(sqlType, " ")
if sqlType == "" {
sqlType = variable.ConfigGormv2Yml.GetString("Gormv2.UseDbType")
}
switch strings.ToLower(sqlType) {
case "mysql":
if variable.GormDbMysql == nil {
variable.ZapLog.Fatal(fmt.Sprintf(my_errors.ErrorsGormNotInitGlobalPointer, sqlType, sqlType))
}
db = variable.GormDbMysql
case "sqlserver":
if variable.GormDbSqlserver == nil {
variable.ZapLog.Fatal(fmt.Sprintf(my_errors.ErrorsGormNotInitGlobalPointer, sqlType, sqlType))
}
db = variable.GormDbSqlserver
case "postgres", "postgre", "postgresql":
if variable.GormDbPostgreSql == nil {
variable.ZapLog.Fatal(fmt.Sprintf(my_errors.ErrorsGormNotInitGlobalPointer, sqlType, sqlType))
}
db = variable.GormDbPostgreSql
default:
variable.ZapLog.Error(my_errors.ErrorsDbDriverNotExists + sqlType)
}
return db
}
// 在 ginskeleton项目中如果在业务 model 设置了回调函数,请看以下说明
// 注意gorm 的自动回调函数BeforeCreate、BeforeUpdate 等),不是由本项目的 Create ... 函数先初始化然后调用的而是gorm自动直接调用的
// 所以 接收器 b 的所有参数都是没有赋值的,因此这里需要给 b.DB 赋予回调的 gormDb
// baseModel 的代码执行顺序晚于其他业务 model 的回调函数如果回调函数名称相同会被普通业务model的同名回调函数覆盖
// gorm 支持的自动回调函数清单https://github.com/go-gorm/gorm/blob/master/callbacks/interfaces.go
//func (b *BaseModel) BeforeCreate(gormDB *gorm.DB) error {
// 第一步必须反向将 gormDB 赋值给 b.DB
// b.DB = gormDB
// 后续的代码就可以像普通业务 model 一样操作,
// b.Exec(sql,参数1参数2...)
// b.Raw(sql,参数1参数2...)
// return nil
//}
// BeforeUpdate、BeforeSave 函数都会因为 更新类的操作而被触发
// 如果baseModel 和 普通业务 model 都想使用回调函数,那么请设置不同的回调函数名,例如:这里设置 BeforeUpdate、普通业务model 设置 BeforeSave 即可
//func (b *BaseModel) BeforeUpdate(gormDB *gorm.DB) error {
// 第一步必须反向将 gormDB 赋值给 b.DB
// b.DB = gormDB
// 后续的代码就可以像普通业务 model 一样操作,
// b.Exec(sql,参数1参数2...)
// b.Raw(sql,参数1参数2...)
// return nil
//}

@ -0,0 +1,301 @@
package model
import (
"go.uber.org/zap"
"goskeleton/app/global/variable"
"goskeleton/app/service/users/token_cache_redis"
"goskeleton/app/utils/md5_encrypt"
"time"
)
// 操作数据库喜欢使用gorm自带语法的开发者可以参考 GinSkeleton-Admin 系统相关代码
// Admin 项目地址https://gitee.com/daitougege/gin-skeleton-admin-backend/
// gorm_v2 提供的语法+ ginskeleton 实践 http://gitee.com/daitougege/gin-skeleton-admin-backend/blob/master/app/model/button_cn_en.go
// 创建 userFactory
// 参数说明: 传递空值,默认使用 配置文件选项UseDbTypemysql
func CreateUserFactory(sqlType string) *UsersModel {
return &UsersModel{BaseModel: BaseModel{DB: UseDbConn(sqlType)}}
}
type UsersModel struct {
BaseModel
UserName string `gorm:"column:user_name" json:"user_name"`
Pass string `json:"-"`
Phone string `json:"phone"`
RealName string `gorm:"column:real_name" json:"real_name"`
Status int `json:"status"`
Token string `json:"token"`
LastLoginIp string `gorm:"column:last_login_ip" json:"last_login_ip"`
}
// 表名
func (u *UsersModel) TableName() string {
return "tb_users"
}
// 用户注册(写一个最简单的使用账号、密码注册即可)
func (u *UsersModel) Register(userName, pass, userIp string) bool {
sql := "INSERT INTO tb_users(user_name,pass,last_login_ip) SELECT ?,?,? FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM tb_users WHERE user_name=?)"
result := u.Exec(sql, userName, pass, userIp, userName)
if result.RowsAffected > 0 {
return true
} else {
return false
}
}
// 用户登录,
func (u *UsersModel) Login(userName string, pass string) *UsersModel {
sql := "select id, user_name,real_name,pass,phone from tb_users where user_name=? limit 1"
result := u.Raw(sql, userName).First(u)
if result.Error == nil {
// 账号密码验证成功
if len(u.Pass) > 0 && (u.Pass == md5_encrypt.Base64Md5(pass)) {
return u
}
} else {
variable.ZapLog.Error("根据账号查询单条记录出错:", zap.Error(result.Error))
}
return nil
}
//记录用户登陆login生成的token每次登陆记录一次token
func (u *UsersModel) OauthLoginToken(userId int64, token string, expiresAt int64, clientIp string) bool {
sql := `
INSERT INTO tb_oauth_access_tokens(fr_user_id,action_name,token,expires_at,client_ip)
SELECT ?,'login',? ,?,? FROM DUAL WHERE NOT EXISTS(SELECT 1 FROM tb_oauth_access_tokens a WHERE a.fr_user_id=? AND a.action_name='login' AND a.token=? )
`
//注意token的精确度为秒如果在一秒之内一个账号多次调用接口生成的token其实是相同的这样写入数据库第二次的影响行数为0知己实际上操作仍然是有效的。
//所以这里只判断无错误即可,判断影响行数的话,>=0 都是ok的
if u.Exec(sql, userId, token, time.Unix(expiresAt, 0).Format(variable.DateFormat), clientIp, userId, token).Error == nil {
// 异步缓存用户有效的token到redis
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.ValidTokenCacheToRedis(userId)
}
return true
}
return false
}
//用户刷新token,条件检查: 相关token在过期的时间之内就符合刷新条件
func (u *UsersModel) OauthRefreshConditionCheck(userId int64, oldToken string) bool {
// 首先判断旧token在本系统自带的数据库已经存在才允许继续执行刷新逻辑
var oldTokenIsExists int
sql := "SELECT count(*) as counts FROM tb_oauth_access_tokens WHERE fr_user_id =? and token=? and NOW()<DATE_ADD(expires_at,INTERVAL ? SECOND)"
if u.Raw(sql, userId, oldToken, variable.ConfigYml.GetInt64("Token.JwtTokenRefreshAllowSec")).First(&oldTokenIsExists).Error == nil && oldTokenIsExists == 1 {
return true
}
return false
}
//用户刷新token
func (u *UsersModel) OauthRefreshToken(userId, expiresAt int64, oldToken, newToken, clientIp string) bool {
sql := "UPDATE tb_oauth_access_tokens SET token=? ,expires_at=?,client_ip=?,updated_at=NOW(),action_name='refresh' WHERE fr_user_id=? AND token=?"
if u.Exec(sql, newToken, time.Unix(expiresAt, 0).Format(variable.DateFormat), clientIp, userId, oldToken).Error == nil {
// 异步缓存用户有效的token到redis
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.ValidTokenCacheToRedis(userId)
}
go u.UpdateUserloginInfo(clientIp, userId)
return true
}
return false
}
// 更新用户登陆次数、最近一次登录ip、最近一次登录时间
func (u *UsersModel) UpdateUserloginInfo(last_login_ip string, userId int64) {
sql := "UPDATE tb_users SET login_times=IFNULL(login_times,0)+1,last_login_ip=?,last_login_time=? WHERE id=? "
_ = u.Exec(sql, last_login_ip, time.Now().Format(variable.DateFormat), userId)
}
//当用户更改密码后所有的token都失效必须重新登录
func (u *UsersModel) OauthResetToken(userId int, newPass, clientIp string) bool {
//如果用户新旧密码一致直接返回true不需要处理
userItem, err := u.ShowOneItem(userId)
if userItem != nil && err == nil && userItem.Pass == newPass {
return true
} else if userItem != nil {
// 如果用户密码被修改那么redis中的token值也清除
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.DelTokenCacheFromRedis(int64(userId))
}
sql := "UPDATE tb_oauth_access_tokens SET revoked=1,updated_at=NOW(),action_name='ResetPass',client_ip=? WHERE fr_user_id=? "
if u.Exec(sql, clientIp, userId).Error == nil {
return true
}
}
return false
}
//当tb_users 删除数据相关的token同步删除
func (u *UsersModel) OauthDestroyToken(userId int) bool {
//如果用户新旧密码一致直接返回true不需要处理
sql := "DELETE FROM tb_oauth_access_tokens WHERE fr_user_id=? "
//判断>=0, 有些没有登录过的用户没有相关token此语句执行影响行数为0但是仍然是执行成功
if u.Exec(sql, userId).Error == nil {
return true
}
return false
}
// 判断用户token是否在数据库存在+状态OK
func (u *UsersModel) OauthCheckTokenIsOk(userId int64, token string) bool {
sql := "SELECT token FROM `tb_oauth_access_tokens` WHERE fr_user_id=? AND revoked=0 AND expires_at>NOW() ORDER BY expires_at DESC , updated_at DESC LIMIT ?"
maxOnlineUsers := variable.ConfigYml.GetInt("Token.JwtTokenOnlineUsers")
rows, err := u.Raw(sql, userId, maxOnlineUsers).Rows()
defer func() {
// 凡是查询类记得释放记录集
_ = rows.Close()
}()
if err == nil && rows != nil {
for rows.Next() {
var tempToken string
err := rows.Scan(&tempToken)
if err == nil {
if tempToken == token {
return true
}
}
}
}
return false
}
// 禁用一个用户的: 1.tb_users表的 status 设置为 0tb_oauth_access_tokens 表的所有token删除
// 禁用一个用户的token请求本质上就是把tb_users表的 status 字段设置为 0 即可)
func (u *UsersModel) SetTokenInvalid(userId int) bool {
sql := "delete from `tb_oauth_access_tokens` where `fr_user_id`=? "
if u.Exec(sql, userId).Error == nil {
if u.Exec("update tb_users set status=0 where id=?", userId).Error == nil {
return true
}
}
return false
}
//根据用户ID查询一条信息
func (u *UsersModel) ShowOneItem(userId int) (*UsersModel, error) {
sql := "SELECT `id`, `user_name`,`pass`, `real_name`, `phone`, `status` FROM `tb_users` WHERE `status`=1 and id=? LIMIT 1"
result := u.Raw(sql, userId).First(u)
if result.Error == nil {
return u, nil
} else {
return nil, result.Error
}
}
// 查询数据之前统计条数
func (u *UsersModel) counts(userName string) (counts int64) {
sql := "SELECT count(*) as counts FROM tb_users WHERE status=1 and user_name like ?"
if res := u.Raw(sql, "%"+userName+"%").First(&counts); res.Error != nil {
variable.ZapLog.Error("UsersModel - counts 查询数据条数出错", zap.Error(res.Error))
}
return counts
}
// 查询(根据关键词模糊查询)
func (u *UsersModel) Show(userName string, limitStart, limitItems int) (counts int64, temp []UsersModel) {
if counts = u.counts(userName); counts > 0 {
sql := "SELECT `id`, `user_name`, `real_name`, `phone`,last_login_ip, `status`,created_at,updated_at FROM `tb_users` WHERE `status`=1 and user_name like ? LIMIT ?,?"
if res := u.Raw(sql, "%"+userName+"%", limitStart, limitItems).Find(&temp); res.RowsAffected > 0 {
return counts, temp
}
}
return 0, nil
}
//新增
func (u *UsersModel) Store(userName string, pass string, realName string, phone string, remark string) bool {
sql := "INSERT INTO tb_users(user_name,pass,real_name,phone,remark) SELECT ?,?,?,?,? FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM tb_users WHERE user_name=?)"
if u.Exec(sql, userName, pass, realName, phone, remark, userName).RowsAffected > 0 {
return true
}
return false
}
//UpdateDataCheckUserNameIsUsed 更新前检查新的用户名是否已经存在(避免和别的账号重名)
func (u *UsersModel) UpdateDataCheckUserNameIsUsed(userId int, userName string) (exists int64) {
sql := "select count(*) as counts from tb_users where id!=? AND user_name=?"
_ = u.Raw(sql, userId, userName).First(&exists)
return exists
}
//更新
func (u *UsersModel) Update(id int, userName string, pass string, realName string, phone string, remark string, clientIp string) bool {
sql := "update tb_users set user_name=?,pass=?,real_name=?,phone=?,remark=? WHERE status=1 AND id=?"
if u.Exec(sql, userName, pass, realName, phone, remark, id).RowsAffected >= 0 {
if u.OauthResetToken(id, pass, clientIp) {
return true
}
}
return false
}
//删除用户以及关联的token记录
func (u *UsersModel) Destroy(id int) bool {
// 删除用户时清除用户缓存在redis的全部token
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.DelTokenCacheFromRedis(int64(id))
}
if u.Delete(u, id).Error == nil {
if u.OauthDestroyToken(id) {
return true
}
}
return false
}
// 后续两个函数专门处理用户 token 缓存到 redis 逻辑
func (u *UsersModel) ValidTokenCacheToRedis(userId int64) {
tokenCacheRedisFact := token_cache_redis.CreateUsersTokenCacheFactory(userId)
if tokenCacheRedisFact == nil {
variable.ZapLog.Error("redis连接失败请检查配置")
return
}
defer tokenCacheRedisFact.ReleaseRedisConn()
sql := "SELECT token,expires_at FROM `tb_oauth_access_tokens` WHERE fr_user_id=? AND revoked=0 AND expires_at>NOW() ORDER BY expires_at DESC , updated_at DESC LIMIT ?"
maxOnlineUsers := variable.ConfigYml.GetInt("Token.JwtTokenOnlineUsers")
rows, err := u.Raw(sql, userId, maxOnlineUsers).Rows()
defer func() {
// 凡是获取原生结果集的查询,记得释放记录集
_ = rows.Close()
}()
var tempToken, expires string
if err == nil && rows != nil {
for i := 1; rows.Next(); i++ {
err = rows.Scan(&tempToken, &expires)
if err == nil {
if ts, err := time.ParseInLocation(variable.DateFormat, expires, time.Local); err == nil {
tokenCacheRedisFact.SetTokenCache(ts.Unix(), tempToken)
// 因为每个用户的token是按照过期时间倒叙排列的第一个是有效期最长的将该用户的总键设置一个最大过期时间到期则自动清理避免不必要的数据残留
if i == 1 {
tokenCacheRedisFact.SetUserTokenExpire(ts.Unix())
}
} else {
variable.ZapLog.Error("expires_at 转换位时间戳出错", zap.Error(err))
}
}
}
}
// 缓存结束之后删除超过系统设置最大在线数量的token
tokenCacheRedisFact.DelOverMaxOnlineCache()
}
// DelTokenCacheFromRedis 用户密码修改后删除redis所有的token
func (u *UsersModel) DelTokenCacheFromRedis(userId int64) {
tokenCacheRedisFact := token_cache_redis.CreateUsersTokenCacheFactory(userId)
if tokenCacheRedisFact == nil {
variable.ZapLog.Error("redis连接失败请检查配置")
return
}
tokenCacheRedisFact.ClearUserToken()
tokenCacheRedisFact.ReleaseRedisConn()
}

@ -0,0 +1,301 @@
package model
import (
"go.uber.org/zap"
"goskeleton/app/global/variable"
"goskeleton/app/service/users/token_cache_redis"
"goskeleton/app/utils/md5_encrypt"
"time"
)
// 操作数据库喜欢使用gorm自带语法的开发者可以参考 GinSkeleton-Admin 系统相关代码
// Admin 项目地址https://gitee.com/daitougege/gin-skeleton-admin-backend/
// gorm_v2 提供的语法+ ginskeleton 实践 http://gitee.com/daitougege/gin-skeleton-admin-backend/blob/master/app/model/button_cn_en.go
// 创建 userFactory
// 参数说明: 传递空值,默认使用 配置文件选项UseDbTypemysql
func CreateUserFactory(sqlType string) *UsersModel {
return &UsersModel{BaseModel: BaseModel{DB: UseDbConn(sqlType)}}
}
type UsersModel struct {
BaseModel
UserName string `gorm:"column:user_name" json:"user_name"`
Pass string `json:"-"`
Phone string `json:"phone"`
RealName string `gorm:"column:real_name" json:"real_name"`
Status int `json:"status"`
Token string `json:"token"`
LastLoginIp string `gorm:"column:last_login_ip" json:"last_login_ip"`
}
// 表名
func (u *UsersModel) TableName() string {
return "tb_users"
}
// 用户注册(写一个最简单的使用账号、密码注册即可)
func (u *UsersModel) Register(userName, pass, userIp string) bool {
sql := "INSERT INTO tb_users(user_name,pass,last_login_ip) SELECT ?,?,? FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM tb_users WHERE user_name=?)"
result := u.Exec(sql, userName, pass, userIp, userName)
if result.RowsAffected > 0 {
return true
} else {
return false
}
}
// 用户登录,
func (u *UsersModel) Login(userName string, pass string) *UsersModel {
sql := "select id, user_name,real_name,pass,phone from tb_users where user_name=? limit 1"
result := u.Raw(sql, userName).First(u)
if result.Error == nil {
// 账号密码验证成功
if len(u.Pass) > 0 && (u.Pass == md5_encrypt.Base64Md5(pass)) {
return u
}
} else {
variable.ZapLog.Error("根据账号查询单条记录出错:", zap.Error(result.Error))
}
return nil
}
//记录用户登陆login生成的token每次登陆记录一次token
func (u *UsersModel) OauthLoginToken(userId int64, token string, expiresAt int64, clientIp string) bool {
sql := `
INSERT INTO tb_oauth_access_tokens(fr_user_id,action_name,token,expires_at,client_ip)
SELECT ?,'login',? ,?,? FROM DUAL WHERE NOT EXISTS(SELECT 1 FROM tb_oauth_access_tokens a WHERE a.fr_user_id=? AND a.action_name='login' AND a.token=? )
`
//注意token的精确度为秒如果在一秒之内一个账号多次调用接口生成的token其实是相同的这样写入数据库第二次的影响行数为0知己实际上操作仍然是有效的。
//所以这里只判断无错误即可,判断影响行数的话,>=0 都是ok的
if u.Exec(sql, userId, token, time.Unix(expiresAt, 0).Format(variable.DateFormat), clientIp, userId, token).Error == nil {
// 异步缓存用户有效的token到redis
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.ValidTokenCacheToRedis(userId)
}
return true
}
return false
}
//用户刷新token,条件检查: 相关token在过期的时间之内就符合刷新条件
func (u *UsersModel) OauthRefreshConditionCheck(userId int64, oldToken string) bool {
// 首先判断旧token在本系统自带的数据库已经存在才允许继续执行刷新逻辑
var oldTokenIsExists int
sql := "SELECT count(*) as counts FROM tb_oauth_access_tokens WHERE fr_user_id =? and token=? and NOW()<DATE_ADD(expires_at,INTERVAL ? SECOND)"
if u.Raw(sql, userId, oldToken, variable.ConfigYml.GetInt64("Token.JwtTokenRefreshAllowSec")).First(&oldTokenIsExists).Error == nil && oldTokenIsExists == 1 {
return true
}
return false
}
//用户刷新token
func (u *UsersModel) OauthRefreshToken(userId, expiresAt int64, oldToken, newToken, clientIp string) bool {
sql := "UPDATE tb_oauth_access_tokens SET token=? ,expires_at=?,client_ip=?,updated_at=NOW(),action_name='refresh' WHERE fr_user_id=? AND token=?"
if u.Exec(sql, newToken, time.Unix(expiresAt, 0).Format(variable.DateFormat), clientIp, userId, oldToken).Error == nil {
// 异步缓存用户有效的token到redis
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.ValidTokenCacheToRedis(userId)
}
go u.UpdateUserloginInfo(clientIp, userId)
return true
}
return false
}
// 更新用户登陆次数、最近一次登录ip、最近一次登录时间
func (u *UsersModel) UpdateUserloginInfo(last_login_ip string, userId int64) {
sql := "UPDATE tb_users SET login_times=IFNULL(login_times,0)+1,last_login_ip=?,last_login_time=? WHERE id=? "
_ = u.Exec(sql, last_login_ip, time.Now().Format(variable.DateFormat), userId)
}
//当用户更改密码后所有的token都失效必须重新登录
func (u *UsersModel) OauthResetToken(userId int, newPass, clientIp string) bool {
//如果用户新旧密码一致直接返回true不需要处理
userItem, err := u.ShowOneItem(userId)
if userItem != nil && err == nil && userItem.Pass == newPass {
return true
} else if userItem != nil {
// 如果用户密码被修改那么redis中的token值也清除
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.DelTokenCacheFromRedis(int64(userId))
}
sql := "UPDATE tb_oauth_access_tokens SET revoked=1,updated_at=NOW(),action_name='ResetPass',client_ip=? WHERE fr_user_id=? "
if u.Exec(sql, clientIp, userId).Error == nil {
return true
}
}
return false
}
//当tb_users 删除数据相关的token同步删除
func (u *UsersModel) OauthDestroyToken(userId int) bool {
//如果用户新旧密码一致直接返回true不需要处理
sql := "DELETE FROM tb_oauth_access_tokens WHERE fr_user_id=? "
//判断>=0, 有些没有登录过的用户没有相关token此语句执行影响行数为0但是仍然是执行成功
if u.Exec(sql, userId).Error == nil {
return true
}
return false
}
// 判断用户token是否在数据库存在+状态OK
func (u *UsersModel) OauthCheckTokenIsOk(userId int64, token string) bool {
sql := "SELECT token FROM `tb_oauth_access_tokens` WHERE fr_user_id=? AND revoked=0 AND expires_at>NOW() ORDER BY expires_at DESC , updated_at DESC LIMIT ?"
maxOnlineUsers := variable.ConfigYml.GetInt("Token.JwtTokenOnlineUsers")
rows, err := u.Raw(sql, userId, maxOnlineUsers).Rows()
defer func() {
// 凡是查询类记得释放记录集
_ = rows.Close()
}()
if err == nil && rows != nil {
for rows.Next() {
var tempToken string
err := rows.Scan(&tempToken)
if err == nil {
if tempToken == token {
return true
}
}
}
}
return false
}
// 禁用一个用户的: 1.tb_users表的 status 设置为 0tb_oauth_access_tokens 表的所有token删除
// 禁用一个用户的token请求本质上就是把tb_users表的 status 字段设置为 0 即可)
func (u *UsersModel) SetTokenInvalid(userId int) bool {
sql := "delete from `tb_oauth_access_tokens` where `fr_user_id`=? "
if u.Exec(sql, userId).Error == nil {
if u.Exec("update tb_users set status=0 where id=?", userId).Error == nil {
return true
}
}
return false
}
//根据用户ID查询一条信息
func (u *UsersModel) ShowOneItem(userId int) (*UsersModel, error) {
sql := "SELECT `id`, `user_name`,`pass`, `real_name`, `phone`, `status` FROM `tb_users` WHERE `status`=1 and id=? LIMIT 1"
result := u.Raw(sql, userId).First(u)
if result.Error == nil {
return u, nil
} else {
return nil, result.Error
}
}
// 查询数据之前统计条数
func (u *UsersModel) counts(userName string) (counts int64) {
sql := "SELECT count(*) as counts FROM tb_users WHERE status=1 and user_name like ?"
if res := u.Raw(sql, "%"+userName+"%").First(&counts); res.Error != nil {
variable.ZapLog.Error("UsersModel - counts 查询数据条数出错", zap.Error(res.Error))
}
return counts
}
// 查询(根据关键词模糊查询)
func (u *UsersModel) Show(userName string, limitStart, limitItems int) (counts int64, temp []UsersModel) {
if counts = u.counts(userName); counts > 0 {
sql := "SELECT `id`, `user_name`, `real_name`, `phone`,last_login_ip, `status`,created_at,updated_at FROM `tb_users` WHERE `status`=1 and user_name like ? LIMIT ?,?"
if res := u.Raw(sql, "%"+userName+"%", limitStart, limitItems).Find(&temp); res.RowsAffected > 0 {
return counts, temp
}
}
return 0, nil
}
//新增
func (u *UsersModel) Store(userName string, pass string, realName string, phone string, remark string) bool {
sql := "INSERT INTO tb_users(user_name,pass,real_name,phone,remark) SELECT ?,?,?,?,? FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM tb_users WHERE user_name=?)"
if u.Exec(sql, userName, pass, realName, phone, remark, userName).RowsAffected > 0 {
return true
}
return false
}
//UpdateDataCheckUserNameIsUsed 更新前检查新的用户名是否已经存在(避免和别的账号重名)
func (u *UsersModel) UpdateDataCheckUserNameIsUsed(userId int, userName string) (exists int64) {
sql := "select count(*) as counts from tb_users where id!=? AND user_name=?"
_ = u.Raw(sql, userId, userName).First(&exists)
return exists
}
//更新
func (u *UsersModel) Update(id int, userName string, pass string, realName string, phone string, remark string, clientIp string) bool {
sql := "update tb_users set user_name=?,pass=?,real_name=?,phone=?,remark=? WHERE status=1 AND id=?"
if u.Exec(sql, userName, pass, realName, phone, remark, id).RowsAffected >= 0 {
if u.OauthResetToken(id, pass, clientIp) {
return true
}
}
return false
}
//删除用户以及关联的token记录
func (u *UsersModel) Destroy(id int) bool {
// 删除用户时清除用户缓存在redis的全部token
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.DelTokenCacheFromRedis(int64(id))
}
if u.Delete(u, id).Error == nil {
if u.OauthDestroyToken(id) {
return true
}
}
return false
}
// 后续两个函数专门处理用户 token 缓存到 redis 逻辑
func (u *UsersModel) ValidTokenCacheToRedis(userId int64) {
tokenCacheRedisFact := token_cache_redis.CreateUsersTokenCacheFactory(userId)
if tokenCacheRedisFact == nil {
variable.ZapLog.Error("redis连接失败请检查配置")
return
}
defer tokenCacheRedisFact.ReleaseRedisConn()
sql := "SELECT token,expires_at FROM `tb_oauth_access_tokens` WHERE fr_user_id=? AND revoked=0 AND expires_at>NOW() ORDER BY expires_at DESC , updated_at DESC LIMIT ?"
maxOnlineUsers := variable.ConfigYml.GetInt("Token.JwtTokenOnlineUsers")
rows, err := u.Raw(sql, userId, maxOnlineUsers).Rows()
defer func() {
// 凡是获取原生结果集的查询,记得释放记录集
_ = rows.Close()
}()
var tempToken, expires string
if err == nil && rows != nil {
for i := 1; rows.Next(); i++ {
err = rows.Scan(&tempToken, &expires)
if err == nil {
if ts, err := time.ParseInLocation(variable.DateFormat, expires, time.Local); err == nil {
tokenCacheRedisFact.SetTokenCache(ts.Unix(), tempToken)
// 因为每个用户的token是按照过期时间倒叙排列的第一个是有效期最长的将该用户的总键设置一个最大过期时间到期则自动清理避免不必要的数据残留
if i == 1 {
tokenCacheRedisFact.SetUserTokenExpire(ts.Unix())
}
} else {
variable.ZapLog.Error("expires_at 转换位时间戳出错", zap.Error(err))
}
}
}
}
// 缓存结束之后删除超过系统设置最大在线数量的token
tokenCacheRedisFact.DelOverMaxOnlineCache()
}
// DelTokenCacheFromRedis 用户密码修改后删除redis所有的token
func (u *UsersModel) DelTokenCacheFromRedis(userId int64) {
tokenCacheRedisFact := token_cache_redis.CreateUsersTokenCacheFactory(userId)
if tokenCacheRedisFact == nil {
variable.ZapLog.Error("redis连接失败请检查配置")
return
}
tokenCacheRedisFact.ClearUserToken()
tokenCacheRedisFact.ReleaseRedisConn()
}

@ -0,0 +1,312 @@
package model
import (
"go.uber.org/zap"
"goskeleton/app/global/variable"
"goskeleton/app/service/users/token_cache_redis"
"goskeleton/app/utils/md5_encrypt"
"strconv"
"time"
)
// 本文件针对 postgresql 数据库有效,请手动使用本文件的所有代码替换同目录的 users.go 中的所有代码即可
// 针对数据库选型为 postgresql 的开发者使用
// 操作数据库喜欢使用gorm自带语法的开发者可以参考 GinSkeleton-Admin 系统相关代码
// Admin 项目地址https://gitee.com/daitougege/gin-skeleton-admin-backend/
// gorm_v2 提供的语法+ ginskeleton 实践 http://gitee.com/daitougege/gin-skeleton-admin-backend/blob/master/app/model/button_cn_en.go
// 创建 userFactory
// 参数说明: 传递空值,默认使用 配置文件选项UseDbTypemysql
func CreateUserFactory(sqlType string) *UsersModel {
return &UsersModel{BaseModel: BaseModel{DB: UseDbConn(sqlType)}}
}
type UsersModel struct {
BaseModel
UserName string `gorm:"column:user_name" json:"user_name"`
Pass string `json:"-"`
Phone string `json:"phone"`
RealName string `gorm:"column:real_name" json:"real_name"`
Status int `json:"status"`
Token string `json:"token"`
LastLoginIp string `gorm:"column:last_login_ip" json:"last_login_ip"`
}
// TableName 表名
func (u *UsersModel) TableName() string {
return "web.tb_users"
}
// Register 用户注册(写一个最简单的使用账号、密码注册即可)
func (u *UsersModel) Register(userName, pass, userIp string) bool {
sql := "INSERT INTO web.tb_users(user_name,pass,last_login_ip) SELECT ?,?,? WHERE NOT EXISTS (SELECT 1 FROM web.tb_users WHERE user_name=?)"
result := u.Exec(sql, userName, pass, userIp, userName)
if result.RowsAffected > 0 {
return true
} else {
return false
}
}
// Login 用户登录,
func (u *UsersModel) Login(userName string, pass string) *UsersModel {
sql := "select id, user_name,real_name,pass,phone from web.tb_users where user_name=? limit 1"
result := u.Raw(sql, userName).First(u)
if result.Error == nil {
// 账号密码验证成功
if len(u.Pass) > 0 && (u.Pass == md5_encrypt.Base64Md5(pass)) {
return u
}
} else {
variable.ZapLog.Error("根据账号查询单条记录出错:", zap.Error(result.Error))
}
return nil
}
// OauthLoginToken 记录用户登陆login生成的token每次登陆记录一次token
func (u *UsersModel) OauthLoginToken(userId int64, token string, expiresAt int64, clientIp string) bool {
sql := `INSERT INTO web.tb_oauth_access_tokens(fr_user_id,action_name,token,expires_at,client_ip)
SELECT ?,'login',? ,?,? WHERE NOT EXISTS(SELECT 1 FROM web.tb_oauth_access_tokens a WHERE a.fr_user_id=? AND a.action_name='login' AND a.token=?)
`
//注意token的精确度为秒如果在一秒之内一个账号多次调用接口生成的token其实是相同的这样写入数据库第二次的影响行数为0知己实际上操作仍然是有效的。
//所以这里只判断无错误即可,判断影响行数的话,>=0 都是ok的
if u.Exec(sql, userId, token, time.Unix(expiresAt, 0).Format(variable.DateFormat), clientIp, userId, token).Error == nil {
// 异步缓存用户有效的token到redis
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.ValidTokenCacheToRedis(userId)
}
return true
}
return false
}
// OauthRefreshConditionCheck 用户刷新token,条件检查: 相关token在过期的时间之内就符合刷新条件
func (u *UsersModel) OauthRefreshConditionCheck(userId int64, oldToken string) bool {
// 首先判断旧token在本系统自带的数据库已经存在才允许继续执行刷新逻辑
var oldTokenIsExists int
sql := "SELECT count(*) as counts FROM web.tb_oauth_access_tokens WHERE fr_user_id =? and token=? and NOW() < (expires_at + cast(? as interval)) "
refreshSec := variable.ConfigYml.GetInt64("Token.JwtTokenRefreshAllowSec")
if u.Raw(sql, userId, oldToken, strconv.FormatInt(refreshSec, 10)+" second").First(&oldTokenIsExists).Error == nil && oldTokenIsExists == 1 {
return true
}
return false
}
// OauthRefreshToken 用户刷新token
func (u *UsersModel) OauthRefreshToken(userId, expiresAt int64, oldToken, newToken, clientIp string) bool {
sql := "UPDATE web.tb_oauth_access_tokens SET token=? ,expires_at=?,client_ip=?,updated_at= now() ,action_name='refresh' WHERE fr_user_id=? AND token=?"
if u.Exec(sql, newToken, time.Unix(expiresAt, 0).Format(variable.DateFormat), clientIp, userId, oldToken).Error == nil {
// 异步缓存用户有效的token到redis
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.ValidTokenCacheToRedis(userId)
}
go u.UpdateUserloginInfo(clientIp, userId)
return true
}
return false
}
// UpdateUserloginInfo 更新用户登陆次数、最近一次登录ip、最近一次登录时间
func (u *UsersModel) UpdateUserloginInfo(last_login_ip string, userId int64) {
sql := "UPDATE web.tb_users SET login_times=COALESCE(login_times,0)+1,last_login_ip=?,last_login_time=? WHERE id=? "
_ = u.Exec(sql, last_login_ip, time.Now().Format(variable.DateFormat), userId)
}
// OauthResetToken 当用户更改密码后所有的token都失效必须重新登录
func (u *UsersModel) OauthResetToken(userId int, newPass, clientIp string) bool {
//如果用户新旧密码一致直接返回true不需要处理
userItem, err := u.ShowOneItem(userId)
if userItem != nil && err == nil && userItem.Pass == newPass {
return true
} else if userItem != nil {
// 如果用户密码被修改那么redis中的token值也清除
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.DelTokenCacheFromRedis(int64(userId))
}
sql := "UPDATE web.tb_oauth_access_tokens SET revoked=1,updated_at= now() ,action_name='ResetPass',client_ip=? WHERE fr_user_id=? "
if u.Exec(sql, clientIp, userId).Error == nil {
return true
}
}
return false
}
//OauthDestroyToken 当tb_users 删除数据相关的token同步删除
func (u *UsersModel) OauthDestroyToken(userId int) bool {
//如果用户新旧密码一致直接返回true不需要处理
sql := "DELETE FROM web.tb_oauth_access_tokens WHERE fr_user_id=? "
//判断>=0, 有些没有登录过的用户没有相关token此语句执行影响行数为0但是仍然是执行成功
if u.Exec(sql, userId).Error == nil {
return true
}
return false
}
// OauthCheckTokenIsOk 判断用户token是否在数据库存在+状态OK
func (u *UsersModel) OauthCheckTokenIsOk(userId int64, token string) bool {
sql := `
SELECT token FROM web.tb_oauth_access_tokens
WHERE fr_user_id=? AND revoked=0 AND expires_at> now()
ORDER BY expires_at DESC , updated_at DESC limit ?
`
maxOnlineUsers := variable.ConfigYml.GetInt("Token.JwtTokenOnlineUsers")
rows, err := u.Raw(sql, userId, maxOnlineUsers).Rows()
defer func() {
// 凡是查询类记得释放记录集
_ = rows.Close()
}()
if err == nil && rows != nil {
for rows.Next() {
var tempToken string
err := rows.Scan(&tempToken)
if err == nil {
if tempToken == token {
_ = rows.Close()
return true
}
}
}
}
return false
}
// 禁用一个用户的: 1.tb_users表的 status 设置为 0web.tb_oauth_access_tokens 表的所有token删除
// 禁用一个用户的token请求本质上就是把tb_users表的 status 字段设置为 0 即可)
func (u *UsersModel) SetTokenInvalid(userId int) bool {
sql := "delete from web.tb_oauth_access_tokens where fr_user_id=? "
if u.Exec(sql, userId).Error == nil {
if u.Exec("update web.tb_users set status=0 where id=?", userId).Error == nil {
return true
}
}
return false
}
//根据用户ID查询一条信息
func (u *UsersModel) ShowOneItem(userId int) (*UsersModel, error) {
sql := "SELECT id, user_name,pass, real_name, phone, status,TO_CHAR(created_at,'yyyy-mm-dd hh24:mi:ss') as created_at, TO_CHAR(updated_at,'yyyy-mm-dd hh24:mi:ss') as updated_at FROM web.tb_users WHERE status=1 and id=? limit 1"
result := u.Raw(sql, userId).First(u)
if result.Error == nil {
return u, nil
} else {
return nil, result.Error
}
}
// counts 查询数据之前统计条数
func (u *UsersModel) counts(userName string) (counts int64) {
sql := "SELECT count(*) as counts FROM web.tb_users WHERE status=1 and user_name like ?"
if res := u.Raw(sql, "%"+userName+"%").First(&counts); res.Error != nil {
variable.ZapLog.Error("UsersModel - counts 查询数据条数出错", zap.Error(res.Error))
}
return counts
}
// Show 查询(根据关键词模糊查询)
func (u *UsersModel) Show(userName string, limitStart, limitItems int) (counts int64, temp []UsersModel) {
if counts = u.counts(userName); counts > 0 {
sql := `
SELECT id, user_name, real_name, phone, status, last_login_ip,phone,
TO_CHAR(created_at,'yyyy-mm-dd hh24:mi:ss') as created_at, TO_CHAR(updated_at,'yyyy-mm-dd hh24:mi:ss') as updated_at
FROM web.tb_users WHERE status=1 and user_name like ? limit ? offset ?
`
if res := u.Raw(sql, "%"+userName+"%", limitItems, limitStart).Find(&temp); res.RowsAffected > 0 {
return counts, temp
}
}
return 0, nil
}
// Store 新增
func (u *UsersModel) Store(userName string, pass string, realName string, phone string, remark string) bool {
sql := "INSERT INTO web.tb_users(user_name,pass,real_name,phone,remark) SELECT ?,?,?,?,? WHERE NOT EXISTS (SELECT 1 FROM web.tb_users WHERE user_name=?)"
if u.Exec(sql, userName, pass, realName, phone, remark, userName).RowsAffected > 0 {
return true
}
return false
}
// UpdateDataCheckUserNameIsUsed 更新前检查新的用户名是否已经存在(避免和别的账号重名)
func (u *UsersModel) UpdateDataCheckUserNameIsUsed(userId int, userName string) (exists int64) {
sql := "select count(*) as counts from web.tb_users where id!=? AND user_name=?"
_ = u.Raw(sql, userId, userName).First(&exists)
return exists
}
// Update 更新
func (u *UsersModel) Update(id int, userName string, pass string, realName string, phone string, remark string, clientIp string) bool {
sql := "update web.tb_users set user_name=?,pass=?,real_name=?,phone=?,remark=? WHERE status=1 AND id=?"
if u.Exec(sql, userName, pass, realName, phone, remark, id).RowsAffected >= 0 {
if u.OauthResetToken(id, pass, clientIp) {
return true
}
}
return false
}
// Destroy 删除用户以及关联的token记录
func (u *UsersModel) Destroy(id int) bool {
// 删除用户时清除用户缓存在redis的全部token
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.DelTokenCacheFromRedis(int64(id))
}
if u.Delete(u, id).Error == nil {
if u.OauthDestroyToken(id) {
return true
}
}
return false
}
// 后续两个函数专门处理用户 token 缓存到 redis 逻辑
func (u *UsersModel) ValidTokenCacheToRedis(userId int64) {
tokenCacheRedisFact := token_cache_redis.CreateUsersTokenCacheFactory(userId)
if tokenCacheRedisFact == nil {
variable.ZapLog.Error("redis连接失败请检查配置")
return
}
defer tokenCacheRedisFact.ReleaseRedisConn()
sql := "SELECT token,to_char(expires_at,'yyyy-mm-dd hh24:mi:ss') as expires_at FROM web.tb_oauth_access_tokens WHERE fr_user_id=? AND revoked=0 AND expires_at>NOW() ORDER BY expires_at DESC , updated_at DESC LIMIT ?"
maxOnlineUsers := variable.ConfigYml.GetInt("Token.JwtTokenOnlineUsers")
rows, err := u.Raw(sql, userId, maxOnlineUsers).Rows()
defer func() {
// 凡是获取原生结果集的查询,记得释放记录集
_ = rows.Close()
}()
var tempToken, expires string
if err == nil && rows != nil {
for i := 1; rows.Next(); i++ {
err = rows.Scan(&tempToken, &expires)
if err == nil {
if ts, err := time.ParseInLocation(variable.DateFormat, expires, time.Local); err == nil {
tokenCacheRedisFact.SetTokenCache(ts.Unix(), tempToken)
// 因为每个用户的token是按照过期时间倒叙排列的第一个是有效期最长的将该用户的总键设置一个最大过期时间到期则自动清理避免不必要的数据残留
if i == 1 {
tokenCacheRedisFact.SetUserTokenExpire(ts.Unix())
}
} else {
variable.ZapLog.Error("expires_at 转换位时间戳出错", zap.Error(err))
}
}
}
}
// 缓存结束之后删除超过系统设置最大在线数量的token
tokenCacheRedisFact.DelOverMaxOnlineCache()
}
// DelTokenCacheFromRedis 用户密码修改后删除redis所有的token
func (u *UsersModel) DelTokenCacheFromRedis(userId int64) {
tokenCacheRedisFact := token_cache_redis.CreateUsersTokenCacheFactory(userId)
if tokenCacheRedisFact == nil {
variable.ZapLog.Error("redis连接失败请检查配置")
return
}
tokenCacheRedisFact.ClearUserToken()
tokenCacheRedisFact.ReleaseRedisConn()
}

@ -0,0 +1,305 @@
package model
import (
"go.uber.org/zap"
"goskeleton/app/global/variable"
"goskeleton/app/service/users/token_cache_redis"
"goskeleton/app/utils/md5_encrypt"
"time"
)
// 本文件针对 sqlserver 数据库有效,请手动使用本文件的所有代码替换 users.go 中的所有代码即可
// 针对数据库选型为sqlserver的开发者使用
// 操作数据库喜欢使用gorm自带语法的开发者可以参考 GinSkeleton-Admin 系统相关代码
// Admin 项目地址https://gitee.com/daitougege/gin-skeleton-admin-backend/
// gorm_v2 提供的语法+ ginskeleton 实践 http://gitee.com/daitougege/gin-skeleton-admin-backend/blob/master/app/model/button_cn_en.go
// 创建 userFactory
// 参数说明: 传递空值,默认使用 配置文件选项UseDbTypemysql
func CreateUserFactory(sqlType string) *UsersModel {
return &UsersModel{BaseModel: BaseModel{DB: UseDbConn(sqlType)}}
}
type UsersModel struct {
BaseModel
UserName string `gorm:"column:user_name" json:"user_name"`
Pass string `json:"-"`
Phone string `json:"phone"`
RealName string `gorm:"column:real_name" json:"real_name"`
Status int `json:"status"`
Token string `json:"token"`
LastLoginIp string `gorm:"column:last_login_ip" json:"last_login_ip"`
}
// 表名
func (u *UsersModel) TableName() string {
return "tb_users"
}
// 用户注册(写一个最简单的使用账号、密码注册即可)
func (u *UsersModel) Register(userName, pass, userIp string) bool {
sql := "INSERT INTO tb_users(user_name,pass,last_login_ip) SELECT ?,?,? WHERE NOT EXISTS (SELECT 1 FROM tb_users WHERE user_name=?)"
result := u.Exec(sql, userName, pass, userIp, userName)
if result.RowsAffected > 0 {
return true
} else {
return false
}
}
// 用户登录,
func (u *UsersModel) Login(userName string, pass string) *UsersModel {
sql := "select top 1 id, user_name,real_name,pass,phone from tb_users where user_name=? "
result := u.Raw(sql, userName).First(u)
if result.Error == nil {
// 账号密码验证成功
if len(u.Pass) > 0 && (u.Pass == md5_encrypt.Base64Md5(pass)) {
return u
}
} else {
variable.ZapLog.Error("根据账号查询单条记录出错:", zap.Error(result.Error))
}
return nil
}
//记录用户登陆login生成的token每次登陆记录一次token
func (u *UsersModel) OauthLoginToken(userId int64, token string, expiresAt int64, clientIp string) bool {
sql := `
INSERT INTO tb_oauth_access_tokens(fr_user_id,action_name,token,expires_at,client_ip)
SELECT ?,'login',? ,?,? WHERE NOT EXISTS(SELECT 1 FROM tb_oauth_access_tokens a WHERE a.fr_user_id=? AND a.action_name='login' AND a.token=?)
`
//注意token的精确度为秒如果在一秒之内一个账号多次调用接口生成的token其实是相同的这样写入数据库第二次的影响行数为0知己实际上操作仍然是有效的。
//所以这里只判断无错误即可,判断影响行数的话,>=0 都是ok的
if u.Exec(sql, userId, token, time.Unix(expiresAt, 0).Format(variable.DateFormat), clientIp, userId, token).Error == nil {
// 异步缓存用户有效的token到redis
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.ValidTokenCacheToRedis(userId)
}
return true
}
return false
}
//用户刷新token,条件检查: 相关token在过期的时间之内就符合刷新条件
func (u *UsersModel) OauthRefreshConditionCheck(userId int64, oldToken string) bool {
// 首先判断旧token在本系统自带的数据库已经存在才允许继续执行刷新逻辑
var oldTokenIsExists int
sql := "SELECT count(*) as counts FROM tb_oauth_access_tokens WHERE fr_user_id =? and token=? and GETDATE()<DATEADD(SS,?,expires_at)"
if u.Raw(sql, userId, oldToken, variable.ConfigYml.GetInt64("Token.JwtTokenRefreshAllowSec")).First(&oldTokenIsExists).Error == nil && oldTokenIsExists == 1 {
return true
}
return false
}
//用户刷新token
func (u *UsersModel) OauthRefreshToken(userId, expiresAt int64, oldToken, newToken, clientIp string) bool {
sql := "UPDATE tb_oauth_access_tokens SET token=? ,expires_at=?,client_ip=?,updated_at=GETDATE(),action_name='refresh' WHERE fr_user_id=? AND token=?"
if u.Exec(sql, newToken, time.Unix(expiresAt, 0).Format(variable.DateFormat), clientIp, userId, oldToken).Error == nil {
// 异步缓存用户有效的token到redis
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
go u.ValidTokenCacheToRedis(userId)
}
go u.UpdateUserloginInfo(clientIp, userId)
return true
}
return false
}
// 更新用户登陆次数、最近一次登录ip、最近一次登录时间
func (u *UsersModel) UpdateUserloginInfo(last_login_ip string, userId int64) {
sql := "UPDATE tb_users SET login_times=isnull(login_times,0)+1,last_login_ip=?,last_login_time=? WHERE id=? "
_ = u.Exec(sql, last_login_ip, time.Now().Format(variable.DateFormat), userId)
}
//当用户更改密码后所有的token都失效必须重新登录
func (u *UsersModel) OauthResetToken(userId int, newPass, clientIp string) bool {
//如果用户新旧密码一致直接返回true不需要处理
userItem, err := u.ShowOneItem(userId)
if userItem != nil && err == nil && userItem.Pass == newPass {
return true
} else if userItem != nil {
sql := "UPDATE tb_oauth_access_tokens SET revoked=1,updated_at=GETDATE(),action_name='ResetPass',client_ip=? WHERE fr_user_id=? "
if u.Exec(sql, clientIp, userId).Error == nil {
return true
}
}
return false
}
//当tb_users 删除数据相关的token同步删除
func (u *UsersModel) OauthDestroyToken(userId int) bool {
//如果用户新旧密码一致直接返回true不需要处理
sql := "DELETE FROM tb_oauth_access_tokens WHERE fr_user_id=? "
//判断>=0, 有些没有登录过的用户没有相关token此语句执行影响行数为0但是仍然是执行成功
if u.Exec(sql, userId).Error == nil {
return true
}
return false
}
// 判断用户token是否在数据库存在+状态OK
func (u *UsersModel) OauthCheckTokenIsOk(userId int64, token string) bool {
sql := `
SELECT token FROM tb_oauth_access_tokens
WHERE fr_user_id=? AND revoked=0 AND expires_at>GETDATE()
ORDER BY expires_at DESC , updated_at DESC
OFFSET 0 ROW FETCH NEXT ? ROWS ONLY
`
maxOnlineUsers := variable.ConfigYml.GetInt("Token.JwtTokenOnlineUsers")
rows, err := u.Raw(sql, userId, maxOnlineUsers).Rows()
defer func() {
// 凡是查询类记得释放记录集
_ = rows.Close()
}()
if err == nil && rows != nil {
for rows.Next() {
var tempToken string
err := rows.Scan(&tempToken)
if err == nil {
if tempToken == token {
_ = rows.Close()
return true
}
}
}
}
return false
}
// 禁用一个用户的: 1.tb_users表的 status 设置为 0tb_oauth_access_tokens 表的所有token删除
// 禁用一个用户的token请求本质上就是把tb_users表的 status 字段设置为 0 即可)
func (u *UsersModel) SetTokenInvalid(userId int) bool {
sql := "delete from tb_oauth_access_tokens where fr_user_id=? "
if u.Exec(sql, userId).Error == nil {
if u.Exec("update tb_users set status=0 where id=?", userId).Error == nil {
return true
}
}
return false
}
//根据用户ID查询一条信息
func (u *UsersModel) ShowOneItem(userId int) (*UsersModel, error) {
sql := "SELECT top 1 id, user_name,pass, real_name, phone, status FROM tb_users WHERE status=1 and id=?"
result := u.Raw(sql, userId).First(u)
if result.Error == nil {
return u, nil
} else {
return nil, result.Error
}
}
// 查询数据之前统计条数
func (u *UsersModel) counts(userName string) (counts int64) {
sql := "SELECT count(*) as counts FROM tb_users WHERE status=1 and user_name like ?"
if res := u.Raw(sql, "%"+userName+"%").First(&counts); res.Error != nil {
variable.ZapLog.Error("UsersModel - counts 查询数据条数出错", zap.Error(res.Error))
}
return counts
}
// 查询(根据关键词模糊查询)
func (u *UsersModel) Show(userName string, limitStart, limitItems int) (counts int64, temp []UsersModel) {
if counts = u.counts(userName); counts > 0 {
sql := `
SELECT id, user_name, real_name, phone,last_login_ip, status, CONVERT(varchar(20), created_at, 120 ) as created_at, CONVERT(varchar(20), updated_at, 120 ) as updated_at
FROM tb_users WHERE status=1 and user_name like ? order by id desc OFFSET ? ROW FETCH NEXT ? ROWS ONLY
`
if res := u.Raw(sql, "%"+userName+"%", limitStart, limitItems).Find(&temp); res.RowsAffected > 0 {
return counts, temp
}
}
return 0, nil
}
//新增
func (u *UsersModel) Store(userName string, pass string, realName string, phone string, remark string) bool {
sql := "INSERT INTO tb_users(user_name,pass,real_name,phone,remark) SELECT ?,?,?,?,? WHERE NOT EXISTS (SELECT 1 FROM tb_users WHERE user_name=?)"
if u.Exec(sql, userName, pass, realName, phone, remark, userName).RowsAffected > 0 {
return true
}
return false
}
//UpdateDataCheckUserNameIsUsed 更新前检查新的用户名是否已经存在(避免和别的账号重名)
func (u *UsersModel) UpdateDataCheckUserNameIsUsed(userId int, userName string) (exists int64) {
sql := "select count(*) as counts from tb_users where id!=? AND user_name=?"
_ = u.Raw(sql, userId, userName).First(&exists)
return exists
}
//更新
func (u *UsersModel) Update(id int, userName string, pass string, realName string, phone string, remark string, clientIp string) bool {
sql := "update tb_users set user_name=?,pass=?,real_name=?,phone=?,remark=? WHERE status=1 AND id=?"
if u.Exec(sql, userName, pass, realName, phone, remark, id).RowsAffected >= 0 {
if u.OauthResetToken(id, pass, clientIp) {
return true
}
}
return false
}
//删除用户以及关联的token记录
func (u *UsersModel) Destroy(id int) bool {
if u.Delete(u, id).Error == nil {
if u.OauthDestroyToken(id) {
return true
}
}
return false
}
// 后续两个函数专门处理用户 token 缓存到 redis 逻辑
func (u *UsersModel) ValidTokenCacheToRedis(userId int64) {
tokenCacheRedisFact := token_cache_redis.CreateUsersTokenCacheFactory(userId)
if tokenCacheRedisFact == nil {
variable.ZapLog.Error("redis连接失败请检查配置")
return
}
defer tokenCacheRedisFact.ReleaseRedisConn()
sql := `
SELECT token,CONVERT(varchar(20), expires_at, 120 ) as expires_at FROM tb_oauth_access_tokens
WHERE fr_user_id=? AND revoked=0 AND expires_at>getdate() ORDER BY expires_at DESC , updated_at DESC
OFFSET 0 ROW FETCH NEXT ? ROWS ONLY
`
maxOnlineUsers := variable.ConfigYml.GetInt("Token.JwtTokenOnlineUsers")
rows, err := u.Raw(sql, userId, maxOnlineUsers).Rows()
defer func() {
// 凡是获取原生结果集的查询,记得释放记录集
_ = rows.Close()
}()
var tempToken, expires string
if err == nil && rows != nil {
for i := 1; rows.Next(); i++ {
err = rows.Scan(&tempToken, &expires)
if err == nil {
if ts, err := time.ParseInLocation(variable.DateFormat, expires, time.Local); err == nil {
tokenCacheRedisFact.SetTokenCache(ts.Unix(), tempToken)
// 因为每个用户的token是按照过期时间倒叙排列的第一个是有效期最长的将该用户的总键设置一个最大过期时间到期则自动清理避免不必要的数据残留
if i == 1 {
tokenCacheRedisFact.SetUserTokenExpire(ts.Unix())
}
} else {
variable.ZapLog.Error("expires_at 转换位时间戳出错", zap.Error(err))
}
}
}
}
// 缓存结束之后删除超过系统设置最大在线数量的token
tokenCacheRedisFact.DelOverMaxOnlineCache()
}
// DelTokenCacheFromRedis 用户密码修改后删除redis所有的token
func (u *UsersModel) DelTokenCacheFromRedis(userId int64) {
tokenCacheRedisFact := token_cache_redis.CreateUsersTokenCacheFactory(userId)
if tokenCacheRedisFact == nil {
variable.ZapLog.Error("redis连接失败请检查配置")
return
}
tokenCacheRedisFact.ClearUserToken()
tokenCacheRedisFact.ReleaseRedisConn()
}

@ -0,0 +1,27 @@
package sys_log_hook
import (
"go.uber.org/zap/zapcore"
)
// GoSkeleton 系统运行日志钩子函数
// 1.单条日志就是一个结构体格式本函数拦截每一条日志您可以进行后续处理例如推送到阿里云日志管理面板、ElasticSearch 日志库等
func ZapLogHandler(entry zapcore.Entry) error {
// 参数 entry 介绍
// entry 参数就是单条日志结构体,主要包括字段如下:
//Level 日志等级
//Time 当前时间
//LoggerName 日志名称
//Message 日志内容
//Caller 各个文件调用路径
//Stack 代码调用栈
//这里启动一个协程hook丝毫不会影响程序性能
go func(paramEntry zapcore.Entry) {
//fmt.Println(" GoSkeleton hook ....,你可以在这里继续处理系统日志....")
//fmt.Printf("%#+v\n", paramEntry)
}(entry)
return nil
}

@ -0,0 +1,58 @@
package upload_file
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"goskeleton/app/utils/md5_encrypt"
"os"
"path"
"strings"
"time"
)
func Upload(context *gin.Context, savePath string) (r bool, finnalSavePath interface{}) {
newSavePath, newReturnPath := generateYearMonthPath(savePath)
// 1.获取上传的文件名(参数验证器已经验证完成了第一步错误,这里简化)
file, _ := context.FormFile(variable.ConfigYml.GetString("FileUploadSetting.UploadFileField")) // file 是一个文件结构体(文件对象)
// 保存文件原始文件名进行全局唯一编码加密、md5 加密,保证在后台存储不重复
var saveErr error
if sequence := variable.SnowFlake.GetId(); sequence > 0 {
saveFileName := fmt.Sprintf("%d%s", sequence, file.Filename)
saveFileName = md5_encrypt.MD5(saveFileName) + path.Ext(saveFileName)
if saveErr = context.SaveUploadedFile(file, newSavePath+saveFileName); saveErr == nil {
// 上传成功,返回资源的相对路径,这里请根据实际返回绝对路径或者相对路径
finnalSavePath = gin.H{
"path": strings.ReplaceAll(newReturnPath+saveFileName, variable.BasePath, ""),
}
return true, finnalSavePath
}
} else {
saveErr = errors.New(my_errors.ErrorsSnowflakeGetIdFail)
variable.ZapLog.Error("文件保存出错:" + saveErr.Error())
}
return false, nil
}
// 文件上传可以设置按照 xxx年-xx月 格式存储
func generateYearMonthPath(savePathPre string) (string, string) {
returnPath := variable.BasePath + variable.ConfigYml.GetString("FileUploadSetting.UploadFileReturnPath")
curYearMonth := time.Now().Format("2006_01")
newSavePathPre := savePathPre + curYearMonth
newReturnPathPre := returnPath + curYearMonth
// 相关路径不存在,创建目录
if _, err := os.Stat(newSavePathPre); err != nil {
if err = os.MkdirAll(newSavePathPre, os.ModePerm); err != nil {
variable.ZapLog.Error("文件上传创建目录出错" + err.Error())
return "", ""
}
}
return newSavePathPre + "/", newReturnPathPre + "/"
}

@ -0,0 +1,31 @@
package curd
import (
"goskeleton/app/model"
"goskeleton/app/utils/md5_encrypt"
)
func CreateUserCurdFactory() *UsersCurd {
return &UsersCurd{model.CreateUserFactory("")}
}
type UsersCurd struct {
userModel *model.UsersModel
}
func (u *UsersCurd) Register(userName, pass, userIp string) bool {
pass = md5_encrypt.Base64Md5(pass) // 预先处理密码加密,然后存储在数据库
return u.userModel.Register(userName, pass, userIp)
}
func (u *UsersCurd) Store(name string, pass string, realName string, phone string, remark string) bool {
pass = md5_encrypt.Base64Md5(pass) // 预先处理密码加密,然后存储在数据库
return u.userModel.Store(name, pass, realName, phone, remark)
}
func (u *UsersCurd) Update(id int, name string, pass string, realName string, phone string, remark string, clientIp string) bool {
//预先处理密码加密等操作,然后进行更新
pass = md5_encrypt.Base64Md5(pass) // 预先处理密码加密,然后存储在数据库
return u.userModel.Update(id, name, pass, realName, phone, remark, clientIp)
}

@ -0,0 +1,139 @@
package token
import (
"errors"
"github.com/dgrijalva/jwt-go"
"goskeleton/app/global/consts"
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"goskeleton/app/http/middleware/my_jwt"
"goskeleton/app/model"
"goskeleton/app/service/users/token_cache_redis"
"time"
)
// CreateUserFactory 创建 userToken 工厂
func CreateUserFactory() *userToken {
return &userToken{
userJwt: my_jwt.CreateMyJWT(variable.ConfigYml.GetString("Token.JwtTokenSignKey")),
}
}
type userToken struct {
userJwt *my_jwt.JwtSign
}
// GenerateToken 生成token
func (u *userToken) GenerateToken(userid int64, username string, phone string, expireAt int64) (tokens string, err error) {
// 根据实际业务自定义token需要包含的参数生成token注意用户密码请勿包含在token
customClaims := my_jwt.CustomClaims{
UserId: userid,
Name: username,
Phone: phone,
// 特别注意,针对前文的匿名结构体,初始化的时候必须指定键名,并且不带 jwt. 否则报错Mixture of field: value and value initializers
StandardClaims: jwt.StandardClaims{
NotBefore: time.Now().Unix() - 10, // 生效开始时间
ExpiresAt: time.Now().Unix() + expireAt, // 失效截止时间
},
}
return u.userJwt.CreateToken(customClaims)
}
// RecordLoginToken 用户login成功记录用户token
func (u *userToken) RecordLoginToken(userToken, clientIp string) bool {
if customClaims, err := u.userJwt.ParseToken(userToken); err == nil {
userId := customClaims.UserId
expiresAt := customClaims.ExpiresAt
return model.CreateUserFactory("").OauthLoginToken(userId, userToken, expiresAt, clientIp)
} else {
return false
}
}
// TokenIsMeetRefreshCondition 检查token是否满足刷新条件
func (u *userToken) TokenIsMeetRefreshCondition(token string) bool {
// token基本信息是否有效1.过期时间在允许的过期范围内;2.基本格式正确
customClaims, code := u.isNotExpired(token, variable.ConfigYml.GetInt64("Token.JwtTokenRefreshAllowSec"))
switch code {
case consts.JwtTokenOK, consts.JwtTokenExpired:
//在数据库的存储信息是否也符合过期刷新刷新条件
if model.CreateUserFactory("").OauthRefreshConditionCheck(customClaims.UserId, token) {
return true
}
}
return false
}
// RefreshToken 刷新token的有效期默认+3600秒参见常量配置项
func (u *userToken) RefreshToken(oldToken, clientIp string) (newToken string, res bool) {
var err error
//如果token是有效的、或者在过期时间内那么执行更新换取新token
if newToken, err = u.userJwt.RefreshToken(oldToken, variable.ConfigYml.GetInt64("Token.JwtTokenRefreshExpireAt")); err == nil {
if customClaims, err := u.userJwt.ParseToken(newToken); err == nil {
userId := customClaims.UserId
expiresAt := customClaims.ExpiresAt
if model.CreateUserFactory("").OauthRefreshToken(userId, expiresAt, oldToken, newToken, clientIp) {
return newToken, true
}
}
}
return "", false
}
// 判断token本身是否未过期
// 参数解释:
// token 待处理的token值
// expireAtSec 过期时间延长的秒数主要用于用户刷新token时判断是否在延长的时间范围内非刷新逻辑默认为0
func (u *userToken) isNotExpired(token string, expireAtSec int64) (*my_jwt.CustomClaims, int) {
if customClaims, err := u.userJwt.ParseToken(token); err == nil {
if time.Now().Unix()-(customClaims.ExpiresAt+expireAtSec) < 0 {
// token有效
return customClaims, consts.JwtTokenOK
} else {
// 过期的token
return customClaims, consts.JwtTokenExpired
}
} else {
// 无效的token
return nil, consts.JwtTokenInvalid
}
}
// IsEffective 判断token是否有效未过期+数据库用户信息正常)
func (u *userToken) IsEffective(token string) bool {
customClaims, code := u.isNotExpired(token, 0)
if consts.JwtTokenOK == code {
//1.首先在redis检测是否存在某个用户对应的有效token如果存在就直接返回不再继续查询mysql否则最后查询mysql逻辑确保万无一失
if variable.ConfigYml.GetInt("Token.IsCacheToRedis") == 1 {
tokenRedisFact := token_cache_redis.CreateUsersTokenCacheFactory(customClaims.UserId)
if tokenRedisFact != nil {
defer tokenRedisFact.ReleaseRedisConn()
if tokenRedisFact.TokenCacheIsExists(token) {
return true
}
}
}
//2.token符合token本身的规则以后继续在数据库校验是不是符合本系统其他设置例如一个用户默认只允许10个账号同时在线10个token同时有效
if model.CreateUserFactory("").OauthCheckTokenIsOk(customClaims.UserId, token) {
return true
}
}
return false
}
// ParseToken 将 token 解析为绑定时传递的参数
func (u *userToken) ParseToken(tokenStr string) (CustomClaims my_jwt.CustomClaims, err error) {
if customClaims, err := u.userJwt.ParseToken(tokenStr); err == nil {
return *customClaims, nil
} else {
return my_jwt.CustomClaims{}, errors.New(my_errors.ErrorsParseTokenFail)
}
}
// DestroyToken 销毁token基本用不到因为一个网站的用户退出都是直接关闭浏览器窗口极少有户会点击“注销、退出”等按钮销毁token其实无多大意义
func (u *userToken) DestroyToken() {
}

@ -0,0 +1,97 @@
package token_cache_redis
import (
"go.uber.org/zap"
"goskeleton/app/global/variable"
"goskeleton/app/utils/md5_encrypt"
"goskeleton/app/utils/redis_factory"
"strconv"
"strings"
"time"
)
func CreateUsersTokenCacheFactory(userId int64) *userTokenCacheRedis {
redCli := redis_factory.GetOneRedisClient()
if redCli == nil {
return nil
}
return &userTokenCacheRedis{redisClient: redCli, userTokenKey: "token_userid_" + strconv.FormatInt(userId, 10)}
}
type userTokenCacheRedis struct {
redisClient *redis_factory.RedisClient
userTokenKey string
}
// SetTokenCache 设置缓存
func (u *userTokenCacheRedis) SetTokenCache(tokenExpire int64, token string) bool {
// 存储用户token时转为MD5下一步比较的时候可以更加快速地比较是否一致
if _, err := u.redisClient.Int(u.redisClient.Execute("zAdd", u.userTokenKey, tokenExpire, md5_encrypt.MD5(token))); err == nil {
return true
} else {
variable.ZapLog.Error("缓存用户token到redis出错", zap.Error(err))
}
return false
}
// DelOverMaxOnlineCache 删除缓存,删除超过系统允许最大在线数量之外的用户
func (u *userTokenCacheRedis) DelOverMaxOnlineCache() bool {
// 首先先删除过期的token
_, _ = u.redisClient.Execute("zRemRangeByScore", u.userTokenKey, 0, time.Now().Unix()-1)
onlineUsers := variable.ConfigYml.GetInt("Token.JwtTokenOnlineUsers")
alreadyCacheNum, err := u.redisClient.Int(u.redisClient.Execute("zCard", u.userTokenKey))
if err == nil && alreadyCacheNum > onlineUsers {
// 删除超过最大在线数量之外的token
if alreadyCacheNum, err = u.redisClient.Int(u.redisClient.Execute("zRemRangeByRank", u.userTokenKey, 0, alreadyCacheNum-onlineUsers-1)); err == nil {
return true
} else {
variable.ZapLog.Error("删除超过系统允许之外的token出错", zap.Error(err))
}
}
return false
}
// TokenCacheIsExists 查询token是否在redis存在
func (u *userTokenCacheRedis) TokenCacheIsExists(token string) (exists bool) {
token = md5_encrypt.MD5(token)
curTimestamp := time.Now().Unix()
onlineUsers := variable.ConfigYml.GetInt("Token.JwtTokenOnlineUsers")
if strSlice, err := u.redisClient.Strings(u.redisClient.Execute("zRevRange", u.userTokenKey, 0, onlineUsers-1)); err == nil {
for _, val := range strSlice {
if score, err := u.redisClient.Int64(u.redisClient.Execute("zScore", u.userTokenKey, token)); err == nil {
if score > curTimestamp {
if strings.Compare(val, token) == 0 {
exists = true
break
}
}
}
}
} else {
variable.ZapLog.Error("获取用户在redis缓存的 token 值出错:", zap.Error(err))
}
return
}
// SetUserTokenExpire 设置用户的 usertoken 键过期时间
// 参数: 时间戳
func (u *userTokenCacheRedis) SetUserTokenExpire(ts int64) bool {
if _, err := u.redisClient.Execute("expireAt", u.userTokenKey, ts); err == nil {
return true
}
return false
}
// ClearUserToken 清除某个用户的全部缓存,当用户更改密码或者用户被禁用则删除该用户的全部缓存
func (u *userTokenCacheRedis) ClearUserToken() bool {
if _, err := u.redisClient.Execute("del", u.userTokenKey); err == nil {
return true
}
return false
}
// ReleaseRedisConn 释放redis
func (u *userTokenCacheRedis) ReleaseRedisConn() {
u.redisClient.ReleaseOneRedisClient()
}

@ -0,0 +1,9 @@
package on_open_success
// ClientMoreParams 为客户端成功上线后设置更多的参数
// ws 客户端成功上线以后,可以通过客户端携带的唯一参数,在数据库查询更多的其他关键信息,设置在 *Client 结构体上
// 这样便于在后续获取在线客户端时快速获取其他关键信息,例如:进行消息广播时记录日志可能需要更多字段信息等
type ClientMoreParams struct {
UserParams1 string `json:"user_params_1"` // 字段名称以及类型由 开发者自己定义
UserParams2 string `json:"user_params_2"`
}

@ -0,0 +1,91 @@
package websocket
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"goskeleton/app/global/consts"
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"goskeleton/app/utils/websocket/core"
)
/**
websocket
1.onOpen
2.OnMessage
3.OnError
4.OnClose
*/
type Ws struct {
WsClient *core.Client
}
// OnOpen 事件函数
func (w *Ws) OnOpen(context *gin.Context) (*Ws, bool) {
if client, ok := (&core.Client{}).OnOpen(context); ok {
token := context.GetString(consts.ValidatorPrefix + "token")
variable.ZapLog.Info("获取到的客户端上线时携带的唯一标记值:", zap.String("token", token))
// 成功上线以后,开发者可以基于客户端上线时携带的唯一参数(这里用token键表示)
// 在数据库查询更多的其他字段信息,直接追加在 Client 结构体上,方便后续使用
//client.ClientMoreParams.UserParams1 = token
//client.ClientMoreParams.UserParams2 = "456"
//fmt.Printf("最终每一个客户端(client) 已有的参数:%+v\n", client)
w.WsClient = client
go w.WsClient.Heartbeat() // 一旦握手+协议升级成功,就为每一个连接开启一个自动化的隐式心跳检测包
return w, true
} else {
return nil, false
}
}
// OnMessage 处理业务消息
func (w *Ws) OnMessage(context *gin.Context) {
go w.WsClient.ReadPump(func(messageType int, receivedData []byte) {
//参数说明
//messageType 消息类型1=文本
//receivedData 服务器接收到客户端例如js客户端发来的的数据[]byte 格式
tempMsg := "服务器已经收到了你的消息==>" + string(receivedData)
// 回复客户端已经收到消息;
if err := w.WsClient.SendMessage(messageType, tempMsg); err != nil {
variable.ZapLog.Error("消息发送出现错误", zap.Error(err))
}
}, w.OnError, w.OnClose)
}
// OnError 客户端与服务端在消息交互过程中发生错误回调函数
func (w *Ws) OnError(err error) {
w.WsClient.State = 0 // 发生错误状态设置为0, 心跳检测协程则自动退出
variable.ZapLog.Error("远端掉线、卡死、刷新浏览器等会触发该错误:", zap.Error(err))
//fmt.Printf("远端掉线、卡死、刷新浏览器等会触发该错误: %v\n", err.Error())
}
// OnClose 客户端关闭回调发生onError回调以后会继续回调该函数
func (w *Ws) OnClose() {
w.WsClient.Hub.UnRegister <- w.WsClient // 向hub管道投递一条注销消息由hub中心负责关闭连接、删除在线数据
}
// GetOnlineClients 获取在线的全部客户端
func (w *Ws) GetOnlineClients() {
fmt.Printf("在线客户端数量:%d\n", len(w.WsClient.Hub.Clients))
}
// BroadcastMsg (每一个客户端都有能力)向全部在线客户端广播消息
func (w *Ws) BroadcastMsg(sendMsg string) {
for onlineClient := range w.WsClient.Hub.Clients {
//获取每一个在线的客户端,向远端发送消息
if err := onlineClient.SendMessage(websocket.TextMessage, sendMsg); err != nil {
variable.ZapLog.Error(my_errors.ErrorsWebsocketWriteMgsFail, zap.Error(err))
}
}
}

@ -0,0 +1,58 @@
package casbin_v2
import (
"errors"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
gormadapter "github.com/casbin/gorm-adapter/v3"
"gorm.io/gorm"
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"strings"
"time"
)
//创建 casbin Enforcer(执行器)
func InitCasbinEnforcer() (*casbin.SyncedEnforcer, error) {
var tmpDbConn *gorm.DB
var Enforcer *casbin.SyncedEnforcer
switch strings.ToLower(variable.ConfigGormv2Yml.GetString("Gormv2.UseDbType")) {
case "mysql":
if variable.GormDbMysql == nil {
return nil, errors.New(my_errors.ErrorCasbinCanNotUseDbPtr)
}
tmpDbConn = variable.GormDbMysql
case "sqlserver", "mssql":
if variable.GormDbSqlserver == nil {
return nil, errors.New(my_errors.ErrorCasbinCanNotUseDbPtr)
}
tmpDbConn = variable.GormDbSqlserver
case "postgre", "postgresql", "postgres":
if variable.GormDbPostgreSql == nil {
return nil, errors.New(my_errors.ErrorCasbinCanNotUseDbPtr)
}
tmpDbConn = variable.GormDbPostgreSql
default:
}
prefix := variable.ConfigYml.GetString("Casbin.TablePrefix")
tbName := variable.ConfigYml.GetString("Casbin.TableName")
a, err := gormadapter.NewAdapterByDBUseTableName(tmpDbConn, prefix, tbName)
if err != nil {
return nil, errors.New(my_errors.ErrorCasbinCreateAdaptFail)
}
modelConfig := variable.ConfigYml.GetString("Casbin.ModelConfig")
if m, err := model.NewModelFromString(modelConfig); err != nil {
return nil, errors.New(my_errors.ErrorCasbinNewModelFromStringFail + err.Error())
} else {
if Enforcer, err = casbin.NewSyncedEnforcer(m, a); err != nil {
return nil, errors.New(my_errors.ErrorCasbinCreateEnforcerFail)
}
_ = Enforcer.LoadPolicy()
AutoLoadSeconds := variable.ConfigYml.GetDuration("Casbin.AutoLoadPolicySeconds")
Enforcer.StartAutoLoadPolicy(time.Second * AutoLoadSeconds)
return Enforcer, nil
}
}

@ -0,0 +1,15 @@
package cur_userinfo
import (
"github.com/gin-gonic/gin"
"goskeleton/app/global/variable"
"goskeleton/app/http/middleware/my_jwt"
)
// GetCurrentUserId 获取当前用户的id
// @context 请求上下文
func GetCurrentUserId(context *gin.Context) (int64, bool) {
tokenKey := variable.ConfigYml.GetString("Token.BindContextKeyName")
currentUser, exist := context.MustGet(tokenKey).(my_jwt.CustomClaims)
return currentUser.UserId, exist
}

@ -0,0 +1,77 @@
package data_bind
import (
"errors"
"github.com/gin-gonic/gin"
"goskeleton/app/global/consts"
"reflect"
)
const (
modelStructMustPtr = "modelStruct 必须传递一个指针"
)
// 绑定form表单验证器已经验证完成的参数到 model 结构体,
// mode 结构体支持匿名嵌套
// 数据绑定原则:
// 1.表单参数验证器中的结构体字段 json 标签必须和 model 结构体定义的 json 标签一致
// 2.model 中的数据类型与表单参数验证器数据类型保持一致:
// 例如model 中的 user_name 是 string 那么表单参数验证器中的 user_name 也必须是 stringbool 类型同理,日期时间字段在 ginskeleton 中请按照 string 处理
// 3.但是 model 中的字段如果是数字类型int、int8、int16、int64、float32、float64等都可以绑定表单参数验证中的 float64 类型,程序会自动将原始的 float64 转换为 model 的定义的数字类型
func ShouldBindFormDataToModel(c *gin.Context, modelStruct interface{}) error {
mTypeOf := reflect.TypeOf(modelStruct)
if mTypeOf.Kind() != reflect.Ptr {
return errors.New(modelStructMustPtr)
}
mValueOf := reflect.ValueOf(modelStruct)
//分析 modelStruct 字段
mValueOfEle := mValueOf.Elem()
mtf := mValueOf.Elem().Type()
fieldNum := mtf.NumField()
for i := 0; i < fieldNum; i++ {
if !mtf.Field(i).Anonymous && mtf.Field(i).Type.Kind() != reflect.Struct {
fieldSetValue(c, mValueOfEle, mtf, i)
} else if mtf.Field(i).Type.Kind() == reflect.Struct {
//处理结构体(有名+匿名)
mValueOfEle.Field(i).Set(analysisAnonymousStruct(c, mValueOfEle.Field(i)))
}
}
return nil
}
// 分析匿名结构体,并且获取匿名结构体的值
func analysisAnonymousStruct(c *gin.Context, value reflect.Value) reflect.Value {
typeOf := value.Type()
fieldNum := typeOf.NumField()
newStruct := reflect.New(typeOf)
newStructElem := newStruct.Elem()
for i := 0; i < fieldNum; i++ {
fieldSetValue(c, newStructElem, typeOf, i)
}
return newStructElem
}
// 为结构体字段赋值
func fieldSetValue(c *gin.Context, valueOf reflect.Value, typeOf reflect.Type, colIndex int) {
relaKey := typeOf.Field(colIndex).Tag.Get("json")
if relaKey != "-" {
relaKey = consts.ValidatorPrefix + typeOf.Field(colIndex).Tag.Get("json")
switch typeOf.Field(colIndex).Type.Kind() {
case reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64:
valueOf.Field(colIndex).SetInt(int64(c.GetFloat64(relaKey)))
case reflect.Float32, reflect.Float64:
valueOf.Field(colIndex).SetFloat(c.GetFloat64(relaKey))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
valueOf.Field(colIndex).SetUint(uint64(c.GetFloat64(relaKey)))
case reflect.String:
valueOf.Field(colIndex).SetString(c.GetString(relaKey))
case reflect.Bool:
valueOf.Field(colIndex).SetBool(c.GetBool(relaKey))
default:
// model 如果有日期时间字段,请统一设置为字符串即可
}
}
}

@ -0,0 +1,49 @@
package files
import (
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"mime/multipart"
"net/http"
"os"
)
// 返回值说明:
// 7z、exe、doc 类型会返回 application/octet-stream 未知的文件类型
// jpg => image/jpeg
// png => image/png
// ico => image/x-icon
// bmp => image/bmp
// xlsx、docx 、zip => application/zip
// tar.gz => application/x-gzip
// txt、json、log等文本文件 => text/plain; charset=utf-8 备注就算txt是gbk、ansi编码也会识别为utf-8
// 通过文件名获取文件mime信息
func GetFilesMimeByFileName(filepath string) string {
f, err := os.Open(filepath)
if err != nil {
variable.ZapLog.Error(my_errors.ErrorsFilesUploadOpenFail + err.Error())
}
defer f.Close()
// 只需要前 32 个字节就可以了
buffer := make([]byte, 32)
if _, err := f.Read(buffer); err != nil {
variable.ZapLog.Error(my_errors.ErrorsFilesUploadReadFail + err.Error())
return ""
}
return http.DetectContentType(buffer)
}
// 通过文件指针获取文件mime信息
func GetFilesMimeByFp(fp multipart.File) string {
buffer := make([]byte, 32)
if _, err := fp.Read(buffer); err != nil {
variable.ZapLog.Error(my_errors.ErrorsFilesUploadReadFail + err.Error())
return ""
}
return http.DetectContentType(buffer)
}

@ -0,0 +1,46 @@
package gin_release
import (
"errors"
"fmt"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"goskeleton/app/global/consts"
"goskeleton/app/global/variable"
"goskeleton/app/utils/response"
"io/ioutil"
)
// ReleaseRouter 根据 gin 路由包官方的建议gin 路由引擎如果在生产模式使用,官方建议设置为 release 模式
// 官方原版提示说明:[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
// 这里我们将按照官方指导进行生产模式精细化处理
func ReleaseRouter() *gin.Engine {
// 切换到生产模式禁用 gin 输出接口访问日志经过并发测试验证可以提升5%的性能
gin.SetMode(gin.ReleaseMode)
gin.DefaultWriter = ioutil.Discard
engine := gin.New()
// 载入gin的中间件关键是第二个中间件我们对它进行了自定义重写将可能的 panic 异常等,统一使用 zaplog 接管,保证全局日志打印统一
engine.Use(gin.Logger(), CustomRecovery())
return engine
}
// CustomRecovery 自定义错误(panic等)拦截中间件、对可能发生的错误进行拦截、统一记录
func CustomRecovery() gin.HandlerFunc {
DefaultErrorWriter := &PanicExceptionRecord{}
return gin.RecoveryWithWriter(DefaultErrorWriter, func(c *gin.Context, err interface{}) {
// 这里针对发生的panic等异常进行统一响应即可
// 这里的 err 数据类型为 runtime.boundsError ,需要转为普通数据类型才可以输出
response.ErrorSystem(c, "", fmt.Sprintf("%s", err))
})
}
// PanicExceptionRecord panic等异常记录
type PanicExceptionRecord struct{}
func (p *PanicExceptionRecord) Write(b []byte) (n int, err error) {
errStr := string(b)
err = errors.New(errStr)
variable.ZapLog.Error(consts.ServerOccurredErrorMsg, zap.String("errStrace", errStr))
return len(errStr), err
}

@ -0,0 +1,190 @@
package gorm_v2
import (
"errors"
"fmt"
"go.uber.org/zap"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlserver"
"gorm.io/gorm"
gormLog "gorm.io/gorm/logger"
"gorm.io/plugin/dbresolver"
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"strings"
"time"
)
// 获取一个 mysql 客户端
func GetOneMysqlClient() (*gorm.DB, error) {
sqlType := "Mysql"
readDbIsOpen := variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + ".IsOpenReadDb")
return GetSqlDriver(sqlType, readDbIsOpen)
}
// 获取一个 sqlserver 客户端
func GetOneSqlserverClient() (*gorm.DB, error) {
sqlType := "SqlServer"
readDbIsOpen := variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + ".IsOpenReadDb")
return GetSqlDriver(sqlType, readDbIsOpen)
}
// 获取一个 postgresql 客户端
func GetOnePostgreSqlClient() (*gorm.DB, error) {
sqlType := "Postgresql"
readDbIsOpen := variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + ".IsOpenReadDb")
return GetSqlDriver(sqlType, readDbIsOpen)
}
// 获取数据库驱动, 可以通过options 动态参数连接任意多个数据库
func GetSqlDriver(sqlType string, readDbIsOpen int, dbConf ...ConfigParams) (*gorm.DB, error) {
var dbDialector gorm.Dialector
if val, err := getDbDialector(sqlType, "Write", dbConf...); err != nil {
variable.ZapLog.Error(my_errors.ErrorsDialectorDbInitFail+sqlType, zap.Error(err))
} else {
dbDialector = val
}
gormDb, err := gorm.Open(dbDialector, &gorm.Config{
SkipDefaultTransaction: true,
PrepareStmt: true,
Logger: redefineLog(sqlType), //拦截、接管 gorm v2 自带日志
})
if err != nil {
//gorm 数据库驱动初始化失败
return nil, err
}
// 如果开启了读写分离配置读数据库resource、read、replicas
// 读写分离配置只
if readDbIsOpen == 1 {
if val, err := getDbDialector(sqlType, "Read", dbConf...); err != nil {
variable.ZapLog.Error(my_errors.ErrorsDialectorDbInitFail+sqlType, zap.Error(err))
} else {
dbDialector = val
}
resolverConf := dbresolver.Config{
Replicas: []gorm.Dialector{dbDialector}, // 读 操作库,查询类
Policy: dbresolver.RandomPolicy{}, // sources/replicas 负载均衡策略适用于
}
err = gormDb.Use(dbresolver.Register(resolverConf).SetConnMaxIdleTime(time.Second * 30).
SetConnMaxLifetime(variable.ConfigGormv2Yml.GetDuration("Gormv2."+sqlType+".Read.SetConnMaxLifetime") * time.Second).
SetMaxIdleConns(variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + ".Read.SetMaxIdleConns")).
SetMaxOpenConns(variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + ".Read.SetMaxOpenConns")))
if err != nil {
return nil, err
}
}
// 查询没有数据,屏蔽 gorm v2 包中会爆出的错误
// https://github.com/go-gorm/gorm/issues/3789 此 issue 所反映的问题就是我们本次解决掉的
_ = gormDb.Callback().Query().Before("gorm:query").Register("disable_raise_record_not_found", MaskNotDataError)
// https://github.com/go-gorm/gorm/issues/4838
_ = gormDb.Callback().Create().Before("gorm:before_create").Register("CreateBeforeHook", CreateBeforeHook)
// 为了完美支持gorm的一系列回调函数
_ = gormDb.Callback().Update().Before("gorm:before_update").Register("UpdateBeforeHook", UpdateBeforeHook)
// 为主连接设置连接池(43行返回的数据库驱动指针)
if rawDb, err := gormDb.DB(); err != nil {
return nil, err
} else {
rawDb.SetConnMaxIdleTime(time.Second * 30)
rawDb.SetConnMaxLifetime(variable.ConfigGormv2Yml.GetDuration("Gormv2."+sqlType+".Write.SetConnMaxLifetime") * time.Second)
rawDb.SetMaxIdleConns(variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + ".Write.SetMaxIdleConns"))
rawDb.SetMaxOpenConns(variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + ".Write.SetMaxOpenConns"))
// 全局sql的debug配置
if variable.ConfigGormv2Yml.GetBool("Gormv2.SqlDebug") {
return gormDb.Debug(), nil
} else {
return gormDb, nil
}
}
}
// 获取一个数据库方言(Dialector),通俗的说就是根据不同的连接参数,获取具体的一类数据库的连接指针
func getDbDialector(sqlType, readWrite string, dbConf ...ConfigParams) (gorm.Dialector, error) {
var dbDialector gorm.Dialector
dsn := getDsn(sqlType, readWrite, dbConf...)
switch strings.ToLower(sqlType) {
case "mysql":
dbDialector = mysql.Open(dsn)
case "sqlserver", "mssql":
dbDialector = sqlserver.Open(dsn)
case "postgres", "postgresql", "postgre":
dbDialector = postgres.Open(dsn)
default:
return nil, errors.New(my_errors.ErrorsDbDriverNotExists + sqlType)
}
return dbDialector, nil
}
// 根据配置参数生成数据库驱动 dsn
func getDsn(sqlType, readWrite string, dbConf ...ConfigParams) string {
Host := variable.ConfigGormv2Yml.GetString("Gormv2." + sqlType + "." + readWrite + ".Host")
DataBase := variable.ConfigGormv2Yml.GetString("Gormv2." + sqlType + "." + readWrite + ".DataBase")
Port := variable.ConfigGormv2Yml.GetInt("Gormv2." + sqlType + "." + readWrite + ".Port")
User := variable.ConfigGormv2Yml.GetString("Gormv2." + sqlType + "." + readWrite + ".User")
Pass := variable.ConfigGormv2Yml.GetString("Gormv2." + sqlType + "." + readWrite + ".Pass")
Charset := variable.ConfigGormv2Yml.GetString("Gormv2." + sqlType + "." + readWrite + ".Charset")
if len(dbConf) > 0 {
if strings.ToLower(readWrite) == "write" {
if len(dbConf[0].Write.Host) > 0 {
Host = dbConf[0].Write.Host
}
if len(dbConf[0].Write.DataBase) > 0 {
DataBase = dbConf[0].Write.DataBase
}
if dbConf[0].Write.Port > 0 {
Port = dbConf[0].Write.Port
}
if len(dbConf[0].Write.User) > 0 {
User = dbConf[0].Write.User
}
if len(dbConf[0].Write.Pass) > 0 {
Pass = dbConf[0].Write.Pass
}
if len(dbConf[0].Write.Charset) > 0 {
Charset = dbConf[0].Write.Charset
}
} else {
if len(dbConf[0].Read.Host) > 0 {
Host = dbConf[0].Read.Host
}
if len(dbConf[0].Read.DataBase) > 0 {
DataBase = dbConf[0].Read.DataBase
}
if dbConf[0].Read.Port > 0 {
Port = dbConf[0].Read.Port
}
if len(dbConf[0].Read.User) > 0 {
User = dbConf[0].Read.User
}
if len(dbConf[0].Read.Pass) > 0 {
Pass = dbConf[0].Read.Pass
}
if len(dbConf[0].Read.Charset) > 0 {
Charset = dbConf[0].Read.Charset
}
}
}
switch strings.ToLower(sqlType) {
case "mysql":
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=false&loc=Local", User, Pass, Host, Port, DataBase, Charset)
case "sqlserver", "mssql":
return fmt.Sprintf("server=%s;port=%d;database=%s;user id=%s;password=%s;encrypt=disable", Host, Port, DataBase, User, Pass)
case "postgresql", "postgre", "postgres":
return fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=disable TimeZone=Asia/Shanghai", Host, Port, DataBase, User, Pass)
}
return ""
}
// 创建自定义日志模块,对 gorm 日志进行拦截、
func redefineLog(sqlType string) gormLog.Interface {
return createCustomGormLog(sqlType,
SetInfoStrFormat("[info] %s\n"), SetWarnStrFormat("[warn] %s\n"), SetErrStrFormat("[error] %s\n"),
SetTraceStrFormat("[traceStr] %s [%.3fms] [rows:%v] %s\n"), SetTracWarnStrFormat("[traceWarn] %s %s [%.3fms] [rows:%v] %s\n"), SetTracErrStrFormat("[traceErr] %s %s [%.3fms] [rows:%v] %s\n"))
}

@ -0,0 +1,19 @@
package gorm_v2
// 数据库参数配置,结构体
// 用于解决复杂的业务场景连接到多台服务器部署的 mysql、sqlserver、postgresql 数据库
// 具体用法参见常用开发模块:多源数据库的操作
type ConfigParams struct {
Write ConfigParamsDetail
Read ConfigParamsDetail
}
type ConfigParamsDetail struct {
Host string
DataBase string
Port int
Prefix string
User string
Pass string
Charset string
}

@ -0,0 +1,174 @@
package gorm_v2
import (
"context"
"errors"
"fmt"
"go.uber.org/zap"
gormLog "gorm.io/gorm/logger"
"gorm.io/gorm/utils"
"goskeleton/app/global/variable"
"strings"
"time"
)
// 自定义日志格式, 对 gorm 自带日志进行拦截重写
func createCustomGormLog(sqlType string, options ...Options) gormLog.Interface {
var (
infoStr = "%s\n[info] "
warnStr = "%s\n[warn] "
errStr = "%s\n[error] "
traceStr = "%s\n[%.3fms] [rows:%v] %s"
traceWarnStr = "%s %s\n[%.3fms] [rows:%v] %s"
traceErrStr = "%s %s\n[%.3fms] [rows:%v] %s"
)
logConf := gormLog.Config{
SlowThreshold: time.Second * variable.ConfigGormv2Yml.GetDuration("Gormv2."+sqlType+".SlowThreshold"),
LogLevel: gormLog.Warn,
Colorful: false,
}
log := &logger{
Writer: logOutPut{},
Config: logConf,
infoStr: infoStr,
warnStr: warnStr,
errStr: errStr,
traceStr: traceStr,
traceWarnStr: traceWarnStr,
traceErrStr: traceErrStr,
}
for _, val := range options {
val.apply(log)
}
return log
}
type logOutPut struct{}
func (l logOutPut) Printf(strFormat string, args ...interface{}) {
logRes := fmt.Sprintf(strFormat, args...)
logFlag := "gorm_v2 日志:"
detailFlag := "详情:"
if strings.HasPrefix(strFormat, "[info]") || strings.HasPrefix(strFormat, "[traceStr]") {
variable.ZapLog.Info(logFlag, zap.String(detailFlag, logRes))
} else if strings.HasPrefix(strFormat, "[error]") || strings.HasPrefix(strFormat, "[traceErr]") {
variable.ZapLog.Error(logFlag, zap.String(detailFlag, logRes))
} else if strings.HasPrefix(strFormat, "[warn]") || strings.HasPrefix(strFormat, "[traceWarn]") {
variable.ZapLog.Warn(logFlag, zap.String(detailFlag, logRes))
}
}
// 尝试从外部重写内部相关的格式化变量
type Options interface {
apply(*logger)
}
type OptionFunc func(log *logger)
func (f OptionFunc) apply(log *logger) {
f(log)
}
// 定义 6 个函数修改内部变量
func SetInfoStrFormat(format string) Options {
return OptionFunc(func(log *logger) {
log.infoStr = format
})
}
func SetWarnStrFormat(format string) Options {
return OptionFunc(func(log *logger) {
log.warnStr = format
})
}
func SetErrStrFormat(format string) Options {
return OptionFunc(func(log *logger) {
log.errStr = format
})
}
func SetTraceStrFormat(format string) Options {
return OptionFunc(func(log *logger) {
log.traceStr = format
})
}
func SetTracWarnStrFormat(format string) Options {
return OptionFunc(func(log *logger) {
log.traceWarnStr = format
})
}
func SetTracErrStrFormat(format string) Options {
return OptionFunc(func(log *logger) {
log.traceErrStr = format
})
}
type logger struct {
gormLog.Writer
gormLog.Config
infoStr, warnStr, errStr string
traceStr, traceErrStr, traceWarnStr string
}
// LogMode log mode
func (l *logger) LogMode(level gormLog.LogLevel) gormLog.Interface {
newlogger := *l
newlogger.LogLevel = level
return &newlogger
}
// Info print info
func (l logger) Info(_ context.Context, msg string, data ...interface{}) {
if l.LogLevel >= gormLog.Info {
l.Printf(l.infoStr+msg, append([]interface{}{utils.FileWithLineNum()}, data...)...)
}
}
// Warn print warn messages
func (l logger) Warn(_ context.Context, msg string, data ...interface{}) {
if l.LogLevel >= gormLog.Warn {
l.Printf(l.warnStr+msg, append([]interface{}{utils.FileWithLineNum()}, data...)...)
}
}
// Error print error messages
func (l logger) Error(_ context.Context, msg string, data ...interface{}) {
if l.LogLevel >= gormLog.Error {
l.Printf(l.errStr+msg, append([]interface{}{utils.FileWithLineNum()}, data...)...)
}
}
// Trace print sql message
func (l logger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
if l.LogLevel <= gormLog.Silent {
return
}
elapsed := time.Since(begin)
switch {
case err != nil && l.LogLevel >= gormLog.Error && (!errors.Is(err, gormLog.ErrRecordNotFound) || !l.IgnoreRecordNotFoundError):
sql, rows := fc()
if rows == -1 {
l.Printf(l.traceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, "-1", sql)
} else {
l.Printf(l.traceErrStr, utils.FileWithLineNum(), err, float64(elapsed.Nanoseconds())/1e6, rows, sql)
}
case elapsed > l.SlowThreshold && l.SlowThreshold != 0 && l.LogLevel >= gormLog.Warn:
sql, rows := fc()
slowLog := fmt.Sprintf("SLOW SQL >= %v", l.SlowThreshold)
if rows == -1 {
l.Printf(l.traceWarnStr, utils.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, "-1", sql)
} else {
l.Printf(l.traceWarnStr, utils.FileWithLineNum(), slowLog, float64(elapsed.Nanoseconds())/1e6, rows, sql)
}
case l.LogLevel == gormLog.Info:
sql, rows := fc()
if rows == -1 {
l.Printf(l.traceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, "-1", sql)
} else {
l.Printf(l.traceStr, utils.FileWithLineNum(), float64(elapsed.Nanoseconds())/1e6, rows, sql)
}
}
}

@ -0,0 +1,166 @@
package gorm_v2
import (
"gorm.io/gorm"
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"reflect"
"strings"
"time"
)
// 这里的函数都是gorm的hook函数拦截一些官方我们认为不合格的操作行为提升项目整体的完美性
// MaskNotDataError 解决gorm v2 包在查询无数据时报错问题record not found但是官方认为报错是应该是我们认为查询无数据代码一切ok不应该报错
func MaskNotDataError(gormDB *gorm.DB) {
gormDB.Statement.RaiseErrorOnNotFound = false
}
// InterceptCreatePramsNotPtrError 拦截 create 函数参数如果是非指针类型的错误,新用户最容犯此错误
func CreateBeforeHook(gormDB *gorm.DB) {
if reflect.TypeOf(gormDB.Statement.Dest).Kind() != reflect.Ptr {
variable.ZapLog.Warn(my_errors.ErrorsGormDBCreateParamsNotPtr)
} else {
destValueOf := reflect.ValueOf(gormDB.Statement.Dest).Elem()
if destValueOf.Type().Kind() == reflect.Slice || destValueOf.Type().Kind() == reflect.Array {
inLen := destValueOf.Len()
for i := 0; i < inLen; i++ {
row := destValueOf.Index(i)
if row.Type().Kind() == reflect.Struct {
if b, column := structHasSpecialField("CreatedAt", row); b {
destValueOf.Index(i).FieldByName(column).Set(reflect.ValueOf(time.Now().Format(variable.DateFormat)))
}
if b, column := structHasSpecialField("UpdatedAt", row); b {
destValueOf.Index(i).FieldByName(column).Set(reflect.ValueOf(time.Now().Format(variable.DateFormat)))
}
} else if row.Type().Kind() == reflect.Map {
if b, column := structHasSpecialField("created_at", row); b {
row.SetMapIndex(reflect.ValueOf(column), reflect.ValueOf(time.Now().Format(variable.DateFormat)))
}
if b, column := structHasSpecialField("updated_at", row); b {
row.SetMapIndex(reflect.ValueOf(column), reflect.ValueOf(time.Now().Format(variable.DateFormat)))
}
}
}
} else if destValueOf.Type().Kind() == reflect.Struct {
// if destValueOf.Type().Kind() == reflect.Struct
// 参数校验无错误自动设置 CreatedAt、 UpdatedAt
if b, column := structHasSpecialField("CreatedAt", gormDB.Statement.Dest); b {
gormDB.Statement.SetColumn(column, time.Now().Format(variable.DateFormat))
}
if b, column := structHasSpecialField("UpdatedAt", gormDB.Statement.Dest); b {
gormDB.Statement.SetColumn(column, time.Now().Format(variable.DateFormat))
}
} else if destValueOf.Type().Kind() == reflect.Map {
if b, column := structHasSpecialField("created_at", gormDB.Statement.Dest); b {
destValueOf.SetMapIndex(reflect.ValueOf(column), reflect.ValueOf(time.Now().Format(variable.DateFormat)))
}
if b, column := structHasSpecialField("updated_at", gormDB.Statement.Dest); b {
destValueOf.SetMapIndex(reflect.ValueOf(column), reflect.ValueOf(time.Now().Format(variable.DateFormat)))
}
}
}
}
// UpdateBeforeHook
// InterceptUpdatePramsNotPtrError 拦截 save、update 函数参数如果是非指针类型的错误
// 对于开发者来说,以结构体形式更新数,只需要在 update 、save 函数的参数前面添加 & 即可
// 最终就可以完美兼支持、兼容 gorm 的所有回调函数
// 但是如果是指定字段更新,例如: UpdateColumn 函数则只传递值即可,不需要做校验
func UpdateBeforeHook(gormDB *gorm.DB) {
if reflect.TypeOf(gormDB.Statement.Dest).Kind() == reflect.Struct {
//_ = gormDB.AddError(errors.New(my_errors.ErrorsGormDBUpdateParamsNotPtr))
variable.ZapLog.Warn(my_errors.ErrorsGormDBUpdateParamsNotPtr)
} else if reflect.TypeOf(gormDB.Statement.Dest).Kind() == reflect.Map {
// 如果是调用了 gorm.Update 、updates 函数 , 在参数没有传递指针的情况下,无法触发回调函数
} else if reflect.TypeOf(gormDB.Statement.Dest).Kind() == reflect.Ptr && reflect.ValueOf(gormDB.Statement.Dest).Elem().Kind() == reflect.Struct {
// 参数校验无错误自动设置 UpdatedAt
if b, column := structHasSpecialField("UpdatedAt", gormDB.Statement.Dest); b {
gormDB.Statement.SetColumn(column, time.Now().Format(variable.DateFormat))
}
} else if reflect.TypeOf(gormDB.Statement.Dest).Kind() == reflect.Ptr && reflect.ValueOf(gormDB.Statement.Dest).Elem().Kind() == reflect.Map {
if b, column := structHasSpecialField("updated_at", gormDB.Statement.Dest); b {
destValueOf := reflect.ValueOf(gormDB.Statement.Dest).Elem()
destValueOf.SetMapIndex(reflect.ValueOf(column), reflect.ValueOf(time.Now().Format(variable.DateFormat)))
}
}
}
// structHasSpecialField 检查结构体是否有特定字段
func structHasSpecialField(fieldName string, anyStructPtr interface{}) (bool, string) {
var tmp reflect.Type
if reflect.TypeOf(anyStructPtr).Kind() == reflect.Ptr && reflect.ValueOf(anyStructPtr).Elem().Kind() == reflect.Map {
destValueOf := reflect.ValueOf(anyStructPtr).Elem()
for _, item := range destValueOf.MapKeys() {
if item.String() == fieldName {
return true, fieldName
}
}
} else if reflect.TypeOf(anyStructPtr).Kind() == reflect.Ptr && reflect.ValueOf(anyStructPtr).Elem().Kind() == reflect.Struct {
destValueOf := reflect.ValueOf(anyStructPtr).Elem()
tf := destValueOf.Type()
for i := 0; i < tf.NumField(); i++ {
if !tf.Field(i).Anonymous && tf.Field(i).Type.Kind() != reflect.Struct {
if tf.Field(i).Name == fieldName {
return true, getColumnNameFromGormTag(fieldName, tf.Field(i).Tag.Get("gorm"))
}
} else if tf.Field(i).Type.Kind() == reflect.Struct {
tmp = tf.Field(i).Type
for j := 0; j < tmp.NumField(); j++ {
if tmp.Field(j).Name == fieldName {
return true, getColumnNameFromGormTag(fieldName, tmp.Field(j).Tag.Get("gorm"))
}
}
}
}
} else if reflect.Indirect(anyStructPtr.(reflect.Value)).Type().Kind() == reflect.Struct {
// 处理结构体
destValueOf := anyStructPtr.(reflect.Value)
tf := destValueOf.Type()
for i := 0; i < tf.NumField(); i++ {
if !tf.Field(i).Anonymous && tf.Field(i).Type.Kind() != reflect.Struct {
if tf.Field(i).Name == fieldName {
return true, getColumnNameFromGormTag(fieldName, tf.Field(i).Tag.Get("gorm"))
}
} else if tf.Field(i).Type.Kind() == reflect.Struct {
tmp = tf.Field(i).Type
for j := 0; j < tmp.NumField(); j++ {
if tmp.Field(j).Name == fieldName {
return true, getColumnNameFromGormTag(fieldName, tmp.Field(j).Tag.Get("gorm"))
}
}
}
}
} else if reflect.Indirect(anyStructPtr.(reflect.Value)).Type().Kind() == reflect.Map {
destValueOf := anyStructPtr.(reflect.Value)
for _, item := range destValueOf.MapKeys() {
if item.String() == fieldName {
return true, fieldName
}
}
}
return false, ""
}
// getColumnNameFromGormTag 从 gorm 标签中获取字段名
// @defaultColumn 如果没有 gormcolumn 标签为字段重命名,则使用默认字段名
// @TagValue 字段中含有的gorm"column:created_at" 标签值可能的格式1. column:created_at 、2. default:null; column:created_at 、3. column:created_at; default:null
func getColumnNameFromGormTag(defaultColumn, TagValue string) (str string) {
pos1 := strings.Index(TagValue, "column:")
if pos1 == -1 {
str = defaultColumn
return
} else {
TagValue = TagValue[pos1+7:]
}
pos2 := strings.Index(TagValue, ";")
if pos2 == -1 {
str = TagValue
} else {
str = TagValue[:pos2]
}
return strings.ReplaceAll(str, " ", "")
}

@ -0,0 +1,18 @@
package md5_encrypt
import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
)
func MD5(params string) string {
md5Ctx := md5.New()
md5Ctx.Write([]byte(params))
return hex.EncodeToString(md5Ctx.Sum(nil))
}
//先base64然后MD5
func Base64Md5(params string) string {
return MD5(base64.StdEncoding.EncodeToString([]byte(params)))
}

@ -0,0 +1,7 @@
package observer_mode
// 观察者角色Observer接口
type ObserverInterface interface {
// 接收状态更新消息
Update(*Subject)
}

@ -0,0 +1,43 @@
package observer_mode
import "container/list"
// 观察者管理中心subject
type Subject struct {
Observers *list.List
params interface{}
}
//注册观察者角色
func (s *Subject) Attach(observe ObserverInterface) {
s.Observers.PushBack(observe)
}
//删除观察者角色
func (s *Subject) Detach(observer ObserverInterface) {
for ob := s.Observers.Front(); ob != nil; ob = ob.Next() {
if ob.Value.(*ObserverInterface) == &observer {
s.Observers.Remove(ob)
break
}
}
}
//通知所有观察者
func (s *Subject) Notify() {
var l_temp *list.List = list.New()
for ob := s.Observers.Front(); ob != nil; ob = ob.Next() {
l_temp.PushBack(ob.Value)
ob.Value.(ObserverInterface).Update(s)
}
s.Observers = l_temp
}
func (s *Subject) BroadCast(args ...interface{}) {
s.params = args
s.Notify()
}
func (s *Subject) GetParams() interface{} {
return s.params
}

@ -0,0 +1,11 @@
package error_record
import "goskeleton/app/global/variable"
// ErrorDeal 记录错误
func ErrorDeal(err error) error {
if err != nil {
variable.ZapLog.Error(err.Error())
}
return err
}

@ -0,0 +1,161 @@
package hello_world
import (
amqp "github.com/rabbitmq/amqp091-go"
"goskeleton/app/global/variable"
"goskeleton/app/utils/rabbitmq/error_record"
"time"
)
func CreateConsumer() (*consumer, error) {
// 获取配置信息
conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.HelloWorld.Addr"))
queueName := variable.ConfigYml.GetString("RabbitMq.HelloWorld.QueueName")
durable := variable.ConfigYml.GetBool("RabbitMq.HelloWorld.Durable")
chanNumber := variable.ConfigYml.GetInt("RabbitMq.HelloWorld.ConsumerChanNumber")
reconnectInterval := variable.ConfigYml.GetDuration("RabbitMq.HelloWorld.OffLineReconnectIntervalSec")
retryTimes := variable.ConfigYml.GetInt("RabbitMq.HelloWorld.RetryCount")
if err != nil {
//log.Println(err.Error())
return nil, err
}
cons := &consumer{
connect: conn,
queueName: queueName,
durable: durable,
chanNumber: chanNumber,
connErr: conn.NotifyClose(make(chan *amqp.Error, 1)),
offLineReconnectIntervalSec: reconnectInterval,
retryTimes: retryTimes,
receivedMsgBlocking: make(chan struct{}),
status: 1,
}
return cons, nil
}
// 定义一个消息队列结构体helloworld 模型
type consumer struct {
connect *amqp.Connection
queueName string
durable bool
chanNumber int
occurError error
connErr chan *amqp.Error
callbackForReceived func(receivedData string) // 断线重连,结构体内部使用
offLineReconnectIntervalSec time.Duration
retryTimes int
callbackOffLine func(err *amqp.Error) // 断线重连,结构体内部使用
receivedMsgBlocking chan struct{} // 接受消息时用于阻塞消息处理函数
status byte // 客户端状态1=正常0=异常
}
// Received 接收、处理消息
func (c *consumer) Received(callbackFunDealSmg func(receivedData string)) {
defer func() {
c.close()
}()
// 将回调函数地址赋值给结构体变量,用于掉线重连使用
c.callbackForReceived = callbackFunDealSmg
for i := 1; i <= c.chanNumber; i++ {
go func(chanNo int) {
ch, err := c.connect.Channel()
c.occurError = error_record.ErrorDeal(err)
defer func() {
_ = ch.Close()
}()
queue, err := ch.QueueDeclare(
c.queueName,
c.durable,
true,
false,
false,
nil,
)
c.occurError = error_record.ErrorDeal(err)
if err != nil {
return
}
msgs, err := ch.Consume(
queue.Name,
"", // 消费者标记,请确保在一个消息通道唯一
true, //是否自动确认,这里设置为 true自动确认
false, //是否私有队列false标识允许多个 consumer 向该队列投递消息true 表示独占
false, //RabbitMQ不支持noLocal标志。
false, // 队列如果已经在服务器声明,设置为 true ,否则设置为 false
nil,
)
c.occurError = error_record.ErrorDeal(err)
if err == nil {
for {
select {
case msg := <-msgs:
// 消息处理
if c.status == 1 && len(msg.Body) > 0 {
callbackFunDealSmg(string(msg.Body))
} else if c.status == 0 {
return
}
}
}
} else {
return
}
}(i)
}
if _, isOk := <-c.receivedMsgBlocking; isOk {
c.status = 0
close(c.receivedMsgBlocking)
}
}
// OnConnectionError 消费者端,掉线重连监听器
func (c *consumer) OnConnectionError(callbackOfflineErr func(err *amqp.Error)) {
c.callbackOffLine = callbackOfflineErr
go func() {
select {
case err := <-c.connErr:
var i = 1
for i = 1; i <= c.retryTimes; i++ {
// 自动重连机制
time.Sleep(c.offLineReconnectIntervalSec * time.Second)
// 发生连接错误时,中断原来的消息监听(包括关闭连接)
if c.status == 1 {
c.receivedMsgBlocking <- struct{}{}
}
conn, err := CreateConsumer()
if err != nil {
continue
} else {
go func() {
c.connErr = conn.connect.NotifyClose(make(chan *amqp.Error, 1))
go conn.OnConnectionError(c.callbackOffLine)
conn.Received(c.callbackForReceived)
}()
// 新的客户端重连成功后,释放旧的回调函数 - OnConnectionError
if c.status == 0 {
return
}
break
}
}
if i > c.retryTimes {
callbackOfflineErr(err)
// 如果超过最大重连次数,同样需要释放回调函数 - OnConnectionError
if c.status == 0 {
return
}
}
}
}()
}
// close 关闭连接
func (c *consumer) close() {
_ = c.connect.Close()
}

@ -0,0 +1,87 @@
package hello_world
import (
amqp "github.com/rabbitmq/amqp091-go"
"goskeleton/app/global/variable"
"goskeleton/app/utils/rabbitmq/error_record"
)
// CreateProducer 创建一个生产者
func CreateProducer() (*producer, error) {
// 获取配置信息
conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.HelloWorld.Addr"))
queueName := variable.ConfigYml.GetString("RabbitMq.HelloWorld.QueueName")
dura := variable.ConfigYml.GetBool("RabbitMq.HelloWorld.Durable")
if err != nil {
variable.ZapLog.Error(err.Error())
return nil, err
}
prod := &producer{
connect: conn,
queueName: queueName,
durable: dura,
}
return prod, nil
}
// 定义一个消息队列结构体helloworld 模型
type producer struct {
connect *amqp.Connection
queueName string
durable bool
occurError error
}
func (p *producer) Send(data string) bool {
// 获取一个通道
ch, err := p.connect.Channel()
p.occurError = error_record.ErrorDeal(err)
defer func() {
_ = ch.Close()
}()
// 声明消息队列
_, err = ch.QueueDeclare(
p.queueName, // 队列名称
p.durable, //是否持久化false模式数据全部处于内存true会保存在erlang自带数据库但是影响速度
!p.durable, //生产者、消费者全部断开时是否删除队列。一般来说,数据需要持久化,就不删除;非持久化,就删除
false, //是否私有队列false标识允许多个 consumer 向该队列投递消息true 表示独占
false, // 队列如果已经在服务器声明,设置为 true ,否则设置为 false
nil, // 相关参数
)
p.occurError = error_record.ErrorDeal(err)
// 如果队列的声明是持久化的,那么消息也设置为持久化
msgPersistent := amqp.Transient
if p.durable {
msgPersistent = amqp.Persistent
}
// 投递消息
if err == nil {
err = ch.Publish(
"", // helloworld 、workqueue 模式设置为空字符串,表示使用默认交换机
p.queueName, // routing key注意简单模式与队列名称相同
false,
false,
amqp.Publishing{
DeliveryMode: msgPersistent, //消息是否持久化,这里与保持保持一致即可
ContentType: "text/plain",
Body: []byte(data),
})
}
p.occurError = error_record.ErrorDeal(err)
if p.occurError != nil { // 发生错误,返回 false
return false
} else {
return true
}
}
// Close 发送完毕手动关闭这样不影响send多次发送数据
func (p *producer) Close() {
_ = p.connect.Close()
}

@ -0,0 +1,193 @@
package publish_subscribe
import (
amqp "github.com/rabbitmq/amqp091-go"
"goskeleton/app/global/variable"
"goskeleton/app/utils/rabbitmq/error_record"
"time"
)
func CreateConsumer(options ...OptionsConsumer) (*consumer, error) {
// 获取配置信息
conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.Addr"))
exchangeType := variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.ExchangeType")
exchangeName := variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.ExchangeName")
queueName := variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.QueueName")
durable := variable.ConfigYml.GetBool("RabbitMq.PublishSubscribe.Durable")
chanNumber := variable.ConfigYml.GetInt("RabbitMq.PublishSubscribe.ConsumerChanNumber")
reconnectInterval := variable.ConfigYml.GetDuration("RabbitMq.PublishSubscribe.OffLineReconnectIntervalSec")
retryTimes := variable.ConfigYml.GetInt("RabbitMq.PublishSubscribe.RetryCount")
if err != nil {
return nil, err
}
cons := &consumer{
connect: conn,
exchangeType: exchangeType,
exchangeName: exchangeName,
queueName: queueName,
durable: durable,
chanNumber: chanNumber,
connErr: conn.NotifyClose(make(chan *amqp.Error, 1)),
offLineReconnectIntervalSec: reconnectInterval,
retryTimes: retryTimes,
receivedMsgBlocking: make(chan struct{}),
status: 1,
}
// rabbitmq 如果启动了延迟消息队列模式。继续初始化一些参数
for _, val := range options {
val.apply(cons)
}
return cons, nil
}
// 定义一个消息队列结构体PublishSubscribe 模型
type consumer struct {
connect *amqp.Connection
exchangeType string
exchangeName string
queueName string
durable bool
chanNumber int
occurError error
connErr chan *amqp.Error
callbackForReceived func(receivedData string) // 断线重连,结构体内部使用
offLineReconnectIntervalSec time.Duration
retryTimes int
callbackOffLine func(err *amqp.Error) // 断线重连,结构体内部使用
enableDelayMsgPlugin bool // 是否使用延迟队列模式
receivedMsgBlocking chan struct{} // 接受消息时用于阻塞消息处理函数
status byte // 客户端状态1=正常0=异常
}
// Received 接收、处理消息
func (c *consumer) Received(callbackFunDealMsg func(receivedData string)) {
defer func() {
c.close()
}()
// 将回调函数地址赋值给结构体变量,用于掉线重连使用
c.callbackForReceived = callbackFunDealMsg
for i := 1; i <= c.chanNumber; i++ {
go func(chanNo int) {
ch, err := c.connect.Channel()
c.occurError = error_record.ErrorDeal(err)
defer func() {
_ = ch.Close()
}()
// 声明exchange交换机
err = ch.ExchangeDeclare(
c.exchangeName, //exchange name
c.exchangeType, //exchange kind
c.durable, //数据是否持久化
!c.durable, //所有连接断开时,交换机是否删除
false,
false,
nil,
)
// 声明队列
queue, err := ch.QueueDeclare(
c.queueName,
c.durable,
true,
false,
false,
nil,
)
c.occurError = error_record.ErrorDeal(err)
if err != nil {
return
}
//队列绑定
err = ch.QueueBind(
queue.Name,
"", //routing key fanout 模式设置为 空 即可
c.exchangeName,
false,
nil,
)
c.occurError = error_record.ErrorDeal(err)
msgs, err := ch.Consume(
queue.Name, // 队列名称
"", // 消费者标记,请确保在一个消息频道唯一
true, //是否自动确认,这里设置为 true自动确认
false, //是否私有队列false标识允许多个 consumer 向该队列投递消息true 表示独占
false, //RabbitMQ不支持noLocal标志。
false, // 队列如果已经在服务器声明,设置为 true ,否则设置为 false
nil,
)
c.occurError = error_record.ErrorDeal(err)
if err == nil {
for {
select {
case msg := <-msgs:
// 消息处理
if c.status == 1 && len(msg.Body) > 0 {
callbackFunDealMsg(string(msg.Body))
} else if c.status == 0 {
return
}
}
}
} else {
return
}
}(i)
}
if _, isOk := <-c.receivedMsgBlocking; isOk {
c.status = 0
close(c.receivedMsgBlocking)
}
}
// OnConnectionError 消费者端,掉线重连失败后的错误回调
func (c *consumer) OnConnectionError(callbackOfflineErr func(err *amqp.Error)) {
c.callbackOffLine = callbackOfflineErr
go func() {
select {
case err := <-c.connErr:
var i = 1
for i = 1; i <= c.retryTimes; i++ {
// 自动重连机制
time.Sleep(c.offLineReconnectIntervalSec * time.Second)
// 发生连接错误时,中断原来的消息监听(包括关闭连接)
if c.status == 1 {
c.receivedMsgBlocking <- struct{}{}
}
conn, err := CreateConsumer()
if err != nil {
continue
} else {
go func() {
c.connErr = conn.connect.NotifyClose(make(chan *amqp.Error, 1))
go conn.OnConnectionError(c.callbackOffLine)
conn.Received(c.callbackForReceived)
}()
// 新的客户端重连成功后,释放旧的回调函数 - OnConnectionError
if c.status == 0 {
return
}
break
}
}
if i > c.retryTimes {
callbackOfflineErr(err)
// 如果超过最大重连次数,同样需要释放回调函数 - OnConnectionError
if c.status == 0 {
return
}
}
}
}()
}
// close 关闭连接
func (c *consumer) close() {
_ = c.connect.Close()
}

@ -0,0 +1,62 @@
package publish_subscribe
import (
amqp "github.com/rabbitmq/amqp091-go"
"goskeleton/app/global/variable"
)
// 等 go 泛型稳定以后,生产者和消费者初始化参数的设置,本段代码就可以继续精简
// 目前 apply(*producer) 的参数只能固定为生产者或者消费者其中之一的具体类型
// 1.生产者初始化参数定义
// OptionsProd 定义动态设置参数接口
type OptionsProd interface {
apply(*producer)
}
// OptionFunc 以函数形式实现上面的接口
type OptionFunc func(*producer)
func (f OptionFunc) apply(prod *producer) {
f(prod)
}
// SetProdMsgDelayParams 开发者设置生产者初始化时的参数
func SetProdMsgDelayParams(enableMsgDelayPlugin bool) OptionsProd {
return OptionFunc(func(p *producer) {
p.enableDelayMsgPlugin = enableMsgDelayPlugin
p.exchangeType = "x-delayed-message"
p.args = amqp.Table{
"x-delayed-type": "fanout",
}
p.exchangeName = variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.DelayedExchangeName")
// 延迟消息队列,交换机、消息全部设置为持久
p.durable = true
})
}
// 2.消费者端初始化参数定义
// OptionsConsumer 定义动态设置参数接口
type OptionsConsumer interface {
apply(*consumer)
}
// OptionsConsumerFunc 以函数形式实现上面的接口
type OptionsConsumerFunc func(*consumer)
func (f OptionsConsumerFunc) apply(cons *consumer) {
f(cons)
}
// SetConsMsgDelayParams 开发者设置消费者端初始化时的参数
func SetConsMsgDelayParams(enableDelayMsgPlugin bool) OptionsConsumer {
return OptionsConsumerFunc(func(c *consumer) {
c.enableDelayMsgPlugin = enableDelayMsgPlugin
c.exchangeType = "x-delayed-message"
c.exchangeName = variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.DelayedExchangeName")
// 延迟消息队列,交换机、消息全部设置为持久
c.durable = true
})
}

@ -0,0 +1,108 @@
package publish_subscribe
import (
amqp "github.com/rabbitmq/amqp091-go"
"goskeleton/app/global/variable"
"goskeleton/app/utils/rabbitmq/error_record"
)
// CreateProducer 创建一个生产者
func CreateProducer(options ...OptionsProd) (*producer, error) {
// 获取配置信息
conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.Addr"))
exchangeType := variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.ExchangeType")
exchangeName := variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.ExchangeName")
queueName := variable.ConfigYml.GetString("RabbitMq.PublishSubscribe.QueueName")
durable := variable.ConfigYml.GetBool("RabbitMq.PublishSubscribe.Durable")
if err != nil {
variable.ZapLog.Error(err.Error())
return nil, err
}
prod := &producer{
connect: conn,
exchangeType: exchangeType,
exchangeName: exchangeName,
queueName: queueName,
durable: durable,
args: nil,
}
// 加载用户设置的参数
for _, val := range options {
val.apply(prod)
}
return prod, nil
}
// 定义一个消息队列结构体PublishSubscribe 模型
type producer struct {
connect *amqp.Connection
exchangeType string
exchangeName string
queueName string
durable bool
occurError error
enableDelayMsgPlugin bool
args amqp.Table
}
// Send 发送消息
// 参数:
// data 发送的数据、
// delayMillisecond 延迟时间(毫秒),只有启用了消息延迟插件才有效果
func (p *producer) Send(data string, delayMillisecond int) bool {
// 获取一个频道
ch, err := p.connect.Channel()
p.occurError = error_record.ErrorDeal(err)
defer func() {
_ = ch.Close()
}()
// 声明交换机,该模式生产者只负责将消息投递到交换机即可
err = ch.ExchangeDeclare(
p.exchangeName, //交换器名称
p.exchangeType, //fanout 模式(扇形模式,发布/订阅 模式) ,解决 发布、订阅场景相关的问题
p.durable, //durable
!p.durable, //autodelete
false,
false,
p.args,
)
p.occurError = error_record.ErrorDeal(err)
// 如果队列的声明是持久化的,那么消息也设置为持久化
msgPersistent := amqp.Transient
if p.durable {
msgPersistent = amqp.Persistent
}
// 投递消息
if err == nil {
err = ch.Publish(
p.exchangeName, // 交换机名称
p.queueName, // fanout 模式默认为空,表示所有订阅的消费者会接受到相同的消息
false,
false,
amqp.Publishing{
DeliveryMode: msgPersistent, //消息是否持久化,这里与保持保持一致即可
ContentType: "text/plain",
Body: []byte(data),
Headers: amqp.Table{
"x-delay": delayMillisecond, // 延迟时间: 毫秒
},
})
}
p.occurError = error_record.ErrorDeal(err)
if p.occurError != nil { // 发生错误,返回 false
return false
} else {
return true
}
}
// Close 发送完毕手动关闭这样不影响send多次发送数据
func (p *producer) Close() {
_ = p.connect.Close()
}

@ -0,0 +1,191 @@
package routing
import (
amqp "github.com/rabbitmq/amqp091-go"
"goskeleton/app/global/variable"
"goskeleton/app/utils/rabbitmq/error_record"
"time"
)
func CreateConsumer(options ...OptionsConsumer) (*consumer, error) {
// 获取配置信息
conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.Routing.Addr"))
exchangeType := variable.ConfigYml.GetString("RabbitMq.Routing.ExchangeType")
exchangeName := variable.ConfigYml.GetString("RabbitMq.Routing.ExchangeName")
queueName := variable.ConfigYml.GetString("RabbitMq.Routing.QueueName")
durable := variable.ConfigYml.GetBool("RabbitMq.Routing.Durable")
reconnectInterval := variable.ConfigYml.GetDuration("RabbitMq.Routing.OffLineReconnectIntervalSec")
retryTimes := variable.ConfigYml.GetInt("RabbitMq.Routing.RetryCount")
if err != nil {
return nil, err
}
cons := &consumer{
connect: conn,
exchangeType: exchangeType,
exchangeName: exchangeName,
queueName: queueName,
durable: durable,
connErr: conn.NotifyClose(make(chan *amqp.Error, 1)),
offLineReconnectIntervalSec: reconnectInterval,
retryTimes: retryTimes,
receivedMsgBlocking: make(chan struct{}),
status: 1,
}
// rabbitmq 如果启动了延迟消息队列模式。继续初始化一些参数
for _, val := range options {
val.apply(cons)
}
return cons, nil
}
// 定义一个消息队列结构体Routing 模型
type consumer struct {
connect *amqp.Connection
exchangeType string
exchangeName string
queueName string
durable bool
occurError error
connErr chan *amqp.Error
routeKey string // 断线重连,结构体内部使用
callbackForReceived func(receivedData string) // 断线重连,结构体内部使用
offLineReconnectIntervalSec time.Duration
retryTimes int
callbackOffLine func(err *amqp.Error) // 断线重连,结构体内部使用
enableDelayMsgPlugin bool // 是否使用延迟队列模式
receivedMsgBlocking chan struct{} // 接受消息时用于阻塞消息处理函数
status byte // 客户端状态1=正常0=异常
}
// Received 接收、处理消息
func (c *consumer) Received(routeKey string, callbackFunDealMsg func(receivedData string)) {
defer func() {
c.close()
}()
// 将回调函数地址赋值给结构体变量,用于掉线重连使用
c.routeKey = routeKey
c.callbackForReceived = callbackFunDealMsg
go func(key string) {
ch, err := c.connect.Channel()
c.occurError = error_record.ErrorDeal(err)
defer func() {
_ = ch.Close()
}()
// 声明exchange交换机
err = ch.ExchangeDeclare(
c.exchangeName, //exchange name
c.exchangeType, //exchange kind
c.durable, //数据是否持久化
!c.durable, //所有连接断开时,交换机是否删除
false,
false,
nil,
)
// 声明队列
queue, err := ch.QueueDeclare(
c.queueName,
c.durable,
true,
false,
false,
nil,
)
c.occurError = error_record.ErrorDeal(err)
//队列绑定
err = ch.QueueBind(
queue.Name,
key, // routing 模式,生产者会将消息投递至交换机的route_key 消费者匹配不同的key获取消息、处理
c.exchangeName,
false,
nil,
)
c.occurError = error_record.ErrorDeal(err)
if err != nil {
return
}
msgs, err := ch.Consume(
queue.Name, // 队列名称
"", // 消费者标记,请确保在一个消息频道唯一
true, //是否自动确认,这里设置为 true自动确认
false, //是否私有队列false标识允许多个 consumer 向该队列投递消息true 表示独占
false, //RabbitMQ不支持noLocal标志。
false, // 队列如果已经在服务器声明,设置为 true ,否则设置为 false
nil,
)
c.occurError = error_record.ErrorDeal(err)
if err == nil {
for {
select {
case msg := <-msgs:
// 消息处理
if c.status == 1 && len(msg.Body) > 0 {
callbackFunDealMsg(string(msg.Body))
} else if c.status == 0 {
return
}
}
}
} else {
return
}
}(routeKey)
if _, isOk := <-c.receivedMsgBlocking; isOk {
c.status = 0
close(c.receivedMsgBlocking)
}
}
// OnConnectionError 消费者端,掉线重连失败后的错误回调
func (c *consumer) OnConnectionError(callbackOfflineErr func(err *amqp.Error)) {
c.callbackOffLine = callbackOfflineErr
go func() {
select {
case err := <-c.connErr:
var i = 1
for i = 1; i <= c.retryTimes; i++ {
// 自动重连机制
time.Sleep(c.offLineReconnectIntervalSec * time.Second)
// 发生连接错误时,中断原来的消息监听(包括关闭连接)
if c.status == 1 {
c.receivedMsgBlocking <- struct{}{}
}
conn, err := CreateConsumer()
if err != nil {
continue
} else {
go func() {
c.connErr = conn.connect.NotifyClose(make(chan *amqp.Error, 1))
go conn.OnConnectionError(c.callbackOffLine)
conn.Received(c.routeKey, c.callbackForReceived)
}()
// 新的客户端重连成功后,释放旧的回调函数 - OnConnectionError
if c.status == 0 {
return
}
break
}
}
if i > c.retryTimes {
callbackOfflineErr(err)
// 如果超过最大重连次数,同样需要释放回调函数 - OnConnectionError
if c.status == 0 {
return
}
}
}
}()
}
// close 关闭连接
func (c *consumer) close() {
_ = c.connect.Close()
}

@ -0,0 +1,62 @@
package routing
import (
amqp "github.com/rabbitmq/amqp091-go"
"goskeleton/app/global/variable"
)
// 等 go 泛型稳定以后,生产者和消费者初始化参数的设置,本段代码就可以继续精简
// 目前 apply(*producer) 的参数只能固定为生产者或者消费者其中之一的具体类型
// 1.生产者初始化参数定义
// OptionsProd 定义动态设置参数接口
type OptionsProd interface {
apply(*producer)
}
// OptionFunc 以函数形式实现上面的接口
type OptionFunc func(*producer)
func (f OptionFunc) apply(prod *producer) {
f(prod)
}
// SetProdMsgDelayParams 开发者设置生产者初始化时的参数
func SetProdMsgDelayParams(enableMsgDelayPlugin bool) OptionsProd {
return OptionFunc(func(p *producer) {
p.enableDelayMsgPlugin = enableMsgDelayPlugin
p.exchangeType = "x-delayed-message"
p.args = amqp.Table{
"x-delayed-type": "direct",
}
p.exchangeName = variable.ConfigYml.GetString("RabbitMq.Routing.DelayedExchangeName")
// 延迟消息队列,交换机、消息全部设置为持久
p.durable = true
})
}
// 2.消费者端初始化参数定义
// OptionsConsumer 定义动态设置参数接口
type OptionsConsumer interface {
apply(*consumer)
}
// OptionsConsumerFunc 以函数形式实现上面的接口
type OptionsConsumerFunc func(*consumer)
func (f OptionsConsumerFunc) apply(cons *consumer) {
f(cons)
}
// SetConsMsgDelayParams 开发者设置消费者端初始化时的参数
func SetConsMsgDelayParams(enableDelayMsgPlugin bool) OptionsConsumer {
return OptionsConsumerFunc(func(c *consumer) {
c.enableDelayMsgPlugin = enableDelayMsgPlugin
c.exchangeType = "x-delayed-message"
c.exchangeName = variable.ConfigYml.GetString("RabbitMq.Routing.DelayedExchangeName")
// 延迟消息队列,交换机、消息全部设置为持久
c.durable = true
})
}

@ -0,0 +1,107 @@
package routing
import (
amqp "github.com/rabbitmq/amqp091-go"
"goskeleton/app/global/variable"
"goskeleton/app/utils/rabbitmq/error_record"
)
// CreateProducer 创建一个生产者
func CreateProducer(options ...OptionsProd) (*producer, error) {
// 获取配置信息
conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.Routing.Addr"))
exchangeType := variable.ConfigYml.GetString("RabbitMq.Routing.ExchangeType")
exchangeName := variable.ConfigYml.GetString("RabbitMq.Routing.ExchangeName")
queueName := variable.ConfigYml.GetString("RabbitMq.Routing.QueueName")
durable := variable.ConfigYml.GetBool("RabbitMq.Routing.Durable")
if err != nil {
variable.ZapLog.Error(err.Error())
return nil, err
}
prod := &producer{
connect: conn,
exchangeType: exchangeType,
exchangeName: exchangeName,
queueName: queueName,
durable: durable,
}
// 加载用户设置的参数
for _, val := range options {
val.apply(prod)
}
return prod, nil
}
// 定义一个消息队列结构体Routing 模型
type producer struct {
connect *amqp.Connection
exchangeType string
exchangeName string
queueName string
durable bool
occurError error
enableDelayMsgPlugin bool // 是否使用延迟队列模式
args amqp.Table
}
// Send 发送消息
// 参数:
// routeKey 路由键、
// data 发送的数据、
// delayMillisecond 延迟时间(毫秒),只有启用了消息延迟插件才有效果
func (p *producer) Send(routeKey, data string, delayMillisecond int) bool {
// 获取一个频道
ch, err := p.connect.Channel()
p.occurError = error_record.ErrorDeal(err)
defer func() {
_ = ch.Close()
}()
// 声明交换机,该模式生产者只负责将消息投递到交换机即可
err = ch.ExchangeDeclare(
p.exchangeName, //交换器名称
p.exchangeType, //direct(定向消息), 按照路由键名匹配消息
p.durable, //消息是否持久化
!p.durable, //交换器是否自动删除
false,
false,
p.args,
)
p.occurError = error_record.ErrorDeal(err)
// 如果队列的声明是持久化的,那么消息也设置为持久化
msgPersistent := amqp.Transient
if p.durable {
msgPersistent = amqp.Persistent
}
// 投递消息
if err == nil {
err = ch.Publish(
p.exchangeName, // 交换机名称
routeKey, // direct 模式默认为空即可
false,
false,
amqp.Publishing{
DeliveryMode: msgPersistent, //消息是否持久化,这里与保持保持一致即可
ContentType: "text/plain",
Body: []byte(data),
Headers: amqp.Table{
"x-delay": delayMillisecond, // 延迟时间: 毫秒
},
})
}
p.occurError = error_record.ErrorDeal(err)
if p.occurError != nil { // 发生错误,返回 false
return false
} else {
return true
}
}
// Close 发送完毕手动关闭这样不影响send多次发送数据
func (p *producer) Close() {
_ = p.connect.Close()
}

@ -0,0 +1,191 @@
package topics
import (
amqp "github.com/rabbitmq/amqp091-go"
"goskeleton/app/global/variable"
"goskeleton/app/utils/rabbitmq/error_record"
"time"
)
func CreateConsumer(options ...OptionsConsumer) (*consumer, error) {
// 获取配置信息
conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.Topics.Addr"))
exchangeType := variable.ConfigYml.GetString("RabbitMq.Topics.ExchangeType")
exchangeName := variable.ConfigYml.GetString("RabbitMq.Topics.ExchangeName")
queueName := variable.ConfigYml.GetString("RabbitMq.Topics.QueueName")
durable := variable.ConfigYml.GetBool("RabbitMq.Topics.Durable")
reconnectInterval := variable.ConfigYml.GetDuration("RabbitMq.Topics.OffLineReconnectIntervalSec")
retryTimes := variable.ConfigYml.GetInt("RabbitMq.Topics.RetryCount")
if err != nil {
return nil, err
}
cons := &consumer{
connect: conn,
exchangeType: exchangeType,
exchangeName: exchangeName,
queueName: queueName,
durable: durable,
connErr: conn.NotifyClose(make(chan *amqp.Error, 1)),
offLineReconnectIntervalSec: reconnectInterval,
retryTimes: retryTimes,
receivedMsgBlocking: make(chan struct{}),
status: 1,
}
// 加载用户设置的参数
for _, val := range options {
val.apply(cons)
}
return cons, nil
}
// 定义一个消息队列结构体Topics 模型
type consumer struct {
connect *amqp.Connection
exchangeType string
exchangeName string
queueName string
durable bool
occurError error // 记录初始化过程中的错误
connErr chan *amqp.Error
routeKey string // 断线重连,结构体内部使用
callbackForReceived func(receivedData string) // 断线重连,结构体内部使用
offLineReconnectIntervalSec time.Duration
retryTimes int
callbackOffLine func(err *amqp.Error) // 断线重连,结构体内部使用
enableDelayMsgPlugin bool // 是否使用延迟队列模式
receivedMsgBlocking chan struct{} // 接受消息时用于阻塞消息处理函数
status byte // 客户端状态1=正常0=异常
}
// Received 接收、处理消息
func (c *consumer) Received(routeKey string, callbackFunDealMsg func(receivedData string)) {
defer func() {
c.close()
}()
// 将回调函数地址赋值给结构体变量,用于掉线重连使用
c.routeKey = routeKey
c.callbackForReceived = callbackFunDealMsg
go func(key string) {
ch, err := c.connect.Channel()
c.occurError = error_record.ErrorDeal(err)
defer func() {
_ = ch.Close()
}()
// 声明exchange交换机
err = ch.ExchangeDeclare(
c.exchangeName, //exchange name
c.exchangeType, //exchange kind
c.durable, //数据是否持久化
!c.durable, //所有连接断开时,交换机是否删除
false,
false,
nil,
)
// 声明队列
queue, err := ch.QueueDeclare(
c.queueName,
c.durable,
true,
false,
false,
nil,
)
c.occurError = error_record.ErrorDeal(err)
//队列绑定
err = ch.QueueBind(
queue.Name,
key, // Topics 模式,生产者会将消息投递至交换机的route_key 消费者匹配不同的key获取消息、处理
c.exchangeName,
false,
nil,
)
c.occurError = error_record.ErrorDeal(err)
if err != nil {
return
}
msgs, err := ch.Consume(
queue.Name, // 队列名称
"", // 消费者标记,请确保在一个消息频道唯一
true, //是否自动确认,这里设置为 true自动确认
false, //是否私有队列false标识允许多个 consumer 向该队列投递消息true 表示独占
false, //RabbitMQ不支持noLocal标志。
false, // 队列如果已经在服务器声明,设置为 true ,否则设置为 false
nil,
)
c.occurError = error_record.ErrorDeal(err)
if err == nil {
for {
select {
case msg := <-msgs:
// 消息处理
if c.status == 1 && len(msg.Body) > 0 {
callbackFunDealMsg(string(msg.Body))
} else if c.status == 0 {
return
}
}
}
} else {
return
}
}(routeKey)
if _, isOk := <-c.receivedMsgBlocking; isOk {
c.status = 0
close(c.receivedMsgBlocking)
}
}
// OnConnectionError 消费者端,掉线重连失败后的错误回调
func (c *consumer) OnConnectionError(callbackOfflineErr func(err *amqp.Error)) {
c.callbackOffLine = callbackOfflineErr
go func() {
select {
case err := <-c.connErr:
var i = 1
for i = 1; i <= c.retryTimes; i++ {
// 自动重连机制
time.Sleep(c.offLineReconnectIntervalSec * time.Second)
// 发生连接错误时,中断原来的消息监听(包括关闭连接)
if c.status == 1 {
c.receivedMsgBlocking <- struct{}{}
}
conn, err := CreateConsumer()
if err != nil {
continue
} else {
go func() {
c.connErr = conn.connect.NotifyClose(make(chan *amqp.Error, 1))
go conn.OnConnectionError(c.callbackOffLine)
conn.Received(c.routeKey, c.callbackForReceived)
}()
// 新的客户端重连成功后,释放旧的回调函数 - OnConnectionError
if c.status == 0 {
return
}
break
}
}
if i > c.retryTimes {
callbackOfflineErr(err)
// 如果超过最大重连次数,同样需要释放回调函数 - OnConnectionError
if c.status == 0 {
return
}
}
}
}()
}
// close 关闭连接
func (c *consumer) close() {
_ = c.connect.Close()
}

@ -0,0 +1,62 @@
package topics
import (
amqp "github.com/rabbitmq/amqp091-go"
"goskeleton/app/global/variable"
)
// 等 go 泛型稳定以后,生产者和消费者初始化参数的设置,本段代码就可以继续精简
// 目前 apply(*producer) 的参数只能固定为生产者或者消费者其中之一的具体类型
// 1.生产者初始化参数定义
// OptionsProd 定义动态设置参数接口
type OptionsProd interface {
apply(*producer)
}
// OptionFunc 以函数形式实现上面的接口
type OptionFunc func(*producer)
func (f OptionFunc) apply(prod *producer) {
f(prod)
}
// SetProdMsgDelayParams 开发者设置生产者初始化时的参数
func SetProdMsgDelayParams(enableMsgDelayPlugin bool) OptionsProd {
return OptionFunc(func(p *producer) {
p.enableDelayMsgPlugin = enableMsgDelayPlugin
p.exchangeType = "x-delayed-message"
p.args = amqp.Table{
"x-delayed-type": "topic",
}
p.exchangeName = variable.ConfigYml.GetString("RabbitMq.Topics.DelayedExchangeName")
// 延迟消息队列,交换机、消息全部设置为持久
p.durable = true
})
}
// 2.消费者端初始化参数定义
// OptionsConsumer 定义动态设置参数接口
type OptionsConsumer interface {
apply(*consumer)
}
// OptionsConsumerFunc 以函数形式实现上面的接口
type OptionsConsumerFunc func(*consumer)
func (f OptionsConsumerFunc) apply(cons *consumer) {
f(cons)
}
// SetConsMsgDelayParams 开发者设置消费者端初始化时的参数
func SetConsMsgDelayParams(enableDelayMsgPlugin bool) OptionsConsumer {
return OptionsConsumerFunc(func(c *consumer) {
c.enableDelayMsgPlugin = enableDelayMsgPlugin
c.exchangeType = "x-delayed-message"
c.exchangeName = variable.ConfigYml.GetString("RabbitMq.Topics.DelayedExchangeName")
// 延迟消息队列,交换机、消息全部设置为持久
c.durable = true
})
}

@ -0,0 +1,107 @@
package topics
import (
amqp "github.com/rabbitmq/amqp091-go"
"goskeleton/app/global/variable"
"goskeleton/app/utils/rabbitmq/error_record"
)
// CreateProducer 创建一个生产者
func CreateProducer(options ...OptionsProd) (*producer, error) {
// 获取配置信息
conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.Topics.Addr"))
exchangeType := variable.ConfigYml.GetString("RabbitMq.Topics.ExchangeType")
exchangeName := variable.ConfigYml.GetString("RabbitMq.Topics.ExchangeName")
queueName := variable.ConfigYml.GetString("RabbitMq.Topics.QueueName")
durable := variable.ConfigYml.GetBool("RabbitMq.Topics.Durable")
if err != nil {
variable.ZapLog.Error(err.Error())
return nil, err
}
prod := &producer{
connect: conn,
exchangeType: exchangeType,
exchangeName: exchangeName,
queueName: queueName,
durable: durable,
}
// 加载用户设置的参数
for _, val := range options {
val.apply(prod)
}
return prod, nil
}
// 定义一个消息队列结构体Topics 模型
type producer struct {
connect *amqp.Connection
exchangeType string
exchangeName string
queueName string
durable bool
occurError error
enableDelayMsgPlugin bool // 是否使用延迟队列模式
args amqp.Table
}
// Send 发送消息
// 参数:
// routeKey 路由键、
// data 发送的数据、
// delayMillisecond 延迟时间(毫秒),只有启用了消息延迟插件才有效果
func (p *producer) Send(routeKey, data string, delayMillisecond int) bool {
// 获取一个频道
ch, err := p.connect.Channel()
p.occurError = error_record.ErrorDeal(err)
defer func() {
_ = ch.Close()
}()
// 声明交换机,该模式生产者只负责将消息投递到交换机即可
err = ch.ExchangeDeclare(
p.exchangeName, //交换器名称
p.exchangeType, //topic模式
p.durable, //交换机是否持久化
!p.durable, //交换器是否自动删除
false,
false,
p.args,
)
p.occurError = error_record.ErrorDeal(err)
// 如果交换机是持久化的,那么消息也设置为持久化
msgPersistent := amqp.Transient
if p.durable {
msgPersistent = amqp.Persistent
}
// 投递消息
if err == nil {
err = ch.Publish(
p.exchangeName, // 交换机名称
routeKey, // topics 模式默认为空即可
false,
false,
amqp.Publishing{
DeliveryMode: msgPersistent, //消息是否持久化,这里与保持保持一致即可
ContentType: "text/plain",
Body: []byte(data),
Headers: amqp.Table{
"x-delay": delayMillisecond, // 延迟时间: 毫秒
},
})
}
p.occurError = error_record.ErrorDeal(err)
if p.occurError != nil { // 发生错误,返回 false
return false
} else {
return true
}
}
// Close 发送完毕手动关闭这样不影响send多次发送数据
func (p *producer) Close() {
_ = p.connect.Close()
}

@ -0,0 +1,167 @@
package work_queue
import (
amqp "github.com/rabbitmq/amqp091-go"
"goskeleton/app/global/variable"
"goskeleton/app/utils/rabbitmq/error_record"
"time"
)
func CreateConsumer() (*consumer, error) {
// 获取配置信息
conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.WorkQueue.Addr"))
queueName := variable.ConfigYml.GetString("RabbitMq.WorkQueue.QueueName")
durable := variable.ConfigYml.GetBool("RabbitMq.WorkQueue.Durable")
chanNumber := variable.ConfigYml.GetInt("RabbitMq.WorkQueue.ConsumerChanNumber")
reconnectInterval := variable.ConfigYml.GetDuration("RabbitMq.WorkQueue.OffLineReconnectIntervalSec")
retryTimes := variable.ConfigYml.GetInt("RabbitMq.WorkQueue.RetryCount")
if err != nil {
return nil, err
}
cons := &consumer{
connect: conn,
queueName: queueName,
durable: durable,
chanNumber: chanNumber,
connErr: conn.NotifyClose(make(chan *amqp.Error, 1)),
offLineReconnectIntervalSec: reconnectInterval,
retryTimes: retryTimes,
receivedMsgBlocking: make(chan struct{}),
status: 1,
}
return cons, nil
}
// 定义一个消息队列结构体WorkQueue 模型
type consumer struct {
connect *amqp.Connection
queueName string
durable bool
chanNumber int
occurError error
connErr chan *amqp.Error
callbackForReceived func(receivedData string) // 断线重连,结构体内部使用
offLineReconnectIntervalSec time.Duration
retryTimes int
callbackOffLine func(err *amqp.Error) // 断线重连,结构体内部使用
receivedMsgBlocking chan struct{} // 接受消息时用于阻塞消息处理函数
status byte // 客户端状态1=正常0=异常
}
// Received 接收、处理消息
func (c *consumer) Received(callbackFunDealMsg func(receivedData string)) {
defer func() {
c.close()
}()
// 将回调函数地址赋值给结构体变量,用于掉线重连使用
c.callbackForReceived = callbackFunDealMsg
for i := 1; i <= c.chanNumber; i++ {
go func(chanNo int) {
ch, err := c.connect.Channel()
c.occurError = error_record.ErrorDeal(err)
defer func() {
_ = ch.Close()
}()
q, err := ch.QueueDeclare(
c.queueName,
c.durable,
true,
false,
false,
nil,
)
c.occurError = error_record.ErrorDeal(err)
err = ch.Qos(
1, // 大于0服务端将会传递该数量的消息到消费者端进行待处理通俗地说就是消费者端积压消息的数量最大值
0, // prefetch size
false, // false 表示本连接只针对本频道有效true表示应用到本连接的所有频道
)
c.occurError = error_record.ErrorDeal(err)
if err != nil {
return
}
msgs, err := ch.Consume(
q.Name,
"", // 消费者标记,请确保在一个消息频道唯一
true, //是否自动确认,这里设置为 true 自动确认,如果是 false 后面需要调用 ack 函数确认
false, //是否私有队列false标识允许多个 consumer 向该队列投递消息true 表示独占
false, //RabbitMQ不支持noLocal标志。
false, // 队列如果已经在服务器声明,设置为 true ,否则设置为 false
nil,
)
c.occurError = error_record.ErrorDeal(err)
if err == nil {
for {
select {
case msg := <-msgs:
// 消息处理
if c.status == 1 && len(msg.Body) > 0 {
callbackFunDealMsg(string(msg.Body))
} else if c.status == 0 {
return
}
}
}
} else {
return
}
}(i)
}
if _, isOk := <-c.receivedMsgBlocking; isOk {
c.status = 0
close(c.receivedMsgBlocking)
}
}
// OnConnectionError 消费者端,掉线重连失败后的错误回调
func (c *consumer) OnConnectionError(callbackOfflineErr func(err *amqp.Error)) {
c.callbackOffLine = callbackOfflineErr
go func() {
select {
case err := <-c.connErr:
var i = 1
for i = 1; i <= c.retryTimes; i++ {
// 自动重连机制
time.Sleep(c.offLineReconnectIntervalSec * time.Second)
// 发生连接错误时,中断原来的消息监听(包括关闭连接)
if c.status == 1 {
c.receivedMsgBlocking <- struct{}{}
}
conn, err := CreateConsumer()
if err != nil {
continue
} else {
go func() {
c.connErr = conn.connect.NotifyClose(make(chan *amqp.Error, 1))
go conn.OnConnectionError(c.callbackOffLine)
conn.Received(c.callbackForReceived)
}()
// 新的客户端重连成功后,释放旧的回调函数 - OnConnectionError
if c.status == 0 {
return
}
break
}
}
if i > c.retryTimes {
callbackOfflineErr(err)
// 如果超过最大重连次数,同样需要释放回调函数 - OnConnectionError
if c.status == 0 {
return
}
}
}
}()
}
// close 关闭连接
func (c *consumer) close() {
_ = c.connect.Close()
}

@ -0,0 +1,88 @@
package work_queue
import (
amqp "github.com/rabbitmq/amqp091-go"
"goskeleton/app/global/variable"
"goskeleton/app/utils/rabbitmq/error_record"
)
// CreateProducer 创建一个生产者
func CreateProducer() (*producer, error) {
// 获取配置信息
conn, err := amqp.Dial(variable.ConfigYml.GetString("RabbitMq.WorkQueue.Addr"))
queueName := variable.ConfigYml.GetString("RabbitMq.WorkQueue.QueueName")
durable := variable.ConfigYml.GetBool("RabbitMq.WorkQueue.Durable")
if err != nil {
variable.ZapLog.Error(err.Error())
return nil, err
}
prod := &producer{
connect: conn,
queueName: queueName,
durable: durable,
}
return prod, nil
}
// 定义一个消息队列结构体helloworld 模型
type producer struct {
connect *amqp.Connection
queueName string
durable bool
occurError error
}
func (p *producer) Send(data string) bool {
// 获取一个频道
ch, err := p.connect.Channel()
p.occurError = error_record.ErrorDeal(err)
defer func() {
_ = ch.Close()
}()
// 声明消息队列
_, err = ch.QueueDeclare(
p.queueName, // 队列名称
p.durable, //队列是否持久化false模式数据全部处于内存true会保存在erlang自带数据库但是影响速度
!p.durable, //生产者、消费者全部断开时是否删除队列。一般来说,数据需要持久化,就不删除;非持久化,就删除
false, //是否私有队列false标识允许多个 consumer 向该队列投递消息true 表示独占
false, // 队列如果已经在服务器声明,设置为 true ,否则设置为 false
nil, // 相关参数
)
p.occurError = error_record.ErrorDeal(err)
// 如果队列的声明是持久化的,那么消息也设置为持久化
msgPersistent := amqp.Transient
if p.durable {
msgPersistent = amqp.Persistent
}
// 投递消息
if err == nil {
err = ch.Publish(
"", // helloworld 、workqueue 模式设置为空字符串,表示使用默认交换机
p.queueName, // 注意:简单模式 key 表示队列名称
false,
false,
amqp.Publishing{
DeliveryMode: msgPersistent, //消息是否持久化,这里与保持保持一致即可
ContentType: "text/plain",
Body: []byte(data),
})
}
p.occurError = error_record.ErrorDeal(err)
if p.occurError != nil { // 发生错误,返回 false
return false
} else {
return true
}
}
// Close 发送完毕手动关闭这样不影响send多次发送数据
func (p *producer) Close() {
_ = p.connect.Close()
}

@ -0,0 +1,162 @@
package redis_factory
import (
"github.com/gomodule/redigo/redis"
"go.uber.org/zap"
"goskeleton/app/core/event_manage"
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"goskeleton/app/utils/yml_config"
"goskeleton/app/utils/yml_config/ymlconfig_interf"
"time"
)
var redisPool *redis.Pool
var configYml ymlconfig_interf.YmlConfigInterf
// 处于程序底层的包init 初始化的代码段的执行会优先于上层代码,因此这里读取配置项不能使用全局配置项变量
func init() {
configYml = yml_config.CreateYamlFactory()
redisPool = initRedisClientPool()
}
func initRedisClientPool() *redis.Pool {
redisPool = &redis.Pool{
MaxIdle: configYml.GetInt("Redis.MaxIdle"), //最大空闲数
MaxActive: configYml.GetInt("Redis.MaxActive"), //最大活跃数
IdleTimeout: configYml.GetDuration("Redis.IdleTimeout") * time.Second, //最大的空闲连接等待时间,超过此时间后,空闲连接将被关闭
Dial: func() (redis.Conn, error) {
//此处对应redis ip及端口号
conn, err := redis.Dial("tcp", configYml.GetString("Redis.Host")+":"+configYml.GetString("Redis.Port"))
if err != nil {
variable.ZapLog.Error(my_errors.ErrorsRedisInitConnFail + err.Error())
return nil, err
}
auth := configYml.GetString("Redis.Auth") //通过配置项设置redis密码
if len(auth) >= 1 {
if _, err := conn.Do("AUTH", auth); err != nil {
_ = conn.Close()
variable.ZapLog.Error(my_errors.ErrorsRedisAuthFail + err.Error())
}
}
_, _ = conn.Do("select", configYml.GetInt("Redis.IndexDb"))
return conn, err
},
}
// 将redis的关闭事件注册在全局事件统一管理器由程序退出时统一销毁
eventManageFactory := event_manage.CreateEventManageFactory()
if _, exists := eventManageFactory.Get(variable.EventDestroyPrefix + "Redis"); exists == false {
eventManageFactory.Set(variable.EventDestroyPrefix+"Redis", func(args ...interface{}) {
_ = redisPool.Close()
})
}
return redisPool
}
// 从连接池获取一个redis连接
func GetOneRedisClient() *RedisClient {
maxRetryTimes := configYml.GetInt("Redis.ConnFailRetryTimes")
var oneConn redis.Conn
for i := 1; i <= maxRetryTimes; i++ {
oneConn = redisPool.Get()
// 首先通过执行一个获取时间的命令检测连接是否有效如果已有的连接无法执行命令则重新尝试连接到redis服务器获取新的连接池地址
// 连接不可用可能会发生的场景主要有服务端redis重启、客户端网络在有线和无线之间切换等
if _, replyErr := oneConn.Do("time"); replyErr != nil {
//fmt.Printf("连接已经失效(出错)%+v\n", replyErr.Error())
// 如果已有的redis连接池获取连接出错(官方库的说法是连接不可用),那么继续使用从新初始化连接池
initRedisClientPool()
oneConn = redisPool.Get()
}
if err := oneConn.Err(); err != nil {
//variable.ZapLog.Error("Redis网络中断,开始重连进行中..." , zap.Error(oneConn.Err()))
if i == maxRetryTimes {
variable.ZapLog.Error(my_errors.ErrorsRedisGetConnFail, zap.Error(oneConn.Err()))
return nil
}
//如果出现网络短暂的抖动,短暂休眠后,支持自动重连
time.Sleep(time.Second * configYml.GetDuration("Redis.ReConnectInterval"))
} else {
break
}
}
return &RedisClient{oneConn}
}
// 定义一个redis客户端结构体
type RedisClient struct {
client redis.Conn
}
// 为redis-go 客户端封装统一操作函数入口
func (r *RedisClient) Execute(cmd string, args ...interface{}) (interface{}, error) {
return r.client.Do(cmd, args...)
}
// 释放连接到连接池
func (r *RedisClient) ReleaseOneRedisClient() {
_ = r.client.Close()
}
// 封装几个数据类型转换的函数
// bool 类型转换
func (r *RedisClient) Bool(reply interface{}, err error) (bool, error) {
return redis.Bool(reply, err)
}
// string 类型转换
func (r *RedisClient) String(reply interface{}, err error) (string, error) {
return redis.String(reply, err)
}
// string map 类型转换
func (r *RedisClient) StringMap(reply interface{}, err error) (map[string]string, error) {
return redis.StringMap(reply, err)
}
// strings 类型转换
func (r *RedisClient) Strings(reply interface{}, err error) ([]string, error) {
return redis.Strings(reply, err)
}
// Float64 类型转换
func (r *RedisClient) Float64(reply interface{}, err error) (float64, error) {
return redis.Float64(reply, err)
}
// int 类型转换
func (r *RedisClient) Int(reply interface{}, err error) (int, error) {
return redis.Int(reply, err)
}
// int64 类型转换
func (r *RedisClient) Int64(reply interface{}, err error) (int64, error) {
return redis.Int64(reply, err)
}
// int map 类型转换
func (r *RedisClient) IntMap(reply interface{}, err error) (map[string]int, error) {
return redis.IntMap(reply, err)
}
// Int64Map 类型转换
func (r *RedisClient) Int64Map(reply interface{}, err error) (map[string]int64, error) {
return redis.Int64Map(reply, err)
}
// int64s 类型转换
func (r *RedisClient) Int64s(reply interface{}, err error) ([]int64, error) {
return redis.Int64s(reply, err)
}
// uint64 类型转换
func (r *RedisClient) Uint64(reply interface{}, err error) (uint64, error) {
return redis.Uint64(reply, err)
}
// Bytes 类型转换
func (r *RedisClient) Bytes(reply interface{}, err error) ([]byte, error) {
return redis.Bytes(reply, err)
}
// 以上封装了很多最常见类型转换函数,其他您可以参考以上格式自行封装

@ -0,0 +1,102 @@
package response
import (
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
"goskeleton/app/global/consts"
"goskeleton/app/global/my_errors"
"goskeleton/app/utils/validator_translation"
"net/http"
"strings"
)
func ReturnJson(Context *gin.Context, httpCode int, dataCode int, msg string, data interface{}) {
//Context.Header("key2020","value2020") //可以根据实际情况在头部添加额外的其他信息
Context.JSON(httpCode, gin.H{
"code": dataCode,
"msg": msg,
"data": data,
})
}
//ReturnJsonFromString 将json字符窜以标准json格式返回例如从redis读取json格式的字符串返回给浏览器json格式
func ReturnJsonFromString(Context *gin.Context, httpCode int, jsonStr string) {
Context.Header("Content-Type", "application/json; charset=utf-8")
Context.String(httpCode, jsonStr)
}
// 语法糖函数封装
//Success 直接返回成功
func Success(c *gin.Context, msg string, data interface{}) {
ReturnJson(c, http.StatusOK, consts.CurdStatusOkCode, msg, data)
}
//Fail 失败的业务逻辑
func Fail(c *gin.Context, dataCode int, msg string, data interface{}) {
ReturnJson(c, http.StatusBadRequest, dataCode, msg, data)
c.Abort()
}
// ErrorTokenBaseInfo token 基本的格式错误
func ErrorTokenBaseInfo(c *gin.Context) {
ReturnJson(c, http.StatusBadRequest, http.StatusBadRequest, my_errors.ErrorsTokenBaseInfo, "")
//终止可能已经被加载的其他回调函数的执行
c.Abort()
}
//ErrorTokenAuthFail token 权限校验失败
func ErrorTokenAuthFail(c *gin.Context) {
ReturnJson(c, http.StatusUnauthorized, http.StatusUnauthorized, my_errors.ErrorsNoAuthorization, "")
//终止可能已经被加载的其他回调函数的执行
c.Abort()
}
//ErrorTokenRefreshFail token不符合刷新条件
func ErrorTokenRefreshFail(c *gin.Context) {
ReturnJson(c, http.StatusUnauthorized, http.StatusUnauthorized, my_errors.ErrorsRefreshTokenFail, "")
//终止可能已经被加载的其他回调函数的执行
c.Abort()
}
//token 参数校验错误
func TokenErrorParam(c *gin.Context, wrongParam interface{}) {
ReturnJson(c, http.StatusUnauthorized, consts.ValidatorParamsCheckFailCode, consts.ValidatorParamsCheckFailMsg, wrongParam)
c.Abort()
}
// ErrorCasbinAuthFail 鉴权失败,返回 405 方法不允许访问
func ErrorCasbinAuthFail(c *gin.Context, msg interface{}) {
ReturnJson(c, http.StatusMethodNotAllowed, http.StatusMethodNotAllowed, my_errors.ErrorsCasbinNoAuthorization, msg)
c.Abort()
}
//ErrorParam 参数校验错误
func ErrorParam(c *gin.Context, wrongParam interface{}) {
ReturnJson(c, http.StatusBadRequest, consts.ValidatorParamsCheckFailCode, consts.ValidatorParamsCheckFailMsg, wrongParam)
c.Abort()
}
// ErrorSystem 系统执行代码错误
func ErrorSystem(c *gin.Context, msg string, data interface{}) {
ReturnJson(c, http.StatusInternalServerError, consts.ServerOccurredErrorCode, consts.ServerOccurredErrorMsg+msg, data)
c.Abort()
}
// ValidatorError 翻译表单参数验证器出现的校验错误
func ValidatorError(c *gin.Context, err error) {
if errs, ok := err.(validator.ValidationErrors); ok {
wrongParam := validator_translation.RemoveTopStruct(errs.Translate(validator_translation.Trans))
ReturnJson(c, http.StatusBadRequest, consts.ValidatorParamsCheckFailCode, consts.ValidatorParamsCheckFailMsg, wrongParam)
} else {
errStr := err.Error()
// multipart:nextpart:eof 错误表示验证器需要一些参数,但是调用者没有提交任何参数
if strings.ReplaceAll(strings.ToLower(errStr), " ", "") == "multipart:nextpart:eof" {
ReturnJson(c, http.StatusBadRequest, consts.ValidatorParamsCheckFailCode, consts.ValidatorParamsCheckFailMsg, gin.H{"tips": my_errors.ErrorNotAllParamsIsBlank})
} else {
ReturnJson(c, http.StatusBadRequest, consts.ValidatorParamsCheckFailCode, consts.ValidatorParamsCheckFailMsg, gin.H{"tips": errStr})
}
}
c.Abort()
}

@ -0,0 +1,47 @@
package snow_flake
import (
"goskeleton/app/global/consts"
"goskeleton/app/global/variable"
"goskeleton/app/utils/snow_flake/snowflake_interf"
"sync"
"time"
)
// 创建一个雪花算法生成器(生成工厂)
func CreateSnowflakeFactory() snowflake_interf.InterfaceSnowFlake {
return &snowflake{
timestamp: 0,
machineId: variable.ConfigYml.GetInt64("SnowFlake.SnowFlakeMachineId"),
sequence: 0,
}
}
type snowflake struct {
sync.Mutex
timestamp int64
machineId int64
sequence int64
}
// 生成分布式ID
func (s *snowflake) GetId() int64 {
s.Lock()
defer func() {
s.Unlock()
}()
now := time.Now().UnixNano() / 1e6
if s.timestamp == now {
s.sequence = (s.sequence + 1) & consts.SequenceMask
if s.sequence == 0 {
for now <= s.timestamp {
now = time.Now().UnixNano() / 1e6
}
}
} else {
s.sequence = 0
}
s.timestamp = now
r := (now-consts.StartTimeStamp)<<consts.TimestampShift | (s.machineId << consts.MachineIdShift) | (s.sequence)
return r
}

@ -0,0 +1,5 @@
package snowflake_interf
type InterfaceSnowFlake interface {
GetId() int64
}

@ -0,0 +1,66 @@
package validator_translation
import (
"fmt"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
enTranslations "github.com/go-playground/validator/v10/translations/en"
zhTranslations "github.com/go-playground/validator/v10/translations/zh"
"reflect"
"strings"
)
//Trans 定义一个全局翻译器T
var Trans ut.Translator
//InitTrans 初始化表单参数验证器的翻译器
func InitTrans(locale string) (err error) {
// 修改gin框架中的Validator引擎属性实现自定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册一个获取json tag的自定义方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
//初始化翻译器
zhT := zh.New()
enT := en.New()
// 第一个参数是备用fallback的语言环境
// 后面的参数是应该支持的语言环境(支持多个)
// uni := ut.New(zhT, zhT) 也是可以的
uni := ut.New(enT, zhT, enT)
// locale 通常取决于 http 请求头的 'Accept-Language'
// 也可以使用 uni.FindTranslator(...) 传入多个locale进行查找
Trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s) failed", locale)
}
//注册翻译器
//默认注册英文en 注册英文 zh 注册中文
switch locale {
case "en":
err = enTranslations.RegisterDefaultTranslations(v, Trans)
case "zh":
err = zhTranslations.RegisterDefaultTranslations(v, Trans)
default:
err = enTranslations.RegisterDefaultTranslations(v, Trans)
}
return
}
return
}
//RemoveTopStruct 将返回的结构体名去除掉,只留下需要的字段名
func RemoveTopStruct(fields map[string]string) map[string]string {
res := map[string]string{}
for field, err := range fields {
res[field[strings.LastIndex(field, ".")+1:]] = err
}
return res
}

@ -0,0 +1,195 @@
package core
import (
"errors"
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"goskeleton/app/service/websocket/on_open_success"
"net/http"
"sync"
"time"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"go.uber.org/zap"
)
type Client struct {
Hub *Hub // 负责处理客户端注册、注销、在线管理
Conn *websocket.Conn // 一个ws连接
Send chan []byte // 一个ws连接存储自己的消息管道
PingPeriod time.Duration
ReadDeadline time.Duration
WriteDeadline time.Duration
HeartbeatFailTimes int
ClientLastPongTime time.Time // 客户端最近一次响应服务端 ping 消息的时间
State uint8 // ws状态1=ok0=出错、掉线等
sync.RWMutex
on_open_success.ClientMoreParams // 这里追加一个结构体,方便开发者在成功上线后,可以自定义追加更多字段信息
}
// 处理握手+协议升级
func (c *Client) OnOpen(context *gin.Context) (*Client, bool) {
// 1.升级连接,从http--->websocket
defer func() {
err := recover()
if err != nil {
if val, ok := err.(error); ok {
variable.ZapLog.Error(my_errors.ErrorsWebsocketOnOpenFail, zap.Error(val))
}
}
}()
var upGrader = websocket.Upgrader{
ReadBufferSize: variable.ConfigYml.GetInt("Websocket.WriteReadBufferSize"),
WriteBufferSize: variable.ConfigYml.GetInt("Websocket.WriteReadBufferSize"),
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// 2.将http协议升级到websocket协议.初始化一个有效的websocket长连接客户端
if wsConn, err := upGrader.Upgrade(context.Writer, context.Request, nil); err != nil {
variable.ZapLog.Error(my_errors.ErrorsWebsocketUpgradeFail + err.Error())
return nil, false
} else {
if wsHub, ok := variable.WebsocketHub.(*Hub); ok {
c.Hub = wsHub
}
c.Conn = wsConn
c.Send = make(chan []byte, variable.ConfigYml.GetInt("Websocket.WriteReadBufferSize"))
c.PingPeriod = time.Second * variable.ConfigYml.GetDuration("Websocket.PingPeriod")
c.ReadDeadline = time.Second * variable.ConfigYml.GetDuration("Websocket.ReadDeadline")
c.WriteDeadline = time.Second * variable.ConfigYml.GetDuration("Websocket.WriteDeadline")
if err := c.SendMessage(websocket.TextMessage, variable.WebsocketHandshakeSuccess); err != nil {
variable.ZapLog.Error(my_errors.ErrorsWebsocketWriteMgsFail, zap.Error(err))
}
c.Conn.SetReadLimit(variable.ConfigYml.GetInt64("Websocket.MaxMessageSize")) // 设置最大读取长度
c.Hub.Register <- c
c.State = 1
c.ClientLastPongTime = time.Now()
return c, true
}
}
// 主要功能主要是实时接收消息
func (c *Client) ReadPump(callbackOnMessage func(messageType int, receivedData []byte), callbackOnError func(err error), callbackOnClose func()) {
// 回调 onclose 事件
defer func() {
err := recover()
if err != nil {
if realErr, isOk := err.(error); isOk {
variable.ZapLog.Error(my_errors.ErrorsWebsocketReadMessageFail, zap.Error(realErr))
}
}
callbackOnClose()
}()
// OnMessage事件
for {
if c.State == 1 {
mt, bReceivedData, err := c.Conn.ReadMessage()
if err == nil {
callbackOnMessage(mt, bReceivedData)
} else {
// OnError事件读消息出错)
callbackOnError(err)
break
}
} else {
// OnError事件(状态不可用,一般是程序事先检测到双方无法进行通信,进行的回调)
callbackOnError(errors.New(my_errors.ErrorsWebsocketStateInvalid))
break
}
}
}
// 发送消息,请统一调用本函数进行发送
// 消息发送时增加互斥锁,加强并发情况下程序稳定性
// 提醒:开发者发送消息时,不要调用 c.Conn.WriteMessage(messageType, []byte(message)) 直接发送消息
func (c *Client) SendMessage(messageType int, message string) error {
c.Lock()
defer func() {
c.Unlock()
}()
// 发送消息时,必须设置本次消息的最大允许时长(秒)
if err := c.Conn.SetWriteDeadline(time.Now().Add(c.WriteDeadline)); err != nil {
variable.ZapLog.Error(my_errors.ErrorsWebsocketSetWriteDeadlineFail, zap.Error(err))
return err
}
if err := c.Conn.WriteMessage(messageType, []byte(message)); err != nil {
return err
} else {
return nil
}
}
// 按照websocket标准协议实现隐式心跳,Server端向Client远端发送ping格式数据包,浏览器收到ping标准格式自动将消息原路返回给服务器
func (c *Client) Heartbeat() {
// 1. 设置一个时钟周期性的向client远端发送心跳数据包
ticker := time.NewTicker(c.PingPeriod)
defer func() {
err := recover()
if err != nil {
if val, ok := err.(error); ok {
variable.ZapLog.Error(my_errors.ErrorsWebsocketBeatHeartFail, zap.Error(val))
}
}
ticker.Stop() // 停止该client的心跳检测
}()
//2.浏览器收到服务器的ping格式消息会自动响应pong消息将服务器消息原路返回过来
if c.ReadDeadline == 0 {
_ = c.Conn.SetReadDeadline(time.Time{})
} else {
_ = c.Conn.SetReadDeadline(time.Now().Add(c.ReadDeadline))
}
c.Conn.SetPongHandler(func(receivedPong string) error {
if c.ReadDeadline > time.Nanosecond {
_ = c.Conn.SetReadDeadline(time.Now().Add(c.ReadDeadline))
} else {
_ = c.Conn.SetReadDeadline(time.Time{})
}
// 客户端响应了服务端的ping消息以后更新最近一次响应的时间
c.ClientLastPongTime = time.Now()
//fmt.Println("浏览器收到ping标准格式自动将消息原路返回给服务器", receivedPong) // 接受到的消息叫做pong实际上就是服务器发送出去的ping数据包
return nil
})
//3.自动心跳数据
for {
select {
case <-ticker.C:
if c.State == 1 {
// 这里优先检查客户端最后一次响应ping消息的时间是否超过了服务端允许的最大时间
// 这种检测针对断电、暴力测试中的拔网线很有用,因为直接断电、拔掉网线,客户端所有的回调函数(close、error等)相关的窗台数据无法传递出去服务端的socket文件状态无法更新
// 服务端无法在第一时间感知到客户端掉线
serverAllowMaxOfflineSeconds := float64(variable.ConfigYml.GetInt("Websocket.HeartbeatFailMaxTimes")) * (float64(variable.ConfigYml.GetDuration("Websocket.PingPeriod")))
if time.Now().Sub(c.ClientLastPongTime).Seconds() > serverAllowMaxOfflineSeconds {
c.State = 0
c.Hub.UnRegister <- c // 掉线的客户端统一注销
variable.ZapLog.Warn(my_errors.ErrorsWebsocketClientOfflineTimeout, zap.Float64("timeout(seconds): ", serverAllowMaxOfflineSeconds))
return
}
// 下面是正常的检测逻辑,只要正常关闭浏览器、通过操作按钮等退出客户端,以下代码就是有效的
if err := c.SendMessage(websocket.PingMessage, variable.WebsocketServerPingMsg); err != nil {
c.HeartbeatFailTimes++
if c.HeartbeatFailTimes > variable.ConfigYml.GetInt("Websocket.HeartbeatFailMaxTimes") {
c.State = 0
c.Hub.UnRegister <- c // 掉线的客户端统一注销
variable.ZapLog.Error(my_errors.ErrorsWebsocketBeatHeartsMoreThanMaxTimes, zap.Error(err))
return
}
} else {
if c.HeartbeatFailTimes > 0 {
c.HeartbeatFailTimes--
}
}
} else {
return
}
}
}
}

@ -0,0 +1,32 @@
package core
type Hub struct {
//上线注册
Register chan *Client
//下线注销
UnRegister chan *Client
//所有在线客户端的内存地址
Clients map[*Client]bool
}
func CreateHubFactory() *Hub {
return &Hub{
Register: make(chan *Client),
UnRegister: make(chan *Client),
Clients: make(map[*Client]bool),
}
}
func (h *Hub) Run() {
for {
select {
case client := <-h.Register:
h.Clients[client] = true
case client := <-h.UnRegister:
if _, ok := h.Clients[client]; ok {
_ = client.Conn.Close()
delete(h.Clients, client)
}
}
}
}

@ -0,0 +1,214 @@
package yml_config
import (
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
"go.uber.org/zap"
"goskeleton/app/core/container"
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"goskeleton/app/utils/yml_config/ymlconfig_interf"
"log"
"sync"
"time"
)
// 由于 vipver 包本身对于文件的变化事件有一个bug相关事件会被回调两次
// 常年未彻底解决,相关的 issue 清单https://github.com/spf13/viper/issues?q=OnConfigChange
// 设置一个内部全局变量记录配置文件变化时的时间点如果两次回调事件事件差小于1秒我们认为是第二次回调事件而不是人工修改配置文件
// 这样就避免了 viper 包的这个bug
var lastChangeTime time.Time
var containerFactory = container.CreateContainersFactory()
func init() {
lastChangeTime = time.Now()
}
// CreateYamlFactory 创建一个yaml配置文件工厂
// 参数设置为可变参数的文件名,这样参数就可以不需要传递,如果传递了多个,我们只取第一个参数作为配置文件名
func CreateYamlFactory(fileName ...string) ymlconfig_interf.YmlConfigInterf {
yamlConfig := viper.New()
// 配置文件所在目录
yamlConfig.AddConfigPath(variable.BasePath + "/config")
// 需要读取的文件名,默认为config
if len(fileName) == 0 {
yamlConfig.SetConfigName("config")
} else {
yamlConfig.SetConfigName(fileName[0])
}
//设置配置文件类型(后缀)为 yml
yamlConfig.SetConfigType("yml")
if err := yamlConfig.ReadInConfig(); err != nil {
log.Fatal(my_errors.ErrorsConfigInitFail + err.Error())
}
return &ymlConfig{
viper: yamlConfig,
mu: new(sync.Mutex),
}
}
type ymlConfig struct {
viper *viper.Viper
mu *sync.Mutex
}
//ConfigFileChangeListen 监听文件变化
func (y *ymlConfig) ConfigFileChangeListen() {
y.viper.OnConfigChange(func(changeEvent fsnotify.Event) {
if time.Now().Sub(lastChangeTime).Seconds() >= 1 {
if changeEvent.Op.String() == "WRITE" {
y.clearCache()
lastChangeTime = time.Now()
}
}
})
y.viper.WatchConfig()
}
// keyIsCache 判断相关键是否已经缓存
func (y *ymlConfig) keyIsCache(keyName string) bool {
if _, exists := containerFactory.KeyIsExists(variable.ConfigKeyPrefix + keyName); exists {
return true
} else {
return false
}
}
// 对键值进行缓存
func (y *ymlConfig) cache(keyName string, value interface{}) bool {
// 避免瞬间缓存键、值时,程序提示键名已经被注册的日志输出
y.mu.Lock()
defer y.mu.Unlock()
if _, exists := containerFactory.KeyIsExists(variable.ConfigKeyPrefix + keyName); exists {
return true
}
return containerFactory.Set(variable.ConfigKeyPrefix+keyName, value)
}
// 通过键获取缓存的值
func (y *ymlConfig) getValueFromCache(keyName string) interface{} {
return containerFactory.Get(variable.ConfigKeyPrefix + keyName)
}
// 清空已经缓存的配置项信息
func (y *ymlConfig) clearCache() {
containerFactory.FuzzyDelete(variable.ConfigKeyPrefix)
}
// Clone 允许 clone 一个相同功能的结构体
func (y *ymlConfig) Clone(fileName string) ymlconfig_interf.YmlConfigInterf {
// 这里存在一个深拷贝,需要注意,避免拷贝的结构体操作对原始结构体造成影响
var ymlC = *y
var ymlConfViper = *(y.viper)
(&ymlC).viper = &ymlConfViper
(&ymlC).viper.SetConfigName(fileName)
if err := (&ymlC).viper.ReadInConfig(); err != nil {
variable.ZapLog.Error(my_errors.ErrorsConfigInitFail, zap.Error(err))
}
return &ymlC
}
// Get 一个原始值
func (y *ymlConfig) Get(keyName string) interface{} {
if y.keyIsCache(keyName) {
return y.getValueFromCache(keyName)
} else {
value := y.viper.Get(keyName)
y.cache(keyName, value)
return value
}
}
// GetString 字符串格式返回值
func (y *ymlConfig) GetString(keyName string) string {
if y.keyIsCache(keyName) {
return y.getValueFromCache(keyName).(string)
} else {
value := y.viper.GetString(keyName)
y.cache(keyName, value)
return value
}
}
// GetBool 布尔格式返回值
func (y *ymlConfig) GetBool(keyName string) bool {
if y.keyIsCache(keyName) {
return y.getValueFromCache(keyName).(bool)
} else {
value := y.viper.GetBool(keyName)
y.cache(keyName, value)
return value
}
}
// GetInt 整数格式返回值
func (y *ymlConfig) GetInt(keyName string) int {
if y.keyIsCache(keyName) {
return y.getValueFromCache(keyName).(int)
} else {
value := y.viper.GetInt(keyName)
y.cache(keyName, value)
return value
}
}
// GetInt32 整数格式返回值
func (y *ymlConfig) GetInt32(keyName string) int32 {
if y.keyIsCache(keyName) {
return y.getValueFromCache(keyName).(int32)
} else {
value := y.viper.GetInt32(keyName)
y.cache(keyName, value)
return value
}
}
// GetInt64 整数格式返回值
func (y *ymlConfig) GetInt64(keyName string) int64 {
if y.keyIsCache(keyName) {
return y.getValueFromCache(keyName).(int64)
} else {
value := y.viper.GetInt64(keyName)
y.cache(keyName, value)
return value
}
}
// GetFloat64 小数格式返回值
func (y *ymlConfig) GetFloat64(keyName string) float64 {
if y.keyIsCache(keyName) {
return y.getValueFromCache(keyName).(float64)
} else {
value := y.viper.GetFloat64(keyName)
y.cache(keyName, value)
return value
}
}
// GetDuration 时间单位格式返回值
func (y *ymlConfig) GetDuration(keyName string) time.Duration {
if y.keyIsCache(keyName) {
return y.getValueFromCache(keyName).(time.Duration)
} else {
value := y.viper.GetDuration(keyName)
y.cache(keyName, value)
return value
}
}
// GetStringSlice 字符串切片数格式返回值
func (y *ymlConfig) GetStringSlice(keyName string) []string {
if y.keyIsCache(keyName) {
return y.getValueFromCache(keyName).([]string)
} else {
value := y.viper.GetStringSlice(keyName)
y.cache(keyName, value)
return value
}
}

@ -0,0 +1,19 @@
package ymlconfig_interf
import (
"time"
)
type YmlConfigInterf interface {
ConfigFileChangeListen()
Clone(fileName string) YmlConfigInterf
Get(keyName string) interface{}
GetString(keyName string) string
GetBool(keyName string) bool
GetInt(keyName string) int
GetInt32(keyName string) int32
GetInt64(keyName string) int64
GetFloat64(keyName string) float64
GetDuration(keyName string) time.Duration
GetStringSlice(keyName string) []string
}

@ -0,0 +1,73 @@
package zap_factory
import (
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"goskeleton/app/global/variable"
"log"
"time"
)
func CreateZapFactory(entry func(zapcore.Entry) error) *zap.Logger {
// 获取程序所处的模式: 开发调试 、 生产
//variable.ConfigYml := yml_config.CreateYamlFactory()
appDebug := variable.ConfigYml.GetBool("AppDebug")
// 判断程序当前所处的模式调试模式直接返回一个便捷的zap日志管理器地址所有的日志打印到控制台即可
if appDebug == true {
if logger, err := zap.NewDevelopment(zap.Hooks(entry)); err == nil {
return logger
} else {
log.Fatal("创建zap日志包失败详情" + err.Error())
}
}
// 以下才是 非调试(生产)模式所需要的代码
encoderConfig := zap.NewProductionEncoderConfig()
timePrecision := variable.ConfigYml.GetString("Logs.TimePrecision")
var recordTimeFormat string
switch timePrecision {
case "second":
recordTimeFormat = "2006-01-02 15:04:05"
case "millisecond":
recordTimeFormat = "2006-01-02 15:04:05.000"
default:
recordTimeFormat = "2006-01-02 15:04:05"
}
encoderConfig.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
enc.AppendString(t.Format(recordTimeFormat))
}
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
encoderConfig.TimeKey = "created_at" // 生成json格式日志的时间键字段默认为 ts,修改以后方便日志导入到 ELK 服务器
var encoder zapcore.Encoder
switch variable.ConfigYml.GetString("Logs.TextFormat") {
case "console":
encoder = zapcore.NewConsoleEncoder(encoderConfig) // 普通模式
case "json":
encoder = zapcore.NewJSONEncoder(encoderConfig) // json格式
default:
encoder = zapcore.NewConsoleEncoder(encoderConfig) // 普通模式
}
//写入器
fileName := variable.BasePath + variable.ConfigYml.GetString("Logs.GoSkeletonLogName")
lumberJackLogger := &lumberjack.Logger{
Filename: fileName, //日志文件的位置
MaxSize: variable.ConfigYml.GetInt("Logs.MaxSize"), //在进行切割之前日志文件的最大大小以MB为单位
MaxBackups: variable.ConfigYml.GetInt("Logs.MaxBackups"), //保留旧文件的最大个数
MaxAge: variable.ConfigYml.GetInt("Logs.MaxAge"), //保留旧文件的最大天数
Compress: variable.ConfigYml.GetBool("Logs.Compress"), //是否压缩/归档旧文件
}
writer := zapcore.AddSync(lumberJackLogger)
// 开始初始化zap日志核心参数
//参数一:编码器
//参数二:写入器
//参数三参数级别debug级别支持后续调用的所有函数写日志如果是 fatal 高级别,则级别>=fatal 才可以写日志
zapCore := zapcore.NewCore(encoder, writer, zap.InfoLevel)
return zap.New(zapCore, zap.AddCaller(), zap.Hooks(entry), zap.AddStacktrace(zap.WarnLevel))
}

@ -0,0 +1,116 @@
package bootstrap
import (
_ "goskeleton/app/core/destroy" // 监听程序退出信号,用于资源的释放
"goskeleton/app/global/my_errors"
"goskeleton/app/global/variable"
"goskeleton/app/http/validator/common/register_validator"
"goskeleton/app/service/sys_log_hook"
"goskeleton/app/utils/casbin_v2"
"goskeleton/app/utils/gorm_v2"
"goskeleton/app/utils/snow_flake"
"goskeleton/app/utils/validator_translation"
"goskeleton/app/utils/websocket/core"
"goskeleton/app/utils/yml_config"
"goskeleton/app/utils/zap_factory"
"log"
"os"
)
// 检查项目必须的非编译目录是否存在,避免编译后调用的时候缺失相关目录
func checkRequiredFolders() {
//1.检查配置文件是否存在
if _, err := os.Stat(variable.BasePath + "/config/config.yml"); err != nil {
log.Fatal(my_errors.ErrorsConfigYamlNotExists + err.Error())
}
if _, err := os.Stat(variable.BasePath + "/config/gorm_v2.yml"); err != nil {
log.Fatal(my_errors.ErrorsConfigGormNotExists + err.Error())
}
//2.检查public目录是否存在
if _, err := os.Stat(variable.BasePath + "/public/"); err != nil {
log.Fatal(my_errors.ErrorsPublicNotExists + err.Error())
}
//3.检查storage/logs 目录是否存在
if _, err := os.Stat(variable.BasePath + "/storage/logs/"); err != nil {
log.Fatal(my_errors.ErrorsStorageLogsNotExists + err.Error())
}
// 4.自动创建软连接、更好的管理静态资源
if _, err := os.Stat(variable.BasePath + "/public/storage"); err == nil {
if err = os.RemoveAll(variable.BasePath + "/public/storage"); err != nil {
log.Fatal(my_errors.ErrorsSoftLinkDeleteFail + err.Error())
}
}
if err := os.Symlink(variable.BasePath+"/storage/app", variable.BasePath+"/public/storage"); err != nil {
log.Fatal(my_errors.ErrorsSoftLinkCreateFail + err.Error())
}
}
func init() {
// 1. 初始化 项目根路径,参见 variable 常量包相关路径app\global\variable\variable.go
//2.检查配置文件以及日志目录等非编译性的必要条件
checkRequiredFolders()
//3.初始化表单参数验证器注册在容器Web、Api共用容器
register_validator.WebRegisterValidator()
register_validator.ApiRegisterValidator()
// 4.启动针对配置文件(confgi.yml、gorm_v2.yml)变化的监听, 配置文件操作指针,初始化为全局变量
variable.ConfigYml = yml_config.CreateYamlFactory()
variable.ConfigYml.ConfigFileChangeListen()
// config>gorm_v2.yml 启动文件变化监听事件
variable.ConfigGormv2Yml = variable.ConfigYml.Clone("gorm_v2")
variable.ConfigGormv2Yml.ConfigFileChangeListen()
// 5.初始化全局日志句柄,并载入日志钩子处理函数
variable.ZapLog = zap_factory.CreateZapFactory(sys_log_hook.ZapLogHandler)
// 6.根据配置初始化 gorm mysql 全局 *gorm.Db
if variable.ConfigGormv2Yml.GetInt("Gormv2.Mysql.IsInitGlobalGormMysql") == 1 {
if dbMysql, err := gorm_v2.GetOneMysqlClient(); err != nil {
log.Fatal(my_errors.ErrorsGormInitFail + err.Error())
} else {
variable.GormDbMysql = dbMysql
}
}
// 根据配置初始化 gorm sqlserver 全局 *gorm.Db
if variable.ConfigGormv2Yml.GetInt("Gormv2.Sqlserver.IsInitGlobalGormSqlserver") == 1 {
if dbSqlserver, err := gorm_v2.GetOneSqlserverClient(); err != nil {
log.Fatal(my_errors.ErrorsGormInitFail + err.Error())
} else {
variable.GormDbSqlserver = dbSqlserver
}
}
// 根据配置初始化 gorm postgresql 全局 *gorm.Db
if variable.ConfigGormv2Yml.GetInt("Gormv2.PostgreSql.IsInitGlobalGormPostgreSql") == 1 {
if dbPostgre, err := gorm_v2.GetOnePostgreSqlClient(); err != nil {
log.Fatal(my_errors.ErrorsGormInitFail + err.Error())
} else {
variable.GormDbPostgreSql = dbPostgre
}
}
// 7.雪花算法全局变量
variable.SnowFlake = snow_flake.CreateSnowflakeFactory()
// 8.websocket Hub中心启动
if variable.ConfigYml.GetInt("Websocket.Start") == 1 {
// websocket 管理中心hub全局初始化一份
variable.WebsocketHub = core.CreateHubFactory()
if Wh, ok := variable.WebsocketHub.(*core.Hub); ok {
go Wh.Run()
}
}
// 9.casbin 依据配置文件设置参数(IsInit=1)初始化
if variable.ConfigYml.GetInt("Casbin.IsInit") == 1 {
var err error
if variable.Enforcer, err = casbin_v2.InitCasbinEnforcer(); err != nil {
log.Fatal(err.Error())
}
}
//10.全局注册 validator 错误翻译器,zh 代表中文en 代表英语
if err := validator_translation.InitTrans("zh"); err != nil {
log.Fatal(my_errors.ErrorsValidatorTransInitFail + err.Error())
}
}

@ -0,0 +1,13 @@
package main
import (
"goskeleton/app/global/variable"
_ "goskeleton/bootstrap"
"goskeleton/routers"
)
// 这里可以存放门户类网站入口
func main() {
router := routers.InitApiRouter()
_ = router.Run(variable.ConfigYml.GetString("HttpServer.Api.Port"))
}

@ -0,0 +1,12 @@
package main
import (
_ "goskeleton/bootstrap"
cmd "goskeleton/command"
)
// 开发非http接口类服务入口
func main() {
// 设置运行模式为 cli(console)
cmd.Execute()
}

@ -0,0 +1,13 @@
package main
import (
"goskeleton/app/global/variable"
_ "goskeleton/bootstrap"
"goskeleton/routers"
)
// 这里可以存放后端路由(例如后台管理系统)
func main() {
router := routers.InitWebRouter()
_ = router.Run(variable.ConfigYml.GetString("HttpServer.Web.Port"))
}

@ -0,0 +1,76 @@
package demo
import (
"github.com/spf13/cobra"
"goskeleton/app/global/variable"
)
// Demo示例文件我们假设一个场景
// 通过一个命令指定 搜索引擎(百度、搜狗、谷歌)、搜索类型(文本、图片)、关键词 执行一系列的命令
var (
// 1.定义一个变量,接收搜索引擎(百度、搜狗、谷歌)
SearchEngines string
// 2.搜索的类型(图片、文字)
SearchType string
// 3.关键词
KeyWords string
)
var logger = variable.ZapLog.Sugar()
// 定义命令
var Demo1 = &cobra.Command{
Use: "sousuo",
Aliases: []string{"sou", "ss", "s"}, // 定义别名
Short: "这是一个Demo以搜索内容进行演示业务逻辑...",
Long: `
1.Ginkeleton
2. go run cmd/cli/main.go sousuo -h //可以查看使用指南
3. go run cmd/cli/main.go sousuo // 快速运行一个Demo
4. go run cmd/cli/main.go sousuo -K -E baidu -T img // 指定参数运行Demo
`,
//Args: cobra.ExactArgs(2), // 限制非flag参数也叫作位置参数的个数必须等于 2 ,否则会报错
// Run命令以及子命令的前置函数
PersistentPreRun: func(cmd *cobra.Command, args []string) {
//如果只想作为子命令的回调,可以通过相关参数做判断,仅在子命令执行
logger.Infof("Run函数子命令的前置方法位置参数%v flag参数%s, %s, %s \n", args[0], SearchEngines, SearchType, KeyWords)
},
// Run命令的前置函数
PreRun: func(cmd *cobra.Command, args []string) {
logger.Infof("Run函数的前置方法位置参数%v flag参数%s, %s, %s \n", args[0], SearchEngines, SearchType, KeyWords)
},
// Run 命令是 核心 命令,其余命令都是为该命令服务,可以删除,由您自由选择
Run: func(cmd *cobra.Command, args []string) {
//args 参数表示非flag也叫作位置参数该参数默认会作为一个数组存储。
//fmt.Println(args)
start(SearchEngines, SearchType, KeyWords)
},
// Run命令的后置函数
PostRun: func(cmd *cobra.Command, args []string) {
logger.Infof("Run函数的后置方法位置参数%v flag参数%s, %s, %s \n", args[0], SearchEngines, SearchType, KeyWords)
},
// Run命令以及子命令的后置函数
PersistentPostRun: func(cmd *cobra.Command, args []string) {
//如果只想作为子命令的回调,可以通过相关参数做判断,仅在子命令执行
logger.Infof("Run函数子命令的后置方法位置参数%v flag参数%s, %s, %s \n", args[0], SearchEngines, SearchType, KeyWords)
},
}
// 注册命令、初始化参数
func init() {
Demo1.AddCommand(subCmd)
Demo1.Flags().StringVarP(&SearchEngines, "Engines", "E", "baidu", "-E 或者 --Engines 选择搜索引擎例如baidu、sogou")
Demo1.Flags().StringVarP(&SearchType, "Type", "T", "img", "-T 或者 --Type 选择搜索的内容类型,例如:图片类")
Demo1.Flags().StringVarP(&KeyWords, "KeyWords", "K", "关键词", "-K 或者 --KeyWords 搜索的关键词")
//Demo1.Flags().BoolP(1,2,3,5) //接收bool类型参数
//Demo1.Flags().Int64P() //接收int型
}
//开始执行
func start(SearchEngines, SearchType, KeyWords string) {
logger.Infof("您输入的搜索引擎:%s 搜索类型:%s, 关键词:%s\n", SearchEngines, SearchType, KeyWords)
}

@ -0,0 +1,23 @@
package demo
import (
"fmt"
"github.com/spf13/cobra"
)
// 定义子命令
var subCmd = &cobra.Command{
Use: "subCmd",
Short: "subCmd 命令简要介绍",
Long: `命令使用详细介绍`,
Args: cobra.ExactArgs(1), // 限制非flag参数的个数 = 1 ,超过1个会报错
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("%s\n", args[0])
},
}
//注册子命令
func init() {
Demo1.AddCommand(subCmd)
// 子命令仍然可以定义 flag 参数,相关语法参见 demo.go 文件
}

@ -0,0 +1,48 @@
package demo_simple
import (
"github.com/spf13/cobra"
"goskeleton/app/global/variable"
"time"
)
var (
LogAction string
Date string
logger = variable.ZapLog.Sugar()
)
// 简单示例
var DemoSimple = &cobra.Command{
Use: "demo_simple",
Aliases: []string{"demo_simple"}, // 定义别名
Short: "这是一个最简单的demo示例",
Long: `
1.Ginkeleton
2. go run cmd/cli/main.go demo_simple -h //可以查看使用指南
3. go run cmd/cli/main.go demo_simple -A create // 通过 Action 动作执行相应的命令
`,
// Run 命令是 核心 命令,其余命令都是为该命令服务,可以删除,由您自由选择
Run: func(cmd *cobra.Command, args []string) {
//args 参数表示非flag也叫作位置参数该参数默认会作为一个数组存储。
//fmt.Println(args)
start(LogAction, Date)
},
}
// 注册命令、初始化参数
func init() {
DemoSimple.Flags().StringVarP(&LogAction, "logAction", "A", "insert", "-A 指定参数动作,例如:-A insert ")
DemoSimple.Flags().StringVarP(&Date, "date", "D", time.Now().Format("2006-01-02"), "-D 指定日期,例如:-D 2021-09-13")
}
// 开始执行业务
func start(actionName, Date string) {
switch actionName {
case "insert":
logger.Info("insert 参数执行对应业务逻辑,Date参数值" + Date)
case "update":
logger.Info("update 参数执行对应业务逻辑,Date参数值" + Date)
}
}

@ -0,0 +1,41 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
"goskeleton/command/demo"
"goskeleton/command/demo_simple"
"os"
)
// cli 命令基于 https://github.com/spf13/cobra 封装
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "Cli",
Short: "A brief description of your application",
Long: `A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the RootCmd.
func Execute() {
if err := RootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func init() {
// 如果子命令是存在于子目录,那么就需要在入口统一添加;
// 如果和 root.go 同目录,则不需要像下一行一样添加
RootCmd.AddCommand(demo.Demo1)
RootCmd.AddCommand(demo_simple.DemoSimple)
}

@ -0,0 +1,144 @@
AppDebug: true # 设置程序所处的模式debug=true 调试模式,日志优先显示在控制台, debug=false 非调试模式,将写入日志文件
HttpServer:
Api:
Port: ":20191" #门户网站类端口,注意前面有冒号
Web:
Port: ":20201" #后端应用类端口,注意前面有冒号
AllowCrossDomain: true #是否允许跨域,默认 允许更多关于跨域的介绍从参考https://www.yuque.com/xiaofensinixidaouxiang/bkfhct/kxddzd
TrustProxies: #设置gin可以信任的代理服务器(例如 nginx 前置代理服务器),详情参见文档https://www.yuque.com/xiaofensinixidaouxiang/bkfhct/vmobe7
IsOpen: 0 # 可选值0 或者 1,如果 go 服务是被 nginx 代理,建议该值设置为 1将 nginx 代理及机器的ip添加到 ProxyServerList 列表
ProxyServerList:
- "192.168.10.1" # nginx 代理服务器ip地址
- "192.168.10.2"
Token:
JwtTokenSignKey: "goskeleton" #设置token生成时加密的签名
JwtTokenOnlineUsers: 10 #一个账号密码允许最大获取几个有效的token当超过这个值第一次获取的token的账号、密码就会失效
JwtTokenCreatedExpireAt: 28800 #创建时token默认有效秒数token生成时间加上该时间秒数算做有效期,3600*8=28800 等于8小时
JwtTokenRefreshAllowSec: 86400 #对于过期的token允许在多少小时之内刷新超过此时间则不允许刷新换取新token86400=3600*24即token过期24小时之内允许换新token
JwtTokenRefreshExpireAt: 36000 #对于过期的token支持从相关接口刷新获取新的token它有效期为10个小时3600*10=36000 等于10小时
BindContextKeyName: "userToken" #用户在 header 头部提交的token绑定到上下文时的键名方便直接从上下文(gin.context)直接获取每个用户的id等信息
IsCacheToRedis: 0 #用户token是否缓存到redis, 如果已经正确配置了redis,建议设置为1, 开启redis缓存token1=用户token缓存到redis; 0=token只存在于mysql
Redis:
Host: "127.0.0.1"
Port: 6379
Auth: ""
MaxIdle: 10
MaxActive: 1000
IdleTimeout: 60
IndexDb: 1 # 注意 redis 默认连接的是 1 号数据库,不是 0号数据库
ConnFailRetryTimes: 3 #从连接池获取连接失败,最大重试次数
ReConnectInterval: 1 # 从连接池获取连接失败,每次重试之间间隔的秒数
Logs:
GinLogName: "/storage/logs/gin.log" #设置 gin 框架的接口访问日志
GoSkeletonLogName: "/storage/logs/goskeleton.log" #设置GoSkeleton项目骨架运行时日志文件名注意该名称不要与上一条重复 ,避免和 gin 框架的日志掺杂一起,造成混乱。
TextFormat: "json" #记录日志的格式参数选项console、json console 表示一般的文本格式
TimePrecision: "millisecond" #记录日志时相关的时间精度该参数选项second 、 millisecond 分别表示 秒 和 毫秒 ,默认为毫秒级别
MaxSize: 10 #每个日志的最大尺寸(以MB为单位 超过该值,系统将会自动进行切割
MaxBackups: 7 #保留旧日志最大个数
MaxAge: 15 #保留旧日志最大天数
Compress: false #日志备份时,是否进行压缩
Websocket: #该服务与Http具有相同的ip、端口因此不需要额外设置端口
Start: 0 #默认不启动该服务1=启动0=不启动)
WriteReadBufferSize: 20480 # 读写缓冲区分配字节,大概能存储 6800 多一点的文字
MaxMessageSize: 65535 # 从消息管道读取消息的最大字节
PingPeriod: 20 #心跳包频率,单位:秒
HeartbeatFailMaxTimes: 4 # 允许心跳失败的最大次数默认设置为PingPeriod=30秒检测一次连续4次没有心跳就会清除后端在线信息
ReadDeadline: 100 # 客户端在线情况下,正常的业务消息间隔秒数必须小于该值,否则服务器将会主动断开,该值不能小于心跳频率*允许失败次数,单位:秒。 0 表示不设限制,即服务器不主动断开不发送任何消息的在线客户端,但会消耗服务器资源
WriteDeadline: 35 # 消息单次写入超时时间,单位:秒
SnowFlake:
SnowFlakeMachineId: 2 #如果本项目同时部署在多台机器并且需要同时使用该算法请为每一台机器设置不同的ID区间范围: [0,1023]
FileUploadSetting:
Size: 32 #设置上传文件的最大值单位M注意 如果go前置nginx服务器nginx 默认限制文件上传大小为 50 M ,用户上传文件限制还需要继续修改 nginx 配置
UploadFileField: "file" #post上传文件时表单的键名
UploadFileSavePath: "/storage/app/uploaded/" #上传文件保存在路径, 该路径与 BasePath 进行拼接使用
UploadFileReturnPath: "/public/storage/uploaded/" # 文件上后返回的路径由于程序会自动创建软连接自动将资源定位到实际路径所有资源的访问入口建议都从public开始
AllowMimeType: #允许的文件mime类型列表
- "image/jpeg" #jpg、jpeg图片格式
- "image/png" #png图片格式
- "image/x-icon" #ico图片
- "image/bmp" #bmp图片
- "application/zip" #xlsx、docx、zip
- "application/x-gzip" #tar.gz
- "text/plain; charset=utf-8" #txt log json等文本文件
- "video/mp4" #视频文件例如mp4
- "audio/mpeg" #音频文件,例如: mp3
# casbin 权限控制api接口
Casbin:
# Casbin打开以后注意事项Mysql/MariDb 低版本数据库如果数据库表的引擎默认是 Myisam
# 程序会报错Specified key was too long; max key length is 1000 bytes
# 请手动复制 database/db_demo_mysql.sql 中创建 tb_auth_casbin_rule 的代码自行创建InnoDb引擎的表重新启动本项目即可
IsInit: 0 # 是否随项目启动同步初始化1=是0=否, 开启 Casbin 前请确保数据库连接配置正确
AutoLoadPolicySeconds: 5 # 扫描数据库策略的频率(单位:秒)
TablePrefix: "tb" # mysql、sqlserver 前缀为 tb postgres 数据库前缀请设置为 web.tb其中 web 是本项目创建的的模式
TableName: "auth_casbin_rule" # 程序最终创建的表为: tb_auth_casbin_rule (即在前缀和名称之间自动添加了下划线 _ )
ModelConfig: | # 竖线 | 表示以下整段文本保持换行格式
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _ , _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = (g(r.sub, p.sub) || p.sub == "*" ) && keyMatch(r.obj , p.obj) && (r.act == p.act || p.act == "*")
RabbitMq:
HelloWorld:
#消息服务器地址、账号、密码, / (斜杠)表示默认的虚拟主机,如果是开发者自己创建的,直接追加在 / (斜杠)后面即可例如amqp://账号:密码@ip地址:5672/ginskeleton
Addr: "amqp://账号:密码@ip地址:5672/"
QueueName: "helloword_queue"
Durable: false #消息是否持久化
ConsumerChanNumber: 2 #消费者通道数量(允许一个消费者使用几个连接通道消费、处理消息)
OffLineReconnectIntervalSec: 5 #消费者掉线后,重连间隔的秒数
RetryCount: 5 #消费者掉线后,尝试重连最大次数
WorkQueue:
Addr: "amqp://账号:密码@ip地址:5672/" #参照前文地址说明
QueueName: "work_queue"
Durable: false #消息是否持久化
ConsumerChanNumber: 2 #消费者通道数量(允许一个消费者使用几个连接通道消费、处理消息)
OffLineReconnectIntervalSec: 5 #消费者掉线后,重连间隔的秒数
RetryCount: 5 #消费者掉线后,尝试重连最大次数
PublishSubscribe:
Addr: "amqp://账号:密码@ip地址:5672/" #消息服务器地址、账号、密码
ExchangeType: "fanout"
ExchangeName: "fanout_exchange" #即时消息队列名称,
DelayedExchangeName: "delayed_fanout_exchange" #延迟消息队列名称必须事先在rabbitmq 服务器管理端手动创建
Durable: false #消息是否持久化,如果初始化的是延迟消息队列,那么该参数会被程序强制设置为 true
QueueName: "" #队列名称,为空 表示所有订阅的消费者consumer都可以接受到同样的消息如果设置名称会导致只有最后一个启动的消费者能接受到消息。
ConsumerChanNumber: 1 #消费者通道数量(发布、订阅模式消费者使用一个通道,避免多个通道重复收到数据)
OffLineReconnectIntervalSec: 10 #消费者掉线后,重连间隔的秒数
RetryCount: 5 #消费者掉线后,尝试重连最大次数
Routing:
Addr: "amqp://账号:密码@ip地址:5672/" #参照前文地址说明
ExchangeType: "direct"
ExchangeName: "direct_exchange"
DelayedExchangeName: "delayed_direct_exchange" #延迟消息队列名称必须事先在rabbitmq 服务器管理端手动创建
Durable: false #消息是否持久化,如果初始化的是延迟消息队列,那么该参数会被程序强制设置为 true
QueueName: ""
OffLineReconnectIntervalSec: 10 #消费者掉线后,重连间隔的秒数
RetryCount: 5 #消费者掉线后,尝试重连最大次数
Topics:
Addr: "amqp://账号:密码@ip地址:5672/" #参照前文地址说明
ExchangeType: "topic"
ExchangeName: "topic_exchange"
DelayedExchangeName: "delayed_topic_exchange" #延迟消息队列名称必须事先在rabbitmq 服务器管理端手动创建
Durable: false #消息是否持久化,如果初始化的是延迟消息队列,那么该参数会被程序强制设置为 true
QueueName: ""
OffLineReconnectIntervalSec: 10 #消费者掉线后,重连间隔的秒数
RetryCount: 5 #消费者掉线后,尝试重连最大次数
#验证码(包括中间件)配置信息
Captcha:
captchaId: "captcha_id" # 验证码id提交时的键名
captchaValue: "captcha_value" #验证码值提交时的键名
length: 4 # 验证码生成时的长度

@ -0,0 +1,87 @@
Gormv2: # 只针对 gorm 操作数据库有效
UseDbType: "mysql" # 备选项 mysql 、sqlserver、 postgresql
SqlDebug: false # 请根据个人习惯设置true 表示执行的sql全部会输出在终端(一般来说开发环境可能会方便调试) false 表示默认不会在终端输出sql(生产环境建议设置为 false),
Mysql:
IsInitGlobalGormMysql: 0 # 随项目启动为gorm db初始化一个全局 variable.GormDbMysql完全等于*gorm.Db,正确配置数据库,该值必须设置为: 1
SlowThreshold: 30 # 慢 SQL 阈值(sql执行时间超过此时间单位就会触发系统日志记录)
Write:
Host: "127.0.0.1"
DataBase: "db_goskeleton"
Port: 3306
Prefix: "tb_" # 目前没有用到该配置项
User: "root"
Pass: "DRsXT5ZJ6Oi55LPQ"
Charset: "utf8"
SetMaxIdleConns: 10
SetMaxOpenConns: 128
SetConnMaxLifetime: 60 # 连接不活动时的最大生存时间(秒)
#ReConnectInterval: 1 # 保留项,重连数据库间隔秒数
#PingFailRetryTimes: 3 # 保留项,最大重连次数
IsOpenReadDb: 0 # 是否开启读写分离配置1=开启、0=关闭IsOpenReadDb=1,Read 部分参数有效否则Read部分参数直接忽略
Read:
Host: "127.0.0.1"
DataBase: "db_goskeleton"
Port: 3308 #注意非3306请自行调整
Prefix: "tb_"
User: "root"
Pass: "yourPassword"
Charset: "utf8"
SetMaxIdleConns: 10
SetMaxOpenConns: 128
SetConnMaxLifetime: 60
# 如果要使用sqlserver数据库请在 app/model 目录,将 users_for_sqlserver.txt 的内容直接覆盖同目录的 users.go 即可
SqlServer:
# 随项目启动为gorm db初始化一个全局 variable.GormDbMysql完全等于*gorm.Db,正确配置数据库,该值必须设置为: 1
# 此外,开启 sqlserver 数据库时,请在 app/model/users_for_sqlserver.txt 文件中,按照说明手动替换一下代码
IsInitGlobalGormSqlserver: 0
SlowThreshold: 30
Write:
Host: "127.0.0.1"
DataBase: "db_goskeleton"
Port: 1433
Prefix: "tb_"
User: "Sa"
Pass: "secret2017"
#ReConnectInterval: 1 # 保留项,重连数据库间隔秒数
#PingFailRetryTimes: 3 # 保留项,最大重连次数
SetMaxIdleConns: 10
SetMaxOpenConns: 128
SetConnMaxLifetime: 60
IsOpenReadDb: 0 # 是否开启读写分离配置1=开启、0=关闭IsOpenReadDb=1,Read 部分参数有效否则Read部分参数直接忽略
Read:
Host: "127.0.0.1"
DataBase: "db_goskeleton"
Port: 1433
Prefix: "tb_"
User: "Sa"
Pass: "secret2017"
SetMaxIdleConns: 10
SetMaxOpenConns: 128
SetConnMaxLifetime: 60
# 如果要使用postgresql数据库请在 app/model 目录,将 users_for_postgres.txt 的内容直接覆盖同目录的 users.go 即可
PostgreSql:
IsInitGlobalGormPostgreSql: 0 # 随项目启动为gorm db初始化一个全局 variable.GormDbMysql完全等于*gorm.Db,正确配置数据库,该值必须设置为: 1
SlowThreshold: 30
Write:
Host: "127.0.0.1"
DataBase: "db_goskeleton"
Port: 5432
Prefix: "tb_"
User: "postgres"
Pass: "Secret2017~"
SetMaxIdleConns: 10
SetMaxOpenConns: 128
SetConnMaxLifetime: 60
#ReConnectInterval: 1 # 保留项,重连数据库间隔秒数
#PingFailRetryTimes: 3 # 保留项,最大重连次数
IsOpenReadDb: 0 # 是否开启读写分离配置1=开启、0=关闭IsOpenReadDb=1,Read 部分参数有效否则Read部分参数直接忽略
Read:
Host: "127.0.0.1"
DataBase: "db_goskeleton"
Port: 5432
Prefix: "tb_"
User: "postgres"
Pass: "secret2017"
SetMaxIdleConns: 10
SetMaxOpenConns: 128
SetConnMaxLifetime: 60

@ -0,0 +1,60 @@
CREATE DATABASE /*!32312 IF NOT EXISTS*/`db_goskeleton` /*!40100 DEFAULT CHARACTER SET utf8 */;
USE `db_goskeleton`;
/*Table structure for table `tb_users` */
DROP TABLE IF EXISTS `tb_users`;
CREATE TABLE `tb_users` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_name` VARCHAR(30) DEFAULT '' COMMENT '账号',
`pass` VARCHAR(128) DEFAULT '' COMMENT '密码',
`real_name` VARCHAR(30) DEFAULT '' COMMENT '姓名',
`phone` CHAR(11) DEFAULT '' COMMENT '手机',
`status` TINYINT(4) DEFAULT 1 COMMENT '状态',
`remark` VARCHAR(255) DEFAULT '' COMMENT '备注',
`last_login_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
`last_login_ip` CHAR(30) DEFAULT '' COMMENT '最近一次登录ip',
`login_times` INT(11) DEFAULT 0 COMMENT '累计登录次数',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
/* oauth 表主要控制一个用户可以同时拥有几个有效的token通俗地说就是允许一个账号同时有几个人登录超过将会导致最前面的人的token失效而退出登录*/
DROP TABLE IF EXISTS `tb_oauth_access_tokens`;
CREATE TABLE `tb_oauth_access_tokens` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`fr_user_id` INT(11) DEFAULT 0 COMMENT '外键:tb_users表id',
`client_id` INT(10) UNSIGNED DEFAULT 1 COMMENT '普通用户的授权默认为1',
`token` VARCHAR(500) DEFAULT NULL,
`action_name` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT '' COMMENT 'login|refresh|reset表示token生成动作',
`scopes` VARCHAR(128) CHARACTER SET utf8 COLLATE utf8_unicode_ci DEFAULT '[*]' COMMENT '暂时预留,未启用',
`revoked` TINYINT(1) DEFAULT 0 COMMENT '是否撤销',
`client_ip` VARCHAR(128) DEFAULT NULL COMMENT 'ipv6最长为128位',
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
`expires_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `oauth_access_tokens_user_id_index` (`fr_user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
/* 创建基于casbin控制接口访问的权限表*/
DROP TABLE IF EXISTS `tb_auth_casbin_rule`;
CREATE TABLE `tb_auth_casbin_rule` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`ptype` varchar(100) DEFAULT 'p',
`v0` varchar(100) DEFAULT '',
`v1` varchar(100) DEFAULT '',
`v2` varchar(100) DEFAULT '*',
`v3` varchar(100) DEFAULT '',
`v4` varchar(100) DEFAULT '',
`v5` varchar(100) DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `unique_index` (`ptype`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8

@ -0,0 +1,297 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 10.17
-- Dumped by pg_dump version 10.17
-- Started on 2021-08-04 12:22:01
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
--
-- TOC entry 5 (class 2615 OID 16570)
-- Name: web; Type: SCHEMA; Schema: -; Owner: postgres
--
CREATE SCHEMA web;
ALTER SCHEMA web OWNER TO postgres;
SET default_tablespace = '';
SET default_with_oids = false;
--
-- TOC entry 200 (class 1259 OID 16609)
-- Name: tb_auth_casbin_rule; Type: TABLE; Schema: web; Owner: postgres
--
CREATE TABLE web.tb_auth_casbin_rule (
id integer NOT NULL,
ptype character varying(100) DEFAULT 'p'::character varying NOT NULL,
p0 character varying(100) DEFAULT ''::character varying NOT NULL,
p1 character varying(100) DEFAULT ''::character varying NOT NULL,
p2 character varying(100) DEFAULT ''::character varying NOT NULL,
p3 character varying(100) DEFAULT ''::character varying NOT NULL,
p4 character varying(100) DEFAULT ''::character varying NOT NULL,
p5 character varying(100) DEFAULT ''::character varying NOT NULL,
created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
v0 character varying(100),
v1 character varying(100),
v2 character varying(100),
v3 character varying(100),
v4 character varying(100),
v5 character varying(100)
);
ALTER TABLE web.tb_auth_casbin_rule OWNER TO postgres;
--
-- TOC entry 199 (class 1259 OID 16607)
-- Name: tb_auth_casbin_rule_id_seq; Type: SEQUENCE; Schema: web; Owner: postgres
--
CREATE SEQUENCE web.tb_auth_casbin_rule_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE web.tb_auth_casbin_rule_id_seq OWNER TO postgres;
--
-- TOC entry 2856 (class 0 OID 0)
-- Dependencies: 199
-- Name: tb_auth_casbin_rule_id_seq; Type: SEQUENCE OWNED BY; Schema: web; Owner: postgres
--
ALTER SEQUENCE web.tb_auth_casbin_rule_id_seq OWNED BY web.tb_auth_casbin_rule.id;
--
-- TOC entry 202 (class 1259 OID 16629)
-- Name: tb_oauth_access_tokens; Type: TABLE; Schema: web; Owner: postgres
--
CREATE TABLE web.tb_oauth_access_tokens (
id integer NOT NULL,
fr_user_id integer DEFAULT 0,
client_id integer DEFAULT 1,
token character varying(500) DEFAULT ''::character varying NOT NULL,
action_name character varying(100) DEFAULT ''::character varying NOT NULL,
scopes character varying(100) DEFAULT '*'::character varying NOT NULL,
revoked smallint DEFAULT 0 NOT NULL,
client_ip character varying(20) DEFAULT ''::character varying,
expires_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE web.tb_oauth_access_tokens OWNER TO postgres;
--
-- TOC entry 201 (class 1259 OID 16627)
-- Name: tb_oauth_access_tokens_id_seq; Type: SEQUENCE; Schema: web; Owner: postgres
--
CREATE SEQUENCE web.tb_oauth_access_tokens_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE web.tb_oauth_access_tokens_id_seq OWNER TO postgres;
--
-- TOC entry 2857 (class 0 OID 0)
-- Dependencies: 201
-- Name: tb_oauth_access_tokens_id_seq; Type: SEQUENCE OWNED BY; Schema: web; Owner: postgres
--
ALTER SEQUENCE web.tb_oauth_access_tokens_id_seq OWNED BY web.tb_oauth_access_tokens.id;
--
-- TOC entry 198 (class 1259 OID 16591)
-- Name: tb_users; Type: TABLE; Schema: web; Owner: postgres
--
CREATE TABLE web.tb_users (
id integer NOT NULL,
user_name character varying(30) DEFAULT ''::character varying NOT NULL,
pass character varying(128) DEFAULT ''::character varying NOT NULL,
real_name character varying(30) DEFAULT ''::character varying,
phone character(11) DEFAULT ''::bpchar,
status smallint DEFAULT 1,
remark character varying(120) DEFAULT ''::character varying,
last_login_time timestamp without time zone,
last_login_ip character varying(20) DEFAULT ''::character varying,
login_times integer DEFAULT 0,
created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE web.tb_users OWNER TO postgres;
--
-- TOC entry 197 (class 1259 OID 16589)
-- Name: tb_users_id_seq; Type: SEQUENCE; Schema: web; Owner: postgres
--
CREATE SEQUENCE web.tb_users_id_seq
AS integer
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER TABLE web.tb_users_id_seq OWNER TO postgres;
--
-- TOC entry 2858 (class 0 OID 0)
-- Dependencies: 197
-- Name: tb_users_id_seq; Type: SEQUENCE OWNED BY; Schema: web; Owner: postgres
--
ALTER SEQUENCE web.tb_users_id_seq OWNED BY web.tb_users.id;
--
-- TOC entry 2696 (class 2604 OID 16612)
-- Name: tb_auth_casbin_rule id; Type: DEFAULT; Schema: web; Owner: postgres
--
ALTER TABLE ONLY web.tb_auth_casbin_rule ALTER COLUMN id SET DEFAULT nextval('web.tb_auth_casbin_rule_id_seq'::regclass);
--
-- TOC entry 2708 (class 2604 OID 16632)
-- Name: tb_oauth_access_tokens id; Type: DEFAULT; Schema: web; Owner: postgres
--
ALTER TABLE ONLY web.tb_oauth_access_tokens ALTER COLUMN id SET DEFAULT nextval('web.tb_oauth_access_tokens_id_seq'::regclass);
--
-- TOC entry 2685 (class 2604 OID 16594)
-- Name: tb_users id; Type: DEFAULT; Schema: web; Owner: postgres
--
ALTER TABLE ONLY web.tb_users ALTER COLUMN id SET DEFAULT nextval('web.tb_users_id_seq'::regclass);
--
-- TOC entry 2848 (class 0 OID 16609)
-- Dependencies: 200
-- Data for Name: tb_auth_casbin_rule; Type: TABLE DATA; Schema: web; Owner: postgres
--
--
-- TOC entry 2850 (class 0 OID 16629)
-- Dependencies: 202
-- Data for Name: tb_oauth_access_tokens; Type: TABLE DATA; Schema: web; Owner: postgres
--
--
-- TOC entry 2846 (class 0 OID 16591)
-- Dependencies: 198
-- Data for Name: tb_users; Type: TABLE DATA; Schema: web; Owner: postgres
--
--
-- TOC entry 2859 (class 0 OID 0)
-- Dependencies: 199
-- Name: tb_auth_casbin_rule_id_seq; Type: SEQUENCE SET; Schema: web; Owner: postgres
--
SELECT pg_catalog.setval('web.tb_auth_casbin_rule_id_seq', 1, false);
--
-- TOC entry 2860 (class 0 OID 0)
-- Dependencies: 201
-- Name: tb_oauth_access_tokens_id_seq; Type: SEQUENCE SET; Schema: web; Owner: postgres
--
SELECT pg_catalog.setval('web.tb_oauth_access_tokens_id_seq', 2, true);
--
-- TOC entry 2861 (class 0 OID 0)
-- Dependencies: 197
-- Name: tb_users_id_seq; Type: SEQUENCE SET; Schema: web; Owner: postgres
--
SELECT pg_catalog.setval('web.tb_users_id_seq', 8, true);
--
-- TOC entry 2721 (class 2606 OID 16626)
-- Name: tb_auth_casbin_rule tb_auth_casbin_rule_pkey; Type: CONSTRAINT; Schema: web; Owner: postgres
--
ALTER TABLE ONLY web.tb_auth_casbin_rule
ADD CONSTRAINT tb_auth_casbin_rule_pkey PRIMARY KEY (id);
--
-- TOC entry 2723 (class 2606 OID 16647)
-- Name: tb_oauth_access_tokens tb_oauth_access_tokens_pkey; Type: CONSTRAINT; Schema: web; Owner: postgres
--
ALTER TABLE ONLY web.tb_oauth_access_tokens
ADD CONSTRAINT tb_oauth_access_tokens_pkey PRIMARY KEY (id);
--
-- TOC entry 2718 (class 2606 OID 16606)
-- Name: tb_users tb_users_pkey; Type: CONSTRAINT; Schema: web; Owner: postgres
--
ALTER TABLE ONLY web.tb_users
ADD CONSTRAINT tb_users_pkey PRIMARY KEY (id);
--
-- TOC entry 2719 (class 1259 OID 16662)
-- Name: idx_web_tb_auth_casbin_rule; Type: INDEX; Schema: web; Owner: postgres
--
CREATE UNIQUE INDEX idx_web_tb_auth_casbin_rule ON web.tb_auth_casbin_rule USING btree (ptype, v0, v1, v2, v3, v4, v5);
-- Completed on 2021-08-04 12:22:02
--
-- PostgreSQL database dump complete
--

@ -0,0 +1,52 @@
-- 创建数据库,例如: db_goskeleton
USE [master]
IF NOT EXISTS(SELECT 1 FROM sysdatabases WHERE NAME=N'db_goskeleton')
BEGIN
CREATE DATABASE db_goskeleton
END
GO
use db_goskeleton ;
-- 创建用户表
CREATE TABLE [dbo].[tb_users](
[id] [int] IDENTITY(1,1) NOT NULL,
[user_name] [nvarchar](50) NOT NULL ,
[pass] [varchar](128) NOT NULL ,
[real_name] [nvarchar](30) DEFAULT (''),
[phone] [char](11) DEFAULT (''),
[status] [tinyint] DEFAULT (1),
[remark] [nvarchar](120) DEFAULT (''),
[last_login_time] [datetime] DEFAULT (getdate()),
[last_login_ip] [varchar](128) DEFAULT (''),
[login_times] [int] DEFAULT ((0)),
[created_at] [datetime] DEFAULT (getdate()),
[updated_at] [datetime] DEFAULT (getdate())
);
-- -- 创建token表
CREATE TABLE [dbo].[tb_oauth_access_tokens](
[id] [int] IDENTITY(1,1) NOT NULL,
[fr_user_id] [int] DEFAULT ((0)),
[client_id] [int] DEFAULT ((0)),
[token] [varchar](500) DEFAULT (''),
[action_name] [varchar](50) DEFAULT ('login') ,
[scopes] [varchar](128) DEFAULT ('*') ,
[revoked] [tinyint] DEFAULT ((0)),
[client_ip] [varchar](128) DEFAULT (''),
[created_at] [datetime] DEFAULT (getdate()) ,
[updated_at] [datetime] DEFAULT (getdate()) ,
[expires_at] [datetime] DEFAULT (getdate()) ,
[remark] [nchar](120) DEFAULT ('')
) ;
-- -- 创建 tb_casbin 接口鉴权表
CREATE TABLE [dbo].[tb_auth_casbin_rule](
[id] [int] IDENTITY(1,1) NOT NULL,
[ptype] [varchar](100) DEFAULT ('p'),
[v0] [varchar](100) DEFAULT (''),
[v1] [varchar](100) DEFAULT (''),
[v2] [varchar](100) DEFAULT (''),
[v3] [varchar](100) DEFAULT (''),
[v4] [varchar](100) DEFAULT (''),
[v5] [varchar](100) DEFAULT (''),
[remark] [nchar](120) DEFAULT ('')
) ;

@ -0,0 +1,102 @@
### 控制器 Aop 面向切面编程,优雅地模拟其他语言的动态代理方案。
> 备注:真正的`Aop` 动态代理,在 `golang` 实现起来非常麻烦尽管github有相关实现的包(https://github.com/bouk/monkey), 此包明确说明仅用于生产环境之外的测试环境,还有一部分使用非常复杂,因此本项目骨架没有引入第三方包。
> 需求场景:
> 1.用户删除数据,需要前置和后置回调函数,但是又不想污染控制器核心代码,此时可以考虑使用Aop思想实现。
> 2.我们以调用控制器函数 `Users/Destroy` 函数为例,进行演示。
#### 前置、后置回调最普通的实现方案
> 此种方案,前置和后置代码比较多的时候,会造成控制器核心代码污染。
```go
func (u *Users) Destroy(context *gin.Context) {
// before 删除之前回调代码... 例如:判断删除数据的用户是否具备相关权限等
userid := context.GetFloat64(consts.ValidatorPrefix + "id")
// 根据 userid 执行删除用户数据(最核心代码)
// after 删除之后回调代码... 例如 将删除的用户数据备份到相关的历史表
}
```
#### 使用 Aop 思想实现前置和后置回调需求
> 1.编写删除数据之前Before的回调函数[示例代码](../app/aop/users/destroy_before.go)
```bash
package Users
import (
"goskeleton/app/global/consts"
"fmt"
"github.com/gin-gonic/gin"
)
// 模拟Aop 实现对某个控制器函数的前置Before回调
type destroy_before struct{}
// 前置函数必须具有返回值,这样才能控制流程是否继续向下执行
func (d *destroy_before) Before(context *gin.Context) bool {
userId := context.GetFloat64(consts.ValidatorPrefix + "id")
fmt.Printf("模拟 Users 删除操作, Before 回调,用户ID%.f\n", userId)
if userId > 10 {
return true
} else {
return false
}
}
```
> 2.编写删除数据之后After的回调,[示例代码](../app/aop/users/destroy_after.go)
```bash
package users
import (
"goskeleton/app/global/consts"
"fmt"
"github.com/gin-gonic/gin"
)
// 模拟Aop 实现对某个控制器函数的后置After回调
type destroy_after struct{}
func (d *destroy_after) After(context *gin.Context) {
// 后置函数可以使用异步执行
go func() {
userId := context.GetFloat64(consts.ValidatorPrefix + "id")
fmt.Printf("模拟 Users 删除操作, After 回调,用户ID%.f\n", userId)
}()
}
```
> 3.由于本项目骨架的控制器调用都是统一由验证器启动,因此在验证器调用控制器函数的地方,使用匿名函数,直接优雅地切入前置、后置回调代码,[示例代码](../app/http/validator/web/users/destroy.go)
```go
//(&Web.Users{}).Destroy(extraAddBindDataContext) // 原始方法进行如下改造
// 使用匿名函数切入前置和后置回调函数
func(before_callback_fn func(context *gin.Context) bool, after_callback_fn func(context *gin.Context)) {
if before_callback_fn(extraAddBindDataContext) {
defer after_callback_fn(extraAddBindDataContext)
(&Web.Users{}).Destroy(extraAddBindDataContext)
} else {
// 这里编写前置函数验证不通过的相关返回提示逻辑...
}
}((&Users.destroy_before{}).Before, (&Users.destroy_after{}).After)
// 接口请求结果展示:
模拟 Users 删除操作, Before 回调,用户ID16
真正的控制器函数被执行userId:16
模拟 Users 删除操作, After 回调,用户ID16
```

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save