Compare commits

..

23 Commits

Author SHA1 Message Date
black1552 60f0d8053d fix(database): 优化SQLite数据库初始化逻辑
- 在创建数据库文件前先检查文件是否存在
- 避免重复创建已存在的数据库文件
- 更新项目配置文件中的文件修改时间戳
2026-02-05 10:30:56 +08:00
black1552 258112d38e feat(valid): 更新验证功能支持多种绑定方式
- 修改 ValidToStruct 函数使用 c.Bind 替代 c.BindJSON
- 新增 ValidToMap 函数支持将参数验证后转换为 map 类型
- 更新 ValidToStructAndMap 函数使用 c.Bind 进行参数绑定
- 添加相应的错误处理和类型转换逻辑
- 更新项目缓存配置文件中的文件扫描路径信息
2026-02-04 16:19:36 +08:00
black1552 8927551779 refactor(valid): 重构验证函数命名和返回值
- 将 ValidAndStruct 函数重命名为 ValidToStruct
- 将 ValidAndMap 函数重命名为 ValidToStructAndMap
- 修改 ValidToStructAndMap 函数返回结构体和映射两个值
- 更新 .idea/GOHCache.xml 中的文件修改时间戳
2026-02-04 15:05:09 +08:00
black1552 0ad7304eca feat(valid): 添加参数验证并返回map功能
- 新增ValidAndMap函数用于验证参数并返回map类型
- 修改ValidAndStruct函数优化对象绑定逻辑
- 添加gconv包导入支持map转换功能
- 更新IDE缓存配置添加新的验证类型映射
2026-02-04 14:59:26 +08:00
black1552 a1a2785c71 refactor(valid): 重构验证模块使用Goframe验证器
- 移除原有的validator和国际化翻译相关代码
- 使用Goframe的g.Validator进行参数验证
- 简化ValidAndStruct函数实现
- 更新依赖包从validator切换到gogf
- 删除不再使用的ValidateError工具函数
- 移除init函数中的翻译器初始化逻辑
2026-02-04 14:18:10 +08:00
black1552 8743f40739 refactor(valid): 使用Gin内置验证器替代手动创建实例
移除ApiResponse结构体并改用Gin绑定的validator实例,确保验证器与翻译器的一致性
2026-02-03 17:21:58 +08:00
black1552 d549cf929b refactor(valid): 重构验证器初始化和错误处理逻辑
- 调整全局变量声明顺序并移除未使用的 uni 变量
- 添加 ApiResponse 结构体定义
- 重命名 GetFirstValidateError 函数为 ValidateError
- 优化 init 函数中的中文本地化初始化流程
- 更新字段名映射函数的注释说明
- 改进错误处理逻辑和代码注释
2026-02-03 17:09:46 +08:00
black1552 6c72a05b64 chore(valid): 初始化验证包
- 添加验证包的基础结构文件
2026-02-03 17:03:40 +08:00
black1552 27e26b1d72 feat(valid): 添加参数验证功能并集成中文翻译
- 引入 go-playground/validator/v10 用于结构体验证
- 集成 locales 和 universal-translator 实现中文错误提示
- 创建全局验证器实例并注册中文翻译
- 实现 GetFirstValidateError 函数提取第一条验证错误
- 注册字段名映射函数使用 json 标签作为错误提示字段名
- 在 go.mod 中添加相关依赖包并更新版本
2026-02-03 16:58:56 +08:00
black1552 edff2f198a fix(middleware): 修复panic恢复处理逻辑
- 统一错误响应结构,使用response.Error进行处理
- 添加字符串类型错误的处理分支
- 为未知错误类型设置默认错误消息
- 确保所有panic情况都会调用End方法结束响应
- 保持原有日志记录功能
2026-02-03 16:40:50 +08:00
black1552 53aa8367dc fix(middleware): 修复panic恢复处理逻辑
- 移除冗余的错误数量检查
- 使用类型断言处理不同类型的panic值
- 改进错误消息处理和日志记录
- 简化了异常恢复流程
2026-02-03 16:36:33 +08:00
black1552 3bfca3805a fix(middleware): 修复panic恢复中的错误处理逻辑
- 当发生panic时检查上下文错误列表
- 使用最后一个错误作为响应内容
- 移除直接使用panic值的逻辑
- 改进错误消息的安全性,避免敏感信息泄露
- 确保在panic情况下正确终止请求处理
2026-02-03 16:25:36 +08:00
black1552 60dcd37901 fix(core): 修复Save方法和panic错误处理
- 将Save方法中的Model操作从Save改为Create
- 更新panic日志格式,改进错误信息显示
- 修改错误响应消息为实际panic错误内容
2026-02-03 15:09:41 +08:00
black1552 b704eee2bc refactor(curd): 修改CURD操作的错误处理方式为panic
- 将Get方法中记录不存在的返回值改为panic("未找到数据")
- 将First方法中记录不存在的返回值改为panic("未找到数据")
- 将Delete方法中主键未配置的返回值改为panic("主键字段未配置")
- 移除Sum方法的error返回值,记录不存在时panic("未找到数据")
- 移除ArrayField方法的error返回值,记录不存在时panic("未找到数据")
- 将FindPri方法中主键未配置和记录不存在的返回值改为panic
- 将Exists方法的错误处理改为panic并移除error返回值
- 将All方法的错误处理改为panic并移除error返回值
- 将Count方法的错误处理改为panic并移除error返回值
- 将Save方法的错误处理改为panic并移除error返回值
- 将Update和UpdatePri方法的错误处理改为panic并移除error返回值
- 将Paginate方法的错误处理改为panic并移除error返回值
2026-02-03 14:58:30 +08:00
black1552 0067b7385e refactor(database): 移除GORM表选项设置
- 移除了 GORM 的 table_options 设置
- 保持数据库连接和迁移逻辑不变
- 简化了数据库配置代码
2026-02-03 14:45:04 +08:00
black1552 bb7b3eacc7 refactor(database): 简化 SQLite 数据库连接配置
- 移除 database.go 中硬编码的数据库连接参数字符串
- 将数据库连接参数统一到 config/fun.go 配置文件中
- 使用 fmt 包重构数据库连接字符串拼接逻辑
- 简化了默认数据库路径配置,移除冗余参数
- 优化了数据库初始化过程中的参数传递方式
2026-02-03 14:37:27 +08:00
black1552 dd8d406c33 fix(database): 解决SQLite数据库初始化问题
- 添加数据库文件创建逻辑确保文件存在
- 添加错误处理记录数据库创建失败日志
- 优化数据库初始化流程提高稳定性
2026-02-03 14:32:50 +08:00
black1552 04736c3ba9 feat(database): 优化SQLite数据库连接配置
- 添加共享缓存模式(cache=shared)以提升并发性能
- 设置读写创建模式(mode=rwc)确保数据库文件可写
- 配置10秒忙等待超时(_busy_timeout=10000)避免锁冲突
- 保持WAL模式和完整同步以确保数据完整性
2026-02-03 14:29:11 +08:00
black1552 931b539b61 feat(database): 集成 SQLite 数据库并优化配置
- 替换 SQLite 驱动为 glebarez/sqlite 并移除旧驱动
- 在数据库连接字符串中添加 WAL 模式和外键支持参数
- 配置 GORM 禁用默认事务和复数表名策略
- 更新 go.mod 和 go.sum 中的相关依赖版本
- 添加 SQLite 相关的间接依赖包
2026-02-03 14:22:49 +08:00
black1552 8dde22e47c config(database): 更改默认数据库配置为SQLite
- 引入gfile包用于文件路径操作
- 将默认数据库类型从mysql更改为sqlite
- 设置默认数据库连接字符串为本地db/database.db文件路径
- 修改初始化逻辑以使用配置的dns值
- 更新mysql和sqlite初始化函数使用统一的dns变量
2026-02-03 13:57:39 +08:00
black1552 a4ca7a754f feat(auth): 添加JWT认证功能
- 引入github.com/golang-jwt/jwt/v5依赖包
- 创建utils/jwt.go文件实现JWT工具函数
- 定义JWTClaims结构体用于存储用户信息
- 实现GenerateToken函数生成JWT令牌
- 实现ParseToken函数解析JWT令牌
- 实现ValidateToken函数验证令牌有效性
- 添加令牌过期和无效的错误处理
2026-02-03 11:57:30 +08:00
black1552 14af10fdce feat(utils): 添加GORM版本的泛型CURD封装
- 实现IDao接口提供GORM数据库操作基础能力
- 添加BuildWhere方法构建灵活的查询条件映射
- 实现分页查询、单条查询、列表查询等基础操作
- 提供按主键删除、按条件删除的数据删除功能
- 添加字段求和、存在性检查、统计数量等辅助方法
- 实现数据更新包括按条件更新和按主键更新
- 集成事务处理和上下文绑定功能
- 包含字段名风格转换支持驼峰和下划线格式
- 提供参数清理和验证的工具函数
- 实现关联查询和排序功能支持
2026-02-03 10:55:11 +08:00
black1552 e6d3272e70 feat(database): 添加数据库连接初始化和ORM支持
- 实现MySQL和SQLite数据库连接初始化功能
- 集成GORM ORM框架支持
- 添加数据库连接状态检测和错误处理
- 实现数据库自动迁移功能
- 添加字段重命名和删除功能
- 更新配置文件以支持数据库类型选择
- 添加TCP服务器示例代码
- 添加WebSocket服务示例代码
- 集成MQTT客户端功能
- 更新项目依赖包列表
2026-02-02 17:48:40 +08:00
20 changed files with 2675 additions and 80 deletions

461
.idea/GOHCache.xml Normal file
View File

@ -0,0 +1,461 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoORMHelperCache">
<option name="schemaMapping">
<map>
<entry key="Api">
<value>
<set>
<option value="file://$PROJECT_DIR$/response/code.go" />
</set>
</value>
</entry>
<entry key="BaseConfig">
<value>
<set>
<option value="file://$PROJECT_DIR$/config/structs.go" />
</set>
</value>
</entry>
<entry key="Client">
<value>
<set>
<option value="file://$PROJECT_DIR$/mqtt/client/mqtt.go" />
</set>
</value>
</entry>
<entry key="Config">
<value>
<set>
<option value="file://$PROJECT_DIR$/ws/websocket.go" />
</set>
</value>
</entry>
<entry key="Connection">
<value>
<set>
<option value="file://$PROJECT_DIR$/ws/websocket.go" />
</set>
</value>
</entry>
<entry key="ConnectionPool">
<value>
<set>
<option value="file://$PROJECT_DIR$/tcp/tcp.go" />
</set>
</value>
</entry>
<entry key="Curd">
<value>
<set>
<option value="file://$PROJECT_DIR$/curd/curd.go" />
</set>
</value>
</entry>
<entry key="DataBaseConfig">
<value>
<set>
<option value="file://$PROJECT_DIR$/config/structs.go" />
</set>
</value>
</entry>
<entry key="JWTClaims">
<value>
<set>
<option value="file://$PROJECT_DIR$/utils/jwt.go" />
</set>
</value>
</entry>
<entry key="JwtConfig">
<value>
<set>
<option value="file://$PROJECT_DIR$/config/structs.go" />
</set>
</value>
</entry>
<entry key="Manager">
<value>
<set>
<option value="file://$PROJECT_DIR$/ws/websocket.go" />
</set>
</value>
</entry>
<entry key="Msg">
<value>
<set>
<option value="file://$PROJECT_DIR$/ws/websocket.go" />
</set>
</value>
</entry>
<entry key="NewsOne">
<value>
<set>
<option value="file://$PROJECT_DIR$/../gin_test/api/new.go" />
<option value="file://$PROJECT_DIR$/../gin_test/req/new.go" />
</set>
</value>
</entry>
<entry key="NewsSave">
<value>
<set>
<option value="file://$PROJECT_DIR$/../gin_test/api/new.go" />
<option value="file://$PROJECT_DIR$/../gin_test/req/new.go" />
</set>
</value>
</entry>
<entry key="Paginate">
<value>
<set>
<option value="file://$PROJECT_DIR$/curd/curd.go" />
</set>
</value>
</entry>
<entry key="ServerConfig">
<value>
<set>
<option value="file://$PROJECT_DIR$/config/structs.go" />
</set>
</value>
</entry>
<entry key="TCPServer">
<value>
<set>
<option value="file://$PROJECT_DIR$/tcp/tcp.go" />
</set>
</value>
</entry>
<entry key="TcpConnection">
<value>
<set>
<option value="file://$PROJECT_DIR$/tcp/tcpConfig.go" />
</set>
</value>
</entry>
<entry key="TcpMessage">
<value>
<set>
<option value="file://$PROJECT_DIR$/tcp/tcpConfig.go" />
</set>
</value>
</entry>
<entry key="TcpPoolConfig">
<value>
<set>
<option value="file://$PROJECT_DIR$/tcp/tcpConfig.go" />
</set>
</value>
</entry>
<entry key="response">
<value>
<set>
<option value="file://$PROJECT_DIR$/response/code.go" />
</set>
</value>
</entry>
</map>
</option>
<option name="scannedPathMapping">
<map>
<entry key="file://$PROJECT_DIR$/config/fun.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770100625919" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/config/index.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770011355476" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/config/structs.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770022381063" />
<option name="schema">
<list>
<option value="BaseConfig" />
<option value="ServerConfig" />
<option value="DataBaseConfig" />
<option value="JwtConfig" />
</list>
</option>
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/curd/curd.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770102564408" />
<option name="schema">
<list>
<option value="Paginate" />
<option value="Curd" />
</list>
</option>
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/database/database.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770258641711" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/database/index.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770025447924" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/database/migrate.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770101088203" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/log/index.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770011347706" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/log/log.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770011076335" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/main.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770025551953" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/middleware/middleware.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770108038945" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/mqtt/client/mqtt.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770025698340" />
<option name="schema">
<list>
<option value="Client" />
</list>
</option>
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/response/code.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770014431840" />
<option name="schema">
<list>
<option value="response" />
<option value="Api" />
</list>
</option>
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/server/server.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770026239951" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/tcp/example.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770025697304" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/tcp/tcp.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770025697304" />
<option name="schema">
<list>
<option value="TCPServer" />
<option value="ConnectionPool" />
</list>
</option>
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/tcp/tcpConfig.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770025697304" />
<option name="schema">
<list>
<option value="TcpPoolConfig" />
<option value="TcpConnection" />
<option value="TcpMessage" />
</list>
</option>
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/utils/file.go">
<value>
<ScannedPath>
<option name="lastModified" value="1769850381300" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/utils/jwt.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770090940817" />
<option name="schema">
<list>
<option value="JWTClaims" />
</list>
</option>
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/utils/ptr.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770016124510" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/valid/valid.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770197737203" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/ws/example.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770025697531" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/ws/websocket.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770018907536" />
<option name="schema">
<list>
<option value="Config" />
<option value="Connection" />
<option value="Manager" />
<option value="Msg" />
</list>
</option>
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/../gin_test/api/home.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770190095707" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/../gin_test/api/new.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770191206242" />
<option name="schema">
<list>
<option value="NewsOne" />
<option value="NewsSave" />
</list>
</option>
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/../gin_test/controller/home/houeRouter.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770191001042" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/../gin_test/controller/home/index.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770255253603" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/../gin_test/controller/home/router.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770190557848" />
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/../gin_test/req/new.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770186504009" />
<option name="schema">
<list>
<option value="NewsOne" />
<option value="NewsSave" />
</list>
</option>
</ScannedPath>
</value>
</entry>
<entry key="file://$PROJECT_DIR$/../gin_test/router/router.go">
<value>
<ScannedPath>
<option name="lastModified" value="1770191024510" />
</ScannedPath>
</value>
</entry>
</map>
</option>
<option name="tableStructMapping">
<map>
<entry key="api" value="Api" />
<entry key="base_config" value="BaseConfig" />
<entry key="client" value="Client" />
<entry key="config" value="Config" />
<entry key="connection" value="Connection" />
<entry key="connection_pool" value="ConnectionPool" />
<entry key="curd" value="Curd" />
<entry key="data_base_config" value="DataBaseConfig" />
<entry key="jwt_claims" value="JWTClaims" />
<entry key="jwt_config" value="JwtConfig" />
<entry key="manager" value="Manager" />
<entry key="msg" value="Msg" />
<entry key="news_one" value="NewsOne" />
<entry key="news_save" value="NewsSave" />
<entry key="paginate" value="Paginate" />
<entry key="response" value="response" />
<entry key="server_config" value="ServerConfig" />
<entry key="tcp_connection" value="TcpConnection" />
<entry key="tcp_message" value="TcpMessage" />
<entry key="tcp_pool_config" value="TcpPoolConfig" />
<entry key="tcp_server" value="TCPServer" />
</map>
</option>
<option name="lastTimeChecked" value="1770185677345" />
</component>
</project>

View File

@ -7,6 +7,7 @@ import (
"git.magicany.cc/black1552/gin-base/log" "git.magicany.cc/black1552/gin-base/log"
"git.magicany.cc/black1552/gin-base/utils" "git.magicany.cc/black1552/gin-base/utils"
"github.com/fsnotify/fsnotify"
"github.com/gogf/gf/v2/container/gvar" "github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/os/gfile" "github.com/gogf/gf/v2/os/gfile"
"github.com/spf13/viper" "github.com/spf13/viper"
@ -35,22 +36,29 @@ func init() {
return return
} }
log.Info("配置文件是否为空", utils.EmptyFile(configPath)) log.Info("配置文件是否为空", utils.EmptyFile(configPath))
setDefault() SetDefault()
err = viper.WriteConfig() err = viper.WriteConfig()
if err != nil { if err != nil {
log.Error("保存配置文件失败: ", err) log.Error("保存配置文件失败: ", err)
return return
} }
} else {
err = viper.ReadInConfig()
if err != nil {
log.Error("读取配置文件失败: ", err)
return
}
} }
viper.OnConfigChange(func(in fsnotify.Event) {
log.Info("配置文件已修改")
})
} }
func setDefault() { func SetDefault() {
viper.Set("SERVER.addr", "127.0.0.1:8080") viper.Set("SERVER.addr", "127.0.0.1:8080")
viper.Set("DATABASE.host", "127.0.0.1") viper.Set("SERVER.mode", "release")
viper.Set("DATABASE.port", 3306) viper.Set("DATABASE.type", "sqlite")
viper.Set("DATABASE.username", "root") viper.Set("DATABASE.dns", gfile.Join(gfile.Pwd(), "db", "database.db"))
viper.Set("DATABASE.password", "")
viper.Set("DATABASE.name", "")
viper.Set("JWT.secret", "SET-YOUR-SECRET") viper.Set("JWT.secret", "SET-YOUR-SECRET")
viper.Set("JWT.expire", 86400) viper.Set("JWT.expire", 86400)
} }
@ -88,18 +96,16 @@ func SetConfigMap(value map[string]any) error {
} }
func GetConfigValue(key string, def ...any) *gvar.Var { func GetConfigValue(key string, def ...any) *gvar.Var {
va := gvar.New(viper.Get(key)) value := gvar.New(viper.Get(key))
if va.IsEmpty() && len(def) > 0 { if value.IsEmpty() && len(def) > 0 {
return gvar.New(def[0]) return gvar.New(def[0])
} }
return va return value
} }
func Unmarshal(s any) (any, error) { func Unmarshal[T any]() (*T, error) {
err := viper.Unmarshal(s) var s T
if err != nil { err := viper.Unmarshal(&s)
return nil, err return &s, err
}
return s, nil
} }
func GetAllConfig() map[string]any { func GetAllConfig() map[string]any {

View File

@ -1,29 +1,21 @@
package config package config
type BaseConfig struct { type BaseConfig struct {
Server ServerConfig `toml:"SERVER"` Server ServerConfig `mapstructure:"SERVER"`
Database DataBaseConfig `toml:"DATABASE"` Database DataBaseConfig `mapstructure:"DATABASE"`
Jwt JwtConfig `toml:"JWT"` Jwt JwtConfig `mapstructure:"JWT"`
Logger Logger `toml:"LOGGER"`
} }
type ServerConfig struct { type ServerConfig struct {
Addr string `toml:"addr"` Addr string `mapstructure:"addr"`
Mode string `mapstructure:"mode"`
} }
type DataBaseConfig struct { type DataBaseConfig struct {
Host string `toml:"host"` Dns string `mapstructure:"dns"`
Port string `toml:"port"` Type string `mapstructure:"type"`
User string `toml:"user"`
Pwd string `toml:"pwd"`
Name string `toml:"name"`
} }
type JwtConfig struct { type JwtConfig struct {
Secret string `toml:"secret"` Secret string `mapstructure:"secret"`
Expire int64 `toml:"expire"` Expire int64 `mapstructure:"expire"`
}
type Logger struct {
Level string `toml:"level"`
Path string `toml:"path"`
} }

614
curd/curd.go Normal file
View File

@ -0,0 +1,614 @@
package utils
import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
// 定义上下文别名,保持原代码风格
type ctx = context.Context
// IDao -------------------------- 核心接口定义 --------------------------
// IDao GORM 版本的Dao接口提供GORM DB实例和表相关信息
type IDao interface {
DB() *gorm.DB // 返回GORM的DB实例
Table() string // 返回表名
PrimaryKey() string // 返回主键字段名id
Ctx(ctx context.Context) *gorm.DB // 绑定上下文的DB实例
Transaction(ctx context.Context, f func(ctx context.Context, tx *gorm.DB) error) error // 事务方法
}
// Paginate -------------------------- 分页结构体定义 --------------------------
// Paginate 分页参数结构体(补全原代码中缺失的定义,保持功能完整)
type Paginate struct {
Page int // 页码从1开始
Limit int // 每页条数
}
// -------------------------- 全局常量 --------------------------
// 分页相关字段,用于清理请求参数
var pageInfo = []string{
"page",
"size",
"num",
"limit",
"pagesize",
"pageSize",
"page_size",
"pageNum",
"pagenum",
"page_num",
}
// Curd -------------------------- 泛型CURD核心结构体 --------------------------
// Curd GORM 版本的泛型CURD封装R为对应的模型结构体
type Curd[R any] struct {
Dao IDao
}
// -------------------------- 工具方法:字段名转换(保持原代码的命名风格转换) --------------------------
// caseConvert 字段名风格转换(下划线 <-> 小驼峰)
func caseConvert(key string, toSnake bool) string {
if toSnake {
// 驼峰转下划线参考GORM的命名策略
return schema.NamingStrategy{}.ColumnName("", key)
}
// 下划线转小驼峰
var result strings.Builder
upperNext := false
for i, c := range key {
if c == '_' && i < len(key)-1 {
upperNext = true
continue
}
if upperNext {
result.WriteRune(rune(strings.ToUpper(string(c))[0]))
upperNext = false
} else {
result.WriteRune(c)
}
}
return result.String()
}
// BuildWhere -------------------------- 原BuildWhere对应实现构建查询条件map --------------------------
func (c Curd[R]) BuildWhere(req any, changeWhere any, subWhere any, removeFields []string, isSnake ...bool) map[string]any {
// 默认使用小写下划线方式
toSnake := true
if len(isSnake) > 0 && !isSnake[0] {
toSnake = false
}
// 1. 转换req为map并清理无效数据
reqMap := convToMap(req)
cleanedReq := make(map[string]any)
for k, v := range reqMap {
// 清理空值
if isEmpty(v) {
continue
}
// 清理分页字段
if strInArray(pageInfo, k) {
continue
}
// 清理指定移除字段
if len(removeFields) > 0 && strInArray(removeFields, k) {
continue
}
// 转换字段名风格并存入
cleanedReq[caseConvert(k, toSnake)] = v
}
// 2. 处理changeWhere修改查询操作符eq -> gt
if changeWhere != nil {
changeMap := convToMap(changeWhere)
for k, v := range changeMap {
// 跳过不存在于cleanedReq的字段
if _, exists := cleanedReq[k]; !exists {
continue
}
// 跳过指定移除的字段
if len(removeFields) > 0 && strInArray(removeFields, k) {
continue
}
vMap := convToMap(v)
value, hasValue := vMap["value"]
op, hasOp := vMap["op"]
if hasValue {
// 存在操作符则重构字段名GORM支持 "字段名 >" 这种格式作为where key
if hasOp && op != "" {
newKey := fmt.Sprintf("%s %s", k, op)
delete(cleanedReq, k)
cleanedReq[newKey] = value
} else {
cleanedReq[k] = value
}
}
}
}
// 3. 字段名风格最终转换(确保一致性)
resultMap := make(map[string]any)
for k, v := range cleanedReq {
// 拆分字段名和操作符
parts := strings.SplitN(k, " ", 2)
fieldName := parts[0]
opStr := ""
if len(parts) == 2 {
opStr = parts[1]
}
// 转换字段名风格
convertedField := caseConvert(fieldName, toSnake)
// 重构带操作符的key
if opStr != "" {
resultMap[fmt.Sprintf("%s %s", convertedField, opStr)] = v
} else {
resultMap[convertedField] = v
}
}
// 4. 合并subWhere附加条件
if subWhere != nil {
subMap := convToMap(subWhere)
for k, v := range subMap {
resultMap[caseConvert(k, toSnake)] = v
}
}
return resultMap
}
// BuildMap -------------------------- 原BuildMap对应实现构建变更条件map --------------------------
func (c Curd[R]) BuildMap(op string, value any, field ...string) map[string]any {
res := map[string]any{
"op": op,
"field": "",
"value": value,
}
if len(field) > 0 {
res["field"] = field[0]
}
return res
}
// ClearField -------------------------- 原ClearField对应实现清理请求参数并返回有效map --------------------------
func (c Curd[R]) ClearField(req any, delField []string, subField ...map[string]any) map[string]any {
reqMap := convToMap(req)
resultMap := make(map[string]any)
// 过滤无效数据和指定删除字段
for k, v := range reqMap {
if isEmpty(v) {
continue
}
if strInArray(pageInfo, k) {
continue
}
if len(delField) > 0 && strInArray(delField, k) {
continue
}
resultMap[k] = v
}
// 合并附加字段
if len(subField) > 0 && subField[0] != nil {
for k, v := range subField[0] {
resultMap[k] = v
}
}
return resultMap
}
// ClearFieldPage -------------------------- 原ClearFieldPage对应实现清理参数+分页查询 --------------------------
func (c Curd[R]) ClearFieldPage(ctx ctx, req any, delField []string, where any, page *Paginate, order any, with bool) (items []*R, total int64, err error) {
// 1. 清理请求参数
filterMap := c.ClearField(req, delField)
// 2. 初始化GORM查询
db := c.Dao.Ctx(ctx)
if with {
db = db.Preload("*") // GORM 关联查询全部对应GF的WithAll()
}
// 3. 构建查询条件
db = db.Model(new(R)).Where(filterMap)
if where != nil {
db = db.Where(where)
}
// 4. 排序
if order != nil {
db = db.Order(order)
}
// 5. 统计总数
if err = db.Count(&total).Error; err != nil {
return nil, 0, err
}
// 6. 分页查询
if page != nil && page.Limit > 0 {
offset := (page.Page - 1) * page.Limit
db = db.Offset(offset).Limit(page.Limit)
}
// 7. 执行查询
err = db.Find(&items).Error
return
}
// ClearFieldList -------------------------- 原ClearFieldList对应实现清理参数+列表查询(不分页) --------------------------
func (c Curd[R]) ClearFieldList(ctx ctx, req any, delField []string, where any, order any, with bool) (items []*R, err error) {
filterMap := c.ClearField(req, delField)
db := c.Dao.Ctx(ctx).Model(new(R))
if with {
db = db.Preload("*")
}
if where != nil {
db = db.Where(where)
}
if order != nil {
db = db.Order(order)
}
err = db.Where(filterMap).Find(&items).Error
return
}
// ClearFieldOne -------------------------- 原ClearFieldOne对应实现清理参数+单条查询 --------------------------
func (c Curd[R]) ClearFieldOne(ctx ctx, req any, delField []string, where any, order any, with bool) (item *R, err error) {
item = new(R)
filterMap := c.ClearField(req, delField)
db := c.Dao.Ctx(ctx).Model(item)
if with {
db = db.Preload("*")
}
if where != nil {
db = db.Where(where)
}
if order != nil {
db = db.Order(order)
}
err = db.Where(filterMap).First(item).Error
// 处理记录不存在的情况GORM会返回ErrRecordNotFound
if errors.Is(err, gorm.ErrRecordNotFound) {
panic("未找到数据")
}
return
}
// Value -------------------------- 原Value对应实现查询单个字段值 --------------------------
func (c Curd[R]) Value(ctx ctx, where any, field any) (interface{}, error) {
var result interface{}
db := c.Dao.Ctx(ctx).Model(new(R)).Where(where)
// 处理字段参数
if field != nil {
fieldStr, ok := field.(string)
if !ok || fieldStr == "" {
fieldStr = "*"
}
db = db.Select(fieldStr)
} else {
db = db.Select("*")
}
// 执行查询(取第一条记录的指定字段)
err := db.First(&result).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
panic("未找到数据")
}
return result, err
}
// DeletePri -------------------------- 原DeletePri对应实现按主键删除 --------------------------
func (c Curd[R]) DeletePri(ctx ctx, primaryKey any) error {
db := c.Dao.Ctx(ctx).Model(new(R))
// 按主键字段构建查询
pk := c.Dao.PrimaryKey()
if pk == "" {
panic("主键字段未配置")
}
return db.Where(fmt.Sprintf("%s = ?", pk), primaryKey).Delete(new(R)).Error
}
// DeleteWhere -------------------------- 原DeleteWhere对应实现按条件删除 --------------------------
func (c Curd[R]) DeleteWhere(ctx ctx, where any) error {
return c.Dao.Ctx(ctx).Model(new(R)).Where(where).Delete(new(R)).Error
}
// Sum -------------------------- 原Sum对应实现字段求和 --------------------------
func (c Curd[R]) Sum(ctx ctx, where any, field string) float64 {
var sum float64
if field == "" {
panic("求和字段不能为空")
}
err := c.Dao.Ctx(ctx).Model(new(R)).Where(where).Select(fmt.Sprintf("SUM(%s) as sum", field)).Scan(&sum).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
panic("未找到数据")
}
return sum
}
// ArrayField -------------------------- 原ArrayField对应实现查询指定字段数组 --------------------------
func (c Curd[R]) ArrayField(ctx ctx, where any, field any) []interface{} {
var result []interface{}
db := c.Dao.Ctx(ctx).Model(new(R)).Where(where)
// 处理字段参数
if field != nil {
fieldStr, ok := field.(string)
if !ok || fieldStr == "" {
fieldStr = "*"
}
db = db.Select(fieldStr)
} else {
db = db.Select("*")
}
// 执行查询
err := db.Find(&result).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
panic("未找到数据")
}
return result
}
// FindPri -------------------------- 原FindPri对应实现按主键查询单条记录 --------------------------
func (c Curd[R]) FindPri(ctx ctx, primaryKey any, with bool) (model *R) {
model = new(R)
db := c.Dao.Ctx(ctx).Model(model)
pk := c.Dao.PrimaryKey()
if pk == "" {
panic("主键字段未配置")
}
if with {
db = db.Preload("*")
}
// 按主键查询
err := db.Where(fmt.Sprintf("%s = ?", pk), primaryKey).First(model).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
panic("未找到数据")
}
return
}
// -------------------------- 原First对应实现按条件查询第一条记录 --------------------------
func (c Curd[R]) First(ctx ctx, where any, order any, with bool) (model *R) {
model = new(R)
db := c.Dao.Ctx(ctx).Model(model)
if with {
db = db.Preload("*")
}
if where != nil {
db = db.Where(where)
}
if order != nil {
db = db.Order(order)
}
err := db.First(model).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
panic("未找到数据")
}
return
}
// -------------------------- 原Exists对应实现判断记录是否存在 --------------------------
func (c Curd[R]) Exists(ctx ctx, where any) (exists bool) {
var count int64
err := c.Dao.Ctx(ctx).Model(new(R)).Where(where).Count(&count).Error
if err != nil {
panic(fmt.Sprintf("Exists查询错误: %v", err))
}
return count > 0
}
// -------------------------- 原All对应实现查询所有符合条件的记录 --------------------------
func (c Curd[R]) All(ctx ctx, where any, order any, with bool) (items []*R) {
db := c.Dao.Ctx(ctx).Model(new(R))
if with {
db = db.Preload("*")
}
if where != nil {
db = db.Where(where)
}
if order != nil {
db = db.Order(order)
}
err := db.Find(&items).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
panic(fmt.Sprintf("All查询错误: %v", err))
}
return
}
// -------------------------- 原Count对应实现统计记录总数 --------------------------
func (c Curd[R]) Count(ctx ctx, where any) (count int64) {
err := c.Dao.Ctx(ctx).Model(new(R)).Where(where).Count(&count).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
panic(fmt.Sprintf("Count查询错误: %v", err))
}
return
}
// -------------------------- 原Save对应实现新增/更新记录对应GORM的Save --------------------------
func (c Curd[R]) Save(ctx ctx, data any) {
err := c.Dao.Ctx(ctx).Model(new(R)).Create(data).Error
if err != nil {
panic(fmt.Sprintf("Save保存错误: %v", err))
}
}
// -------------------------- 原Update对应实现按条件更新记录 --------------------------
func (c Curd[R]) Update(ctx ctx, where any, data any) (count int64) {
result := c.Dao.Ctx(ctx).Model(new(R)).Where(where).Updates(data)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
panic(fmt.Sprintf("Update更新错误: %v", result.Error.Error()))
}
return result.RowsAffected
}
// -------------------------- 原UpdatePri对应实现按主键更新记录 --------------------------
func (c Curd[R]) UpdatePri(ctx ctx, primaryKey any, data any) (count int64) {
db := c.Dao.Ctx(ctx).Model(new(R))
pk := c.Dao.PrimaryKey()
if pk == "" {
panic("主键字段未配置")
}
result := db.Where(fmt.Sprintf("%s = ?", pk), primaryKey).Updates(data)
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
panic(fmt.Sprintf("UpdatePri更新错误: %v", result.Error.Error()))
}
return result.RowsAffected
}
// -------------------------- 原Paginate对应实现分页查询 --------------------------
func (c Curd[R]) Paginate(ctx context.Context, where any, p Paginate, with bool, order any) (items []*R, total int64) {
db := c.Dao.Ctx(ctx).Model(new(R))
// 1. 构建查询条件
if where != nil {
db = db.Where(where)
}
// 2. 统计总数
if err := db.Count(&total).Error; err != nil {
panic(fmt.Sprintf("Paginate查询错误: %v", err))
}
// 3. 关联查询
if with {
db = db.Preload("*")
}
// 4. 排序
if order != nil {
db = db.Order(order)
}
// 5. 分页offset = (页码-1)*每页条数)
if p.Limit > 0 {
offset := (p.Page - 1) * p.Limit
db = db.Offset(offset).Limit(p.Limit)
}
// 6. 执行查询
err := db.Find(&items).Error
if err != nil || errors.Is(err, gorm.ErrRecordNotFound) {
panic(fmt.Sprintf("Paginate查询错误: %v", err))
}
return
}
// -------------------------- 内部辅助工具函数 --------------------------
// convToMap 将任意类型转换为map[string]any简化版适配常见场景
func convToMap(v any) map[string]any {
if v == nil {
return make(map[string]any)
}
val := reflect.ValueOf(v)
// 处理指针类型
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
// 只处理结构体和map类型
if val.Kind() != reflect.Struct && val.Kind() != reflect.Map {
return make(map[string]any)
}
result := make(map[string]any)
if val.Kind() == reflect.Map {
// 处理map类型
for _, key := range val.MapKeys() {
keyStr, ok := key.Interface().(string)
if !ok {
continue
}
result[keyStr] = val.MapIndex(key).Interface()
}
} else {
// 处理结构体类型
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
fieldVal := val.Field(i)
// 获取json标签作为key优先否则用字段名
jsonTag := field.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
jsonTag = field.Name
} else {
// 分割json标签忽略omitempty等选项
jsonTag = strings.Split(jsonTag, ",")[0]
}
result[jsonTag] = fieldVal.Interface()
}
}
return result
}
// isEmpty 判断值是否为空
func isEmpty(v any) bool {
if v == nil {
return true
}
val := reflect.ValueOf(v)
switch val.Kind() {
case reflect.String:
return val.String() == ""
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return val.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return val.Uint() == 0
case reflect.Float32, reflect.Float64:
return val.Float() == 0
case reflect.Bool:
return !val.Bool()
case reflect.Slice, reflect.Array, reflect.Map, reflect.Chan:
return val.Len() == 0
case reflect.Ptr, reflect.Interface:
return val.IsNil()
default:
return false
}
}
// strInArray 判断字符串是否在数组中
func strInArray(arr []string, str string) bool {
for _, v := range arr {
if strings.EqualFold(v, str) { // 忽略大小写比较
return true
}
}
return false
}

View File

@ -1,5 +1,79 @@
package database package database
func init() { import (
"database/sql"
"fmt"
"git.magicany.cc/black1552/gin-base/config"
"git.magicany.cc/black1552/gin-base/log"
"github.com/glebarez/sqlite"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gfile"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
var (
Type gorm.Dialector
Db *gorm.DB
err error
sqlDb *sql.DB
dns = config.GetConfigValue("database.dns", gfile.Join(gfile.Pwd(), "db", "database.db"))
)
func init() {
if g.IsEmpty(dns) {
log.Error("gormDns未配置", "请检查配置文件")
return
}
switch config.GetConfigValue("database.type", "sqlite").String() {
case "mysql":
log.Info("使用mysql数据库")
mysqlInit()
case "sqlite":
log.Info("使用sqlite数据库")
sqliteInit()
}
Db, err = gorm.Open(Type, &gorm.Config{
SkipDefaultTransaction: true,
// 命名策略:保持与模型一致,避免字段/表名转换问题
NamingStrategy: schema.NamingStrategy{
SingularTable: true, // 表名禁用复数形式(例如 User 对应 user 表,而非 users
},
})
if err != nil {
log.Error("数据库连接失败: ", err)
return
}
sqlDb, err = Db.DB()
if err != nil {
log.Error("获取sqlDb失败", err)
return
}
if err = sqlDb.Ping(); err != nil {
log.Error("数据库未正常连接", err)
return
}
}
func mysqlInit() {
Type = mysql.New(mysql.Config{
DSN: dns.String(),
DefaultStringSize: 255, // string 类型字段的默认长度
DisableDatetimePrecision: true, // 禁用 datetime 精度MySQL 5.6 之前的数据库不支持
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
})
}
func sqliteInit() {
if !gfile.Exists(dns.String()) {
_, err = gfile.Create(dns.String())
if err != nil {
log.Error("创建数据库文件失败: ", err)
return
}
}
Type = sqlite.Open(fmt.Sprintf("%s?cache=shared&mode=rwc&_busy_timeout=10000&_fk=1&_journal=WAL&_sync=FULL", dns.String()))
} }

1
database/index.go Normal file
View File

@ -0,0 +1 @@
package database

43
database/migrate.go Normal file
View File

@ -0,0 +1,43 @@
package database
import (
"git.magicany.cc/black1552/gin-base/log"
"github.com/gogf/gf/v2/frame/g"
)
func SetAutoMigrate(models ...interface{}) {
if g.IsNil(Db) {
log.Error("数据库连接失败")
return
}
err := Db.AutoMigrate(models...)
if err != nil {
log.Error("数据库迁移失败", err)
}
}
func RenameColumn(dst interface{}, name, newName string) {
if Db.Migrator().HasColumn(dst, name) {
err := Db.Migrator().RenameColumn(dst, name, newName)
if err != nil {
log.Error("数据库修改字段失败", err)
return
}
} else {
log.Info("数据库字段不存在", name)
}
}
// DropColumn
// 删除字段
// 例DropColumn(&User{}, "Sex")
func DropColumn(dst interface{}, name string) {
if Db.Migrator().HasColumn(dst, name) {
err := Db.Migrator().DropColumn(dst, name)
if err != nil {
log.Error("数据库删除字段失败", err)
return
}
} else {
log.Info("数据库字段不存在", name)
}
}

63
go.mod
View File

@ -3,45 +3,76 @@ module git.magicany.cc/black1552/gin-base
go 1.25 go 1.25
require ( require (
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/fsnotify/fsnotify v1.9.0
github.com/gin-gonic/gin v1.7.7 github.com/gin-gonic/gin v1.7.7
github.com/glebarez/sqlite v1.11.0
github.com/go-playground/locales v0.13.0
github.com/go-playground/universal-translator v0.17.0
github.com/go-playground/validator/v10 v10.4.2
github.com/gogf/gf/v2 v2.10.0 github.com/gogf/gf/v2 v2.10.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/gorilla/websocket v1.5.3
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/natefinch/lumberjack.v2 v2.2.1
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.1
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect filippo.io/edwards25519 v1.1.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/clbanning/mxj/v2 v2.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fatih/color v1.18.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.13.0 // indirect github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/golang/protobuf v1.3.3 // indirect github.com/golang/protobuf v1.3.5 // indirect
github.com/json-iterator/go v1.1.9 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/leodido/go-urn v1.2.0 // indirect github.com/grokify/html-strip-tags-go v0.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/olekukonko/errors v1.1.0 // indirect
github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.1.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect github.com/ugorji/go/codec v1.1.14 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect golang.org/x/crypto v0.42.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.33.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect gopkg.in/yaml.v2 v2.2.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.22.6 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
) )

82
go.sum
View File

@ -1,3 +1,5 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
@ -5,6 +7,10 @@ github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU= github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A= github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
@ -17,6 +23,11 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@ -27,47 +38,61 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.4.2 h1:RqFe5MzGf2UOFhxQYnjHabHOT6CLbYWkeXOfcXB7fsM=
github.com/go-playground/validator/v10 v10.4.2/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gogf/gf/v2 v2.10.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs= github.com/gogf/gf/v2 v2.10.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs=
github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0= github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4= github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc= github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI= github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
@ -78,6 +103,9 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
@ -95,15 +123,22 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go v1.1.14/go.mod h1:8lpbXS0IfYpSwsLmwWThGZ7Qm2dNUIb0/kcsxHbwNpM=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.1.14 h1:3kmVO19VOoEBcborYacgFopagfu1s1+73nR7uRuN0lM=
github.com/ugorji/go/codec v1.1.14/go.mod h1:CburFl4ZXbHpzvp1gzm2I8tpnFk9XPHOJwIheJViqKk=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
@ -112,19 +147,27 @@ go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgf
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
@ -141,5 +184,18 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYs
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/libc v1.22.6 h1:cbXU8R+A6aOjRuhsFh3nbDWXO/Hs4ClJRXYB11KmPDo=
modernc.org/libc v1.22.6/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=

20
main.go
View File

@ -3,25 +3,17 @@ package main
import ( import (
"git.magicany.cc/black1552/gin-base/config" "git.magicany.cc/black1552/gin-base/config"
"git.magicany.cc/black1552/gin-base/log" "git.magicany.cc/black1552/gin-base/log"
"git.magicany.cc/black1552/gin-base/response" "git.magicany.cc/black1552/gin-base/server"
"github.com/gin-gonic/gin"
) )
// TIP <p>To run your response, right-click the response and select <b>Run</b>.</p> <p>Alternatively, click // TIP <p>To run your response, right-click the response and select <b>Run</b>.</p> <p>Alternatively, click
// the <icon src="AllIcons.Actions.Execute"/> icon in the gutter and select the <b>Run</b> menu item from here.</p> // the <icon src="AllIcons.Actions.Execute"/> icon in the gutter and select the <b>Run</b> menu item from here.</p>
func main() { func main() {
err := config.LoadConfigFromFile() g := server.New()
cf, err := config.Unmarshal[config.BaseConfig]()
if err != nil { if err != nil {
log.Info("err: ", err.Error()) log.Error("转换配置失败", err)
} }
err = config.SetConfigValue("SERVICE.user-service", "127.0.0.1:3001") log.Info("启动服务:", cf.Server.Addr)
if err != nil { server.Run(g)
log.Info("err: ", err.Error())
}
err = config.SetConfigMap(map[string]any{
"SERVICE.product-service": "127.0.0.1:3002",
"SERVICE.order-service": "127.0.0.1:3003",
})
response.Success(&gin.Context{}).SetMsg("text").End()
log.Info("ceshi", config.GetAllConfig())
} }

View File

@ -12,8 +12,17 @@ func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
log.Error("发生panic", "path", c.Request.URL.Path, "method", c.Request.Method, err) res := response.Error(c).SetCode(http.StatusInternalServerError)
response.Error(c).SetCode(http.StatusInternalServerError).SetMsg("服务器内部错误").End() switch e := err.(type) {
case string:
res = res.SetMsg(e)
case error:
res = res.SetMsg(e.Error())
default:
res = res.SetMsg("服务器内部异常,请稍后重试")
}
res.End()
log.Error("发生panic=》", "path", c.Request.URL.Path, ",method:", c.Request.Method, ",", err)
c.Abort() c.Abort()
} }
}() }()

220
mqtt/client/mqtt.go Normal file
View File

@ -0,0 +1,220 @@
package client
import (
"context"
"fmt"
"sync"
"time"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/gogf/gf/v2/os/glog"
)
// Client MQTT客户端结构
type Client struct {
client mqtt.Client
opts *mqtt.ClientOptions
ctx context.Context
subscribed map[string]byte
subMutex sync.RWMutex
callback mqtt.MessageHandler
// 错误处理相关
onConnectionLost func(error)
onReconnect func()
onConnect func()
onSubscriptionError func(error)
onPublishError func(error)
}
// NewClientWithAuth 创建带用户名密码认证的MQTT客户端
func NewClientWithAuth(ctx context.Context, broker, clientId, username, password string) *Client {
c := &Client{
ctx: ctx,
subscribed: make(map[string]byte),
}
opts := mqtt.NewClientOptions()
opts.AddBroker(broker)
opts.SetClientID(clientId)
opts.SetUsername(username)
opts.SetPassword(password)
// 设置连接丢失处理函数
opts.SetConnectionLostHandler(func(client mqtt.Client, err error) {
glog.Error(ctx, "MQTT连接断开:", err)
if c.onConnectionLost != nil {
c.onConnectionLost(err)
}
})
// 设置重连处理函数
opts.SetReconnectingHandler(func(client mqtt.Client, opts *mqtt.ClientOptions) {
glog.Info(ctx, "MQTT客户端正在尝试重连...")
if c.onReconnect != nil {
c.onReconnect()
}
})
// 设置连接成功处理函数,重连成功后重新订阅主题
opts.SetOnConnectHandler(func(client mqtt.Client) {
glog.Info(ctx, "MQTT客户端重新连接成功")
if c.onConnect != nil {
c.onConnect()
}
// 重连成功后重新订阅主题
go c.resubscribe()
})
// 设置其他选项...
mqttClient := mqtt.NewClient(opts)
c.client = mqttClient
c.opts = opts
return c
}
// Connect 连接到MQTT服务器
func (c *Client) Connect() error {
glog.Info(c.ctx, "开始连接到MQTT服务器...")
token := c.client.Connect()
glog.Info(c.ctx, "等待连接完成...")
// 使用更长的超时时间,避免网络延迟导致连接失败
if token.WaitTimeout(30 * time.Second) {
glog.Info(c.ctx, "连接操作完成")
if token.Error() != nil {
err := fmt.Errorf("连接到MQTT代理时发生错误: %w", token.Error())
glog.Error(c.ctx, "连接MQTT服务器时发生错误:", token.Error())
return err
}
} else {
// 连接超时
err := fmt.Errorf("连接到MQTT服务器超时")
glog.Error(c.ctx, "连接MQTT服务器超时")
return err
}
glog.Info(c.ctx, "成功连接到MQTT服务器")
return nil
}
// Disconnect 断开MQTT连接
func (c *Client) Disconnect() {
if c.client.IsConnected() {
c.client.Disconnect(250)
glog.Info(c.ctx, "已断开MQTT连接")
}
}
// SubscribeMultiple 同时订阅多个主题
func (c *Client) SubscribeMultiple(topics map[string]byte, callback mqtt.MessageHandler) error {
// 保存订阅信息
c.subMutex.Lock()
for topic, qos := range topics {
c.subscribed[topic] = qos
}
c.callback = callback
c.subMutex.Unlock()
token := c.client.SubscribeMultiple(topics, callback)
// 增加订阅超时时间
if token.WaitTimeout(30*time.Second) && token.Error() != nil {
err := fmt.Errorf("同时订阅多个主题出现错误: %w", token.Error())
glog.Error(c.ctx, "订阅主题时发生错误:", token.Error())
if c.onSubscriptionError != nil {
c.onSubscriptionError(err)
}
return err
}
// 检查订阅是否成功
if token.WaitTimeout(30 * time.Second) {
glog.Info(c.ctx, "成功订阅主题:", topics)
} else {
err := fmt.Errorf("订阅主题超时: %v", topics)
glog.Error(c.ctx, "订阅主题超时:", topics)
if c.onSubscriptionError != nil {
c.onSubscriptionError(err)
}
return err
}
return nil
}
// Publish 发布消息
func (c *Client) Publish(topic string, qos byte, retained bool, payload interface{}) error {
token := c.client.Publish(topic, qos, retained, payload)
// 增加发布超时时间
if token.WaitTimeout(30*time.Second) && token.Error() != nil {
err := fmt.Errorf("发送消息到主题%s出现错误: %w", topic, token.Error())
glog.Error(c.ctx, "发布消息到主题", topic, "时发生错误:", token.Error())
if c.onPublishError != nil {
c.onPublishError(err)
}
return err
}
// 检查发布是否成功
if token.WaitTimeout(30 * time.Second) {
glog.Info(c.ctx, "成功发布消息到主题:", topic)
} else {
err := fmt.Errorf("发布消息到主题%s超时", topic)
glog.Error(c.ctx, "发布消息到主题超时:", topic)
if c.onPublishError != nil {
c.onPublishError(err)
}
return err
}
return nil
}
// resubscribe 重新订阅主题
func (c *Client) resubscribe() {
c.subMutex.RLock()
defer c.subMutex.RUnlock()
if len(c.subscribed) == 0 {
glog.Info(c.ctx, "没有需要重新订阅的主题")
return
}
// 复制订阅信息避免并发问题
topics := make(map[string]byte)
for topic, qos := range c.subscribed {
topics[topic] = qos
}
glog.Info(c.ctx, "开始重新订阅主题:", topics)
// 增加重新订阅的超时时间
token := c.client.SubscribeMultiple(topics, c.callback)
if token.WaitTimeout(30 * time.Second) {
if token.Error() != nil {
err := fmt.Errorf("重新订阅主题时发生错误: %w", token.Error())
glog.Error(c.ctx, "重新订阅主题时发生错误:", token.Error())
if c.onSubscriptionError != nil {
c.onSubscriptionError(err)
}
} else {
glog.Info(c.ctx, "重新订阅主题成功")
}
} else {
err := fmt.Errorf("重新订阅主题超时")
glog.Error(c.ctx, "重新订阅主题超时")
if c.onSubscriptionError != nil {
c.onSubscriptionError(err)
}
}
}
// IsConnected 检查是否连接
func (c *Client) IsConnected() bool {
isConnected := c.client.IsConnected()
glog.Debug(c.ctx, "检查连接状态:", isConnected)
return isConnected
}
// Subscribe 单独订阅一个主题
func (c *Client) Subscribe(topic string, qos byte, callback mqtt.MessageHandler) error {
topics := map[string]byte{topic: qos}
return c.SubscribeMultiple(topics, callback)
}

View File

@ -1,15 +1,71 @@
package server package server
import ( import (
"context"
"errors"
"fmt"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"git.magicany.cc/black1552/gin-base/config" "git.magicany.cc/black1552/gin-base/config"
"git.magicany.cc/black1552/gin-base/log"
"git.magicany.cc/black1552/gin-base/middleware" "git.magicany.cc/black1552/gin-base/middleware"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// New 创建一个gin实例
// 默认使用全局异常处理中间件
// 默认使用跨域中间件
// @Return *gin.Engine 可以使用创建路由
func New() *gin.Engine { func New() *gin.Engine {
gin.SetMode(config.GetConfigValue("server.mode", "debug").String())
g := gin.New() g := gin.New()
g.Use(middleware.ErrorHandler()) g.Use(middleware.ErrorHandler())
g.Use(middleware.CORSMiddleware()) g.Use(middleware.CORSMiddleware())
g.Run(config.GetConfigValue("server.addr", "127.0.0.1:8080").String())
return g return g
} }
// Run 启动服务
// @Param *gin.Engine 路由实例
// 设置监听挂壁
func Run(g *gin.Engine) {
s := &http.Server{
Addr: config.GetConfigValue("server.addr", ":8080").String(),
Handler: g,
ReadTimeout: 60 * time.Second,
ReadHeaderTimeout: 60 * time.Second,
WriteTimeout: 60 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 20 * 1024 * 1024,
}
go func() {
if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Error("服务器启动失败:", err)
}
}()
log.Info("服务器启动成功....")
if strings.Contains(s.Addr, "127.0.0.1") || strings.Contains(s.Addr, "0.0.0.0") || strings.Contains(s.Addr, "locahost") {
log.Info("请使用打开:", fmt.Sprintf("http://%s\n", s.Addr))
} else {
log.Info("请使用打开:", fmt.Sprintf("http://localhost%s\n", s.Addr))
}
// 等待中断信号以优雅地关闭服务器
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Info("正在关闭服务器...")
// 设置5秒的超时时间用于关闭服务器
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := s.Shutdown(ctx); err != nil {
log.Error("服务器强制关闭")
}
log.Info("服务器已退出")
}

47
tcp/example.go Normal file
View File

@ -0,0 +1,47 @@
package tcp
import (
"fmt"
"time"
)
// Example 展示如何使用TCP服务
func Example() {
// 创建配置
config := &TcpPoolConfig{
BufferSize: 2048,
MaxConnections: 100000,
ConnectTimeout: time.Second * 5,
ReadTimeout: time.Second * 30,
WriteTimeout: time.Second * 10,
MaxIdleTime: time.Minute * 5,
}
// 创建TCP服务器
server := NewTCPServer("0.0.0.0:8888", config)
// 设置消息处理函数
server.SetMessageHandler(func(conn *TcpConnection, msg *TcpMessage) error {
fmt.Printf("Received message from %s: %s\n", conn.Id, string(msg.Data))
// 回显消息
return server.SendTo(conn.Id, []byte(fmt.Sprintf("Echo: %s", msg.Data)))
})
// 启动服务器
if err := server.Start(); err != nil {
fmt.Printf("Failed to start server: %v\n", err)
return
}
// 运行10秒后停止
fmt.Println("TCP server started. Running for 10 seconds...")
time.Sleep(time.Second * 10)
// 停止服务器
if err := server.Stop(); err != nil {
fmt.Printf("Failed to stop server: %v\n", err)
}
fmt.Println("TCP server stopped.")
}

280
tcp/tcp.go Normal file
View File

@ -0,0 +1,280 @@
package tcp
import (
"context"
"fmt"
"sync"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/net/gtcp"
"github.com/gogf/gf/v2/os/glog"
"github.com/gogf/gf/v2/os/grpool"
"github.com/gogf/gf/v2/os/gtime"
)
// MessageHandler 消息处理函数类型
type MessageHandler func(conn *TcpConnection, msg *TcpMessage) error
// TCPServer TCP服务器结构
type TCPServer struct {
Address string
Config *TcpPoolConfig
Listener *gtcp.Server
Connection *ConnectionPool
Logger *glog.Logger
MessageHandler MessageHandler
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
}
// ConnectionPool 连接池结构
type ConnectionPool struct {
connections map[string]*TcpConnection
mutex sync.RWMutex
config *TcpPoolConfig
logger *glog.Logger
}
// NewTCPServer 创建一个新的TCP服务器
func NewTCPServer(address string, config *TcpPoolConfig) *TCPServer {
logger := g.Log(address)
ctx, cancel := context.WithCancel(context.Background())
pool := &ConnectionPool{
connections: make(map[string]*TcpConnection),
config: config,
logger: logger,
}
server := &TCPServer{
Address: address,
Config: config,
Connection: pool,
Logger: logger,
ctx: ctx,
cancel: cancel,
}
server.Listener = gtcp.NewServer(address, server.handleConnection)
return server
}
// SetMessageHandler 设置消息处理函数
func (s *TCPServer) SetMessageHandler(handler MessageHandler) {
s.MessageHandler = handler
}
// Start 启动TCP服务器
func (s *TCPServer) Start() error {
s.Logger.Info(s.ctx, fmt.Sprintf("TCP server starting on %s", s.Address))
go func() {
s.wg.Add(1)
defer s.wg.Done()
if err := s.Listener.Run(); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("TCP server stopped with error: %v", err))
}
}()
return nil
}
// Stop 停止TCP服务器
func (s *TCPServer) Stop() error {
s.Logger.Info(s.ctx, "TCP server stopping...")
s.cancel()
s.Listener.Close()
s.wg.Wait()
s.Connection.Clear()
s.Logger.Info(s.ctx, "TCP server stopped")
return nil
}
// handleConnection 处理新连接
func (s *TCPServer) handleConnection(conn *gtcp.Conn) {
// 生成连接ID
connID := fmt.Sprintf("%s_%d", conn.RemoteAddr().String(), gtime.TimestampNano())
// 创建连接对象
tcpConn := &TcpConnection{
Id: connID,
Address: conn.RemoteAddr().String(),
Server: *conn,
IsActive: true,
LastUsed: time.Now(),
CreatedAt: time.Now(),
}
// 添加到连接池
s.Connection.Add(tcpConn)
s.Logger.Info(s.ctx, fmt.Sprintf("New connection established: %s", connID))
// 启动消息接收协程
go s.receiveMessages(tcpConn)
}
// receiveMessages 接收消息
func (s *TCPServer) receiveMessages(conn *TcpConnection) {
defer func() {
if err := recover(); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Panic in receiveMessages: %v", err))
}
s.Connection.Remove(conn.Id)
conn.Server.Close()
s.Logger.Info(s.ctx, fmt.Sprintf("Connection closed: %s", conn.Id))
}()
buffer := make([]byte, s.Config.BufferSize)
for {
select {
case <-s.ctx.Done():
return
default:
// 设置读取超时
conn.Server.SetReadDeadline(time.Now().Add(s.Config.ReadTimeout))
// 读取数据
n, err := conn.Server.Read(buffer)
if err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Read error from %s: %v", conn.Id, err))
return
}
if n > 0 {
// 更新最后使用时间
conn.Mutex.Lock()
conn.LastUsed = time.Now()
conn.Mutex.Unlock()
// 处理消息
data := make([]byte, n)
copy(data, buffer[:n])
msg := &TcpMessage{
Id: fmt.Sprintf("msg_%d", gtime.TimestampNano()),
ConnId: conn.Id,
Data: data,
Timestamp: time.Now(),
IsSend: false,
}
// 使用协程池处理消息,避免阻塞
grpool.AddWithRecover(s.ctx, func(ctx context.Context) {
if s.MessageHandler != nil {
if err := s.MessageHandler(conn, msg); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Message handling error: %v", err))
}
}
}, func(ctx context.Context, err error) {
s.Logger.Error(ctx, fmt.Sprintf("Message handling error: %v", err))
})
}
}
}
}
// SendTo 发送消息到指定连接
func (s *TCPServer) SendTo(connID string, data []byte) error {
conn := s.Connection.Get(connID)
if conn == nil {
return fmt.Errorf("connection not found: %s", connID)
}
return s.sendMessage(conn, data)
}
// SendToAll 发送消息到所有连接
func (s *TCPServer) SendToAll(data []byte) error {
conns := s.Connection.GetAll()
for _, conn := range conns {
if err := s.sendMessage(conn, data); err != nil {
s.Logger.Error(s.ctx, fmt.Sprintf("Send to %s failed: %v", conn.Id, err))
// 继续发送给其他连接
}
}
return nil
}
// sendMessage 发送消息
func (s *TCPServer) sendMessage(conn *TcpConnection, data []byte) error {
conn.Mutex.Lock()
defer conn.Mutex.Unlock()
// 设置写入超时
conn.Server.SetWriteDeadline(time.Now().Add(s.Config.WriteTimeout))
// 发送数据
_, err := conn.Server.Write(data)
if err != nil {
return err
}
// 更新最后使用时间
conn.LastUsed = time.Now()
return nil
}
// Kick 强制退出客户端
func (s *TCPServer) Kick(connID string) error {
conn := s.Connection.Get(connID)
if conn == nil {
return fmt.Errorf("connection not found: %s", connID)
}
// 关闭连接
conn.Server.Close()
// 从连接池移除
s.Connection.Remove(connID)
s.Logger.Info(s.ctx, fmt.Sprintf("Kicked connection: %s", connID))
return nil
}
// Add 添加连接到连接池
func (p *ConnectionPool) Add(conn *TcpConnection) {
p.mutex.Lock()
defer p.mutex.Unlock()
p.connections[conn.Id] = conn
}
// Get 获取连接
func (p *ConnectionPool) Get(connID string) *TcpConnection {
p.mutex.RLock()
defer p.mutex.RUnlock()
return p.connections[connID]
}
// GetAll 获取所有连接
func (p *ConnectionPool) GetAll() []*TcpConnection {
p.mutex.RLock()
defer p.mutex.RUnlock()
conns := make([]*TcpConnection, 0, len(p.connections))
for _, conn := range p.connections {
conns = append(conns, conn)
}
return conns
}
// Remove 从连接池移除连接
func (p *ConnectionPool) Remove(connID string) {
p.mutex.Lock()
defer p.mutex.Unlock()
delete(p.connections, connID)
}
// Clear 清空连接池
func (p *ConnectionPool) Clear() {
p.mutex.Lock()
defer p.mutex.Unlock()
for connID, conn := range p.connections {
conn.Server.Close()
delete(p.connections, connID)
}
}
// Count 获取连接数量
func (p *ConnectionPool) Count() int {
p.mutex.RLock()
defer p.mutex.RUnlock()
return len(p.connections)
}

38
tcp/tcpConfig.go Normal file
View File

@ -0,0 +1,38 @@
package tcp
import (
"sync"
"time"
"github.com/gogf/gf/v2/net/gtcp"
)
// TcpPoolConfig TCP连接池配置
type TcpPoolConfig struct {
BufferSize int `json:"bufferSize"` // 缓冲区大小
MaxConnections int `json:"maxConnections"` // 最大连接数
ConnectTimeout time.Duration `json:"connectTimeout"` // 连接超时时间
ReadTimeout time.Duration `json:"readTimeout"` // 读取超时时间
WriteTimeout time.Duration `json:"writeTimeout"` // 写入超时时间
MaxIdleTime time.Duration `json:"maxIdleTime"` // 最大空闲时间
}
// TcpConnection TCP连接结构
type TcpConnection struct {
Id string `json:"id"` // 连接ID
Address string `json:"address"` // 连接地址
Server gtcp.Conn `json:"server"` // 实际连接
IsActive bool `json:"isActive"` // 是否活跃
LastUsed time.Time `json:"lastUsed"` // 最后使用时间
CreatedAt time.Time `json:"createdAt"` // 创建时间
Mutex sync.RWMutex `json:"-"` // 读写锁
}
// TcpMessage TCP消息结构
type TcpMessage struct {
Id string `json:"id"` // 消息ID
ConnId string `json:"connId"` // 连接ID
Data []byte `json:"data"` // 消息数据
Timestamp time.Time `json:"timestamp"` // 时间戳
IsSend bool `json:"isSend"` // 是否是发送的消息
}

70
utils/jwt.go Normal file
View File

@ -0,0 +1,70 @@
package utils
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
)
var (
ErrTokenInvalid = errors.New("token无效")
ErrTokenExpired = errors.New("token已过期")
)
// JWTClaims JWT声明
type JWTClaims struct {
UserID int `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
// GenerateToken 生成JWT Token
func GenerateToken(userID int, username, secret string, expiresInHours int) (string, error) {
claims := JWTClaims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expiresInHours) * time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
NotBefore: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
// ParseToken 解析JWT Token
func ParseToken(tokenString, secret string) (*JWTClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("签名方法无效")
}
return []byte(secret), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
return claims, nil
}
return nil, ErrTokenInvalid
}
// ValidateToken 验证Token是否有效
func ValidateToken(tokenString, secret string) error {
claims, err := ParseToken(tokenString, secret)
if err != nil {
return err
}
if claims.ExpiresAt != nil && claims.ExpiresAt.Time.Before(time.Now()) {
return ErrTokenExpired
}
return nil
}

44
valid/valid.go Normal file
View File

@ -0,0 +1,44 @@
package valid
import (
"github.com/gin-gonic/gin"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
)
// ValidToStruct 验证参数并返回结构体
func ValidToStruct[T any](c *gin.Context) (object *T) {
obj := new(T)
if err := c.Bind(obj); err != nil {
panic(err)
}
if err := g.Validator().Data(obj).Run(c); err != nil {
panic(gerror.Current(err).Error())
}
return obj
}
// ValidToMap 验证参数并返回结构体
func ValidToMap[T any](c *gin.Context) (object map[string]any) {
obj := new(T)
if err := c.Bind(obj); err != nil {
panic(err)
}
if err := g.Validator().Data(obj).Run(c); err != nil {
panic(gerror.Current(err).Error())
}
return gconv.Map(obj)
}
// ValidToStructAndMap 验证参数并返回map
func ValidToStructAndMap[T any](c *gin.Context) (stru *T, object map[string]any) {
obj := new(T)
if err := c.Bind(obj); err != nil {
panic(err)
}
if err := g.Validator().Data(obj).Run(c); err != nil {
panic(gerror.Current(err).Error())
}
return obj, gconv.Map(obj)
}

73
ws/example.go Normal file
View File

@ -0,0 +1,73 @@
package ws
import (
"log"
"net/http"
"time"
"github.com/gogf/gf/v2/util/gconv"
)
var manager = NewWs()
func NewWs() *Manager {
// 1. 自定义配置(可选,也可使用默认配置)
customConfig := &Config{
AllowAllOrigins: true,
HeartbeatInterval: 20 * time.Second, // 20秒发一次心跳
HeartbeatTimeout: 40 * time.Second, // 40秒超时
}
// 2. 创建管理器
m := NewManager(customConfig)
// 3. 覆盖业务回调(核心:自定义消息处理逻辑)
// 连接建立回调
m.OnConnect = func(connID string) {
log.Printf("业务回调:连接[%s]上线,当前在线数:%d", connID, m.GetOnlineCount())
// 欢迎消息
_ = m.SendToConn(connID, []byte("欢迎连接WebSocket服务"))
}
// 收到消息回调
m.OnMessage = func(connID string, msgType int, data any) {
log.Printf("业务回调:收到连接[%s]消息:%s", connID, gconv.String(data))
// 示例echo回复
reply := []byte("服务端回复:" + gconv.String(data))
_ = m.SendToConn(connID, reply)
// 示例:广播消息给所有连接
_ = m.Broadcast([]byte("广播:" + connID + "说:" + gconv.String(data)))
}
// 连接断开回调
m.OnDisconnect = func(connID string, err error) {
log.Printf("业务回调:连接[%s]下线,原因:%v当前在线数%d", connID, err, m.GetOnlineCount())
}
return m
}
func Upgrade(w http.ResponseWriter, r *http.Request, connID string) {
_, err := manager.Upgrade(w, r, connID)
if err != nil {
log.Printf("升级连接失败:%v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
func main() {
// 4. 注册WebSocket路由
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
// 自定义连接ID示例使用请求参数中的user_id
connID := r.URL.Query().Get("user_id")
if connID == "" {
http.Error(w, "user_id不能为空", http.StatusBadRequest)
return
}
// 升级连接
Upgrade(w, r, connID)
})
// 5. 启动服务
log.Println("WebSocket服务启动http://localhost:8080/ws")
log.Fatal(http.ListenAndServe(":8080", nil))
}

488
ws/websocket.go Normal file
View File

@ -0,0 +1,488 @@
package ws
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"sync"
"time"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/os/gctx"
"github.com/gogf/gf/v2/os/gtime"
"github.com/gogf/gf/v2/os/gtimer"
"github.com/gogf/gf/v2/text/gstr"
"github.com/gogf/gf/v2/util/gconv"
"github.com/gorilla/websocket"
)
// 常量定义:默认配置
const (
// DefaultReadBufferSize 默认读写缓冲区大小(字节)
DefaultReadBufferSize = 1024
DefaultWriteBufferSize = 1024
// DefaultHeartbeatInterval 默认心跳间隔每30秒发送一次心跳
DefaultHeartbeatInterval = 30 * time.Second
// DefaultHeartbeatTimeout 默认心跳超时60秒未收到客户端心跳响应则关闭连接
DefaultHeartbeatTimeout = 60 * time.Second
// DefaultReadTimeout 默认读写超时(秒)
DefaultReadTimeout = 60 * time.Second
DefaultWriteTimeout = 10 * time.Second
// MessageTypeText 消息类型
MessageTypeText = websocket.TextMessage
MessageTypeBinary = websocket.BinaryMessage
// HeartbeatMaxRetry 心跳最大重试次数
HeartbeatMaxRetry = 3
)
// Config WebSocket服务端配置
type Config struct {
// 读写缓冲区大小
ReadBufferSize int
WriteBufferSize int
// 跨域配置是否允许所有跨域生产环境建议指定Origin
AllowAllOrigins bool
// 允许的跨域Origin列表AllowAllOrigins=false时生效
AllowedOrigins []string
// 心跳配置
HeartbeatInterval time.Duration // 心跳发送间隔
HeartbeatTimeout time.Duration // 心跳超时时间
// 读写超时
ReadTimeout time.Duration
WriteTimeout time.Duration
MsgType int // 发送消息的默认类型
HeartbeatValue string // 心跳消息的标识字段值(如"heartbeat"、"pong"
HeartbeatKey string // 心跳消息的标识字段名(如"type"
}
// 默认配置
func DefaultConfig() *Config {
return &Config{
ReadBufferSize: DefaultReadBufferSize,
WriteBufferSize: DefaultWriteBufferSize,
AllowAllOrigins: true,
AllowedOrigins: []string{},
HeartbeatInterval: DefaultHeartbeatInterval,
HeartbeatTimeout: DefaultHeartbeatTimeout,
ReadTimeout: DefaultReadTimeout,
WriteTimeout: DefaultWriteTimeout,
MsgType: MessageTypeText,
HeartbeatValue: "heartbeat",
HeartbeatKey: "type", // 心跳消息的标识字段名,默认"type"
}
}
// Connection WebSocket连接结构体
type Connection struct {
conn *websocket.Conn // 底层连接
connID string // 唯一连接ID
manager *Manager // 所属管理器
createTime time.Time // 连接创建时间
heartbeatChan time.Time // 心跳通道(用于检测客户端响应)
heartbeatTime *gtimer.Entry
ctx context.Context // 上下文
cancel context.CancelFunc // 上下文取消函数
writeMutex sync.Mutex // 写消息互斥锁(防止并发写)
heartbeatRetry int // 心跳发送重试次数
}
// Manager WebSocket连接管理器
type Manager struct {
config *Config // 配置
upgrader *websocket.Upgrader // HTTP升级器
connections map[string]*Connection // 所有在线连接connID -> Connection
mutex sync.RWMutex // 读写锁保护connections
// 业务回调:收到消息时触发(用户自定义处理逻辑)
OnMessage func(connID string, msgType int, data any)
// 业务回调:连接建立时触发
OnConnect func(connID string)
// 业务回调:连接关闭时触发
OnDisconnect func(connID string, err error)
}
// Merge 合并配置,用传入的配置覆盖非零值部分
func (c *Config) Merge(other *Config) *Config {
result := *c // 复制当前配置
if other == nil {
return &result
}
if other.ReadBufferSize > 0 {
result.ReadBufferSize = other.ReadBufferSize
}
if other.WriteBufferSize > 0 {
result.WriteBufferSize = other.WriteBufferSize
}
if other.HeartbeatInterval > 0 {
result.HeartbeatInterval = other.HeartbeatInterval
}
if other.HeartbeatTimeout > 0 {
result.HeartbeatTimeout = other.HeartbeatTimeout
}
if other.ReadTimeout > 0 {
result.ReadTimeout = other.ReadTimeout
}
if other.WriteTimeout > 0 {
result.WriteTimeout = other.WriteTimeout
}
if other.AllowAllOrigins {
result.AllowAllOrigins = other.AllowAllOrigins
}
if other.HeartbeatValue != "" {
result.HeartbeatValue = other.HeartbeatValue
}
if other.HeartbeatKey != "" {
result.HeartbeatKey = other.HeartbeatKey
}
if len(other.AllowedOrigins) > 0 {
result.AllowedOrigins = other.AllowedOrigins
}
if other.MsgType != 0 {
result.MsgType = other.MsgType
}
return &result
}
// NewManager 创建连接管理器
func NewManager(config *Config) *Manager {
defaultConfig := DefaultConfig()
finalConfig := defaultConfig.Merge(config)
// 初始化升级器
upgrader := &websocket.Upgrader{
ReadBufferSize: config.ReadBufferSize,
WriteBufferSize: config.WriteBufferSize,
CheckOrigin: func(r *http.Request) bool {
// 跨域检查
if config.AllowAllOrigins {
return true
}
origin := r.Header.Get("Origin")
for _, allowed := range finalConfig.AllowedOrigins {
if origin == allowed {
return true
}
}
return false
},
}
return &Manager{
config: finalConfig,
upgrader: upgrader,
connections: make(map[string]*Connection),
mutex: sync.RWMutex{},
// 默认回调(用户可覆盖)
OnMessage: func(connID string, msgType int, data any) {
log.Printf("[默认回调] 收到连接[%s]消息:%s", connID, gconv.String(data))
},
OnConnect: func(connID string) {
log.Printf("[默认回调] 连接[%s]已建立", connID)
},
OnDisconnect: func(connID string, err error) {
log.Printf("[默认回调] 连接[%s]已关闭:%v", connID, err)
},
}
}
// Upgrade HTTP升级为WebSocket连接
// connID自定义连接唯一ID如用户ID、设备ID
func (m *Manager) Upgrade(w http.ResponseWriter, r *http.Request, connID string) (*Connection, error) {
if connID == "" {
return nil, errors.New("连接ID不能为空")
}
// 检查连接ID是否已存在
m.mutex.RLock()
_, exists := m.connections[connID]
m.mutex.RUnlock()
if exists {
return nil, fmt.Errorf("连接ID[%s]已存在", connID)
}
// 升级HTTP连接
conn, err := m.upgrader.Upgrade(w, r, nil)
if err != nil {
return nil, fmt.Errorf("升级WebSocket失败%w", err)
}
// 创建上下文(用于优雅关闭)
ctx, cancel := context.WithCancel(context.Background())
// 创建连接实例
wsConn := &Connection{
conn: conn,
connID: connID,
manager: m,
createTime: time.Now(),
heartbeatChan: time.Now(), // 缓冲1防止阻塞
ctx: ctx,
cancel: cancel,
writeMutex: sync.Mutex{},
heartbeatRetry: 0,
}
wsConn.heartbeatTime = gtimer.AddSingleton(gctx.New(), m.config.HeartbeatTimeout, func(ctx context.Context) {
log.Printf("[心跳检测] 连接[%s]已关闭:心跳超时", wsConn.connID)
wsConn.heartbeatTime.Close()
wsConn.heartbeatTime.Stop()
wsConn.heartbeatTime = nil
wsConn.ctx.Done()
wsConn.Close(fmt.Errorf("心跳超时"))
})
// 添加到管理器
m.mutex.Lock()
m.connections[connID] = wsConn
m.mutex.Unlock()
// 触发连接建立回调
m.OnConnect(connID)
// 启动读消息协程
go wsConn.ReadPump()
// 启动写消息协程(处理异步发送)
go wsConn.WritePump()
// 启动心跳检测协程
go wsConn.Heartbeat()
return wsConn, nil
}
// ReadPump 读取客户端消息(持续运行)
func (c *Connection) ReadPump() {
defer func() {
// 发生panic时关闭连接
if err := recover(); err != nil {
log.Printf("连接[%s]读消息协程panic%v", c.connID, err)
}
// 关闭连接并清理
c.Close(fmt.Errorf("读消息协程退出"))
}()
// 循环读取消息
for {
select {
case <-c.ctx.Done():
return // 上下文已取消,退出
default:
// 设置读超时(每次读取前重置,防止长时间无消息超时)
c.conn.SetReadDeadline(time.Now().Add(c.manager.config.ReadTimeout))
// 读取客户端消息
msgType, data, err := c.conn.ReadMessage()
if err != nil {
// 区分正常关闭和异常错误
var closeErr *websocket.CloseError
if errors.As(err, &closeErr) {
c.Close(fmt.Errorf("客户端主动关闭:%s代码%d", closeErr.Text, closeErr.Code))
} else {
c.Close(fmt.Errorf("读取消息失败:%w", err))
}
return
}
// 尝试解析JSON格式的心跳消息精准判断替代包含判断
isHeartbeat := false
// 先尝试解析为JSON对象
var msgMap map[string]interface{}
if err := gjson.DecodeTo(data, &msgMap); err == nil {
// 获取心跳标识字段的值
heartbeatValue := gconv.String(msgMap[c.manager.config.HeartbeatKey])
if heartbeatValue == c.manager.config.HeartbeatValue {
isHeartbeat = true
}
} else {
// 非JSON格式降级为包含判断兼容纯文本心跳
str := gconv.String(data)
if gstr.Contains(str, c.manager.config.HeartbeatValue) {
isHeartbeat = true
}
}
if isHeartbeat {
log.Printf("[心跳] 收到连接[%s]心跳消息:%s", c.connID, string(data))
// 心跳消息:重置重试次数 + 发送心跳信号 + 重置读超时
js, err := gjson.Encode(&Msg[any]{c.manager.config.HeartbeatValue, nil, gtime.Timestamp()})
if err != nil {
log.Printf("[心跳] 客户端[%s]json编码失败", c.connID)
continue
}
err = c.Send(js)
if err != nil {
log.Printf("[心跳] 客户端[%s]发送心跳消息失败", c.connID)
continue
}
c.heartbeatTime.Reset()
continue // 跳过业务回调
}
// 非心跳消息:触发业务回调
c.manager.OnMessage(c.connID, msgType, data)
}
}
}
type Msg[T any] struct {
Type string `json:"type"`
Data T `json:"data"`
Timestamp int64 `json:"timestamp"`
}
// WritePump 处理异步写消息(持续运行)
// 扩展为监听写队列,防止消息丢失
func (c *Connection) WritePump() {
defer func() {
if err := recover(); err != nil {
log.Printf("连接[%s]写消息协程panic%v", c.connID, err)
}
}()
// 暂时保持简化,实际可扩展为带缓冲的写队列
<-c.ctx.Done()
}
// Heartbeat 心跳检测(持续运行)
func (c *Connection) Heartbeat() {
defer func() {
if err := recover(); err != nil {
log.Printf("连接[%s]心跳协程panic%v", c.connID, err)
}
}()
c.heartbeatTime.Start()
}
// Send 发送消息到客户端(线程安全)
func (c *Connection) Send(data []byte) error {
select {
case <-c.ctx.Done():
return errors.New("连接已关闭,无法发送消息")
default:
// 加锁防止并发写
c.writeMutex.Lock()
defer c.writeMutex.Unlock()
// 设置写超时
c.conn.SetWriteDeadline(time.Now().Add(c.manager.config.WriteTimeout))
// 发送消息(使用连接的默认类型,支持动态调整)
err := c.conn.WriteMessage(c.manager.config.MsgType, data)
if err != nil {
return fmt.Errorf("发送消息失败:%w", err)
}
return nil
}
}
// Close 关闭连接(优雅清理)
func (c *Connection) Close(err error) {
// 防止重复关闭
select {
case <-c.ctx.Done():
return
default:
}
// 取消上下文(终止所有协程)
c.cancel()
// 关闭底层连接(友好关闭)
_ = c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, err.Error()))
_ = c.conn.Close()
// 从管理器移除
c.manager.mutex.Lock()
delete(c.manager.connections, c.connID)
c.manager.mutex.Unlock()
// 触发断开回调
c.manager.OnDisconnect(c.connID, err)
log.Printf("连接[%s]已关闭,当前在线数:%d原因%v", c.connID, c.manager.GetOnlineCount(), err)
}
// GetOnlineCount 获取在线连接数
func (m *Manager) GetOnlineCount() int {
m.mutex.RLock()
defer m.mutex.RUnlock()
return len(m.connections)
}
// Broadcast 广播消息到所有在线连接
func (m *Manager) Broadcast(data []byte) error {
m.mutex.RLock()
defer m.mutex.RUnlock()
if len(m.connections) == 0 {
return errors.New("无在线连接")
}
// 并发发送(非阻塞)
var wg sync.WaitGroup
var errMsg string
for _, conn := range m.connections {
wg.Add(1)
go func(c *Connection) {
defer wg.Done()
if err := c.Send(data); err != nil {
errMsg += fmt.Sprintf("连接[%s]广播失败:%v", c.connID, err)
}
}(conn)
}
wg.Wait()
if errMsg != "" {
return errors.New(errMsg)
}
return nil
}
// SendToConn 定向发送消息到指定连接
func (m *Manager) SendToConn(connID string, data []byte) error {
m.mutex.RLock()
conn, exists := m.connections[connID]
m.mutex.RUnlock()
if !exists {
return fmt.Errorf("连接[%s]不存在", connID)
}
return conn.Send(data)
}
func (m *Manager) GetAllConn() map[string]*Connection {
m.mutex.RLock()
defer m.mutex.RUnlock()
// 返回副本,防止外部修改
connCopy := make(map[string]*Connection, len(m.connections))
for k, v := range m.connections {
connCopy[k] = v
}
return connCopy
}
func (m *Manager) GetConn(connID string) *Connection {
m.mutex.RLock()
defer m.mutex.RUnlock()
return m.connections[connID]
}
// CloseAll 关闭所有连接
func (m *Manager) CloseAll() {
m.mutex.RLock()
connIDs := make([]string, 0, len(m.connections))
for connID := range m.connections {
connIDs = append(connIDs, connID)
}
m.mutex.RUnlock()
for _, connID := range connIDs {
m.mutex.RLock()
conn := m.connections[connID]
m.mutex.RUnlock()
if conn != nil {
conn.Close(errors.New("服务端主动关闭所有连接"))
}
}
}