Compare commits
43 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
f50930ec74 | |
|
|
d50f150d98 | |
|
|
0238ec9a01 | |
|
|
1cc976ed72 | |
|
|
7d3ddc62e3 | |
|
|
427f568db6 | |
|
|
828e19de93 | |
|
|
1e18be8326 | |
|
|
21e9f7c79d | |
|
|
93fbb99ee0 | |
|
|
d41b554acf | |
|
|
7b6026950a | |
|
|
80ad00f98d | |
|
|
3209cfe830 | |
|
|
ed7d18a72a | |
|
|
f710f03be0 | |
|
|
5d7e584ffd | |
|
|
6661ad7fb3 | |
|
|
af31b688cf | |
|
|
abf5529019 | |
|
|
4a3339fcfb | |
|
|
7925439270 | |
|
|
a656e00daa | |
|
|
ca82c166d0 | |
|
|
6805772746 | |
|
|
eeb671cd0d | |
|
|
3096d7dedd | |
|
|
5affb5e653 | |
|
|
de8348e424 | |
|
|
1a33330214 | |
|
|
5581041da4 | |
|
|
8bacccb894 | |
|
|
fa756c25fe | |
|
|
e4feed241f | |
|
|
20d43c4e92 | |
|
|
c96cb92d3d | |
|
|
2a7ab5f216 | |
|
|
d958f4c059 | |
|
|
86116618e4 | |
|
|
2520655fbd | |
|
|
d4f78124d8 | |
|
|
8d1c11d76d | |
|
|
d409dc0e2f |
|
|
@ -3,10 +3,9 @@
|
|||
.project
|
||||
.orig
|
||||
.swp
|
||||
.idea/
|
||||
.idea
|
||||
.settings/
|
||||
.vscode/
|
||||
bin/
|
||||
.vscode
|
||||
**/.DS_Store
|
||||
gf
|
||||
main
|
||||
|
|
@ -16,4 +15,5 @@ manifest/output/
|
|||
temp/
|
||||
temp.yaml
|
||||
bin
|
||||
**/config/config.yaml
|
||||
**/config/config.yaml
|
||||
.arts
|
||||
|
|
@ -3,6 +3,20 @@
|
|||
<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="ApiFile">
|
||||
<value>
|
||||
<set>
|
||||
<option value="file://$PROJECT_DIR$/response/code.go" />
|
||||
</set>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="BaseConfig">
|
||||
<value>
|
||||
<set>
|
||||
|
|
@ -31,6 +45,13 @@
|
|||
</set>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="ConnectionInfo">
|
||||
<value>
|
||||
<set>
|
||||
<option value="file://$PROJECT_DIR$/pool/common.go" />
|
||||
</set>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="ConnectionPool">
|
||||
<value>
|
||||
<set>
|
||||
|
|
@ -38,10 +59,10 @@
|
|||
</set>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="Curd">
|
||||
<entry key="Crud">
|
||||
<value>
|
||||
<set>
|
||||
<option value="file://$PROJECT_DIR$/curd/curd.go" />
|
||||
<option value="file://$PROJECT_DIR$/crud/curd.go" />
|
||||
</set>
|
||||
</value>
|
||||
</entry>
|
||||
|
|
@ -80,24 +101,17 @@
|
|||
</set>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="NewsOne">
|
||||
<value>
|
||||
<set>
|
||||
<option value="file://$PROJECT_DIR$/../gin_test/api/new.go" />
|
||||
</set>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="NewsSave">
|
||||
<value>
|
||||
<set>
|
||||
<option value="file://$PROJECT_DIR$/../gin_test/api/new.go" />
|
||||
</set>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="Paginate">
|
||||
<value>
|
||||
<set>
|
||||
<option value="file://$PROJECT_DIR$/curd/curd.go" />
|
||||
<option value="file://$PROJECT_DIR$/crud/curd.go" />
|
||||
</set>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="SQLitePool">
|
||||
<value>
|
||||
<set>
|
||||
<option value="file://$PROJECT_DIR$/pool/sqlite.go" />
|
||||
</set>
|
||||
</value>
|
||||
</entry>
|
||||
|
|
@ -136,7 +150,16 @@
|
|||
</set>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="api">
|
||||
<entry key="logWriter">
|
||||
<value>
|
||||
<set>
|
||||
<option value="file://$PROJECT_DIR$/../gf-common/log/log.go" />
|
||||
<option value="file://$PROJECT_DIR$/log/log.go" />
|
||||
<option value="file://$PROJECT_DIR$/../yingji/api-gateway/internal/utils/log.go" />
|
||||
</set>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="resFile">
|
||||
<value>
|
||||
<set>
|
||||
<option value="file://$PROJECT_DIR$/response/code.go" />
|
||||
|
|
@ -154,10 +177,29 @@
|
|||
</option>
|
||||
<option name="scannedPathMapping">
|
||||
<map>
|
||||
<entry key="file://$PROJECT_DIR$/../gf-common/log/log.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1772763640548" />
|
||||
<option name="schema">
|
||||
<list>
|
||||
<option value="logWriter" />
|
||||
</list>
|
||||
</option>
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/../gf-common/server/gateway.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1772764256932" />
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/config/fun.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1770427966184" />
|
||||
<option name="lastModified" value="1772004328410" />
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
|
|
@ -183,14 +225,14 @@
|
|||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/curd/curd.go">
|
||||
<entry key="file://$PROJECT_DIR$/crud/curd.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1770362565302" />
|
||||
<option name="lastModified" value="1772676907769" />
|
||||
<option name="schema">
|
||||
<list>
|
||||
<option value="Paginate" />
|
||||
<option value="Curd" />
|
||||
<option value="Crud" />
|
||||
</list>
|
||||
</option>
|
||||
</ScannedPath>
|
||||
|
|
@ -227,7 +269,12 @@
|
|||
<entry key="file://$PROJECT_DIR$/log/log.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1770011076335" />
|
||||
<option name="lastModified" value="1772763464061" />
|
||||
<option name="schema">
|
||||
<list>
|
||||
<option value="logWriter" />
|
||||
</list>
|
||||
</option>
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
|
|
@ -257,19 +304,52 @@
|
|||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/pool/common.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1772181310902" />
|
||||
<option name="schema">
|
||||
<list>
|
||||
<option value="ConnectionInfo" />
|
||||
</list>
|
||||
</option>
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/pool/sqlite.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1772181310902" />
|
||||
<option name="schema">
|
||||
<list>
|
||||
<option value="SQLitePool" />
|
||||
</list>
|
||||
</option>
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/response/code.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1770444657704" />
|
||||
<option name="lastModified" value="1772068030461" />
|
||||
<option name="schema">
|
||||
<list>
|
||||
<option value="response" />
|
||||
<option value="api" />
|
||||
<option value="Api" />
|
||||
<option value="resFile" />
|
||||
<option value="ApiFile" />
|
||||
</list>
|
||||
</option>
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/server/gateway.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1772765089927" />
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/server/server.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
|
|
@ -280,14 +360,14 @@
|
|||
<entry key="file://$PROJECT_DIR$/tcp/example.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1770025697304" />
|
||||
<option name="lastModified" value="1772159997490" />
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/tcp/tcp.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1770025697304" />
|
||||
<option name="lastModified" value="1772416836034" />
|
||||
<option name="schema">
|
||||
<list>
|
||||
<option value="TCPServer" />
|
||||
|
|
@ -300,7 +380,7 @@
|
|||
<entry key="file://$PROJECT_DIR$/tcp/tcpConfig.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1770025697304" />
|
||||
<option name="lastModified" value="1772413520343" />
|
||||
<option name="schema">
|
||||
<list>
|
||||
<option value="TcpPoolConfig" />
|
||||
|
|
@ -311,10 +391,17 @@
|
|||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/utils/bcrypt.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1772432291705" />
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/utils/file.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1769850381300" />
|
||||
<option name="lastModified" value="1772592717105" />
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
|
|
@ -337,24 +424,31 @@
|
|||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/utils/time.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1772431247912" />
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/valid/valid.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1772004278917" />
|
||||
<option name="lastModified" value="1772007792251" />
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/ws/example.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1770025697531" />
|
||||
<option name="lastModified" value="1772181056448" />
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/ws/websocket.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1770018907536" />
|
||||
<option name="lastModified" value="1772185691346" />
|
||||
<option name="schema">
|
||||
<list>
|
||||
<option value="Config" />
|
||||
|
|
@ -366,60 +460,55 @@
|
|||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/../gin_test/api/new.go">
|
||||
<entry key="file://$PROJECT_DIR$/../yingji/api-gateway/internal/cmd/cmd.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1772003979851" />
|
||||
<option name="lastModified" value="1772763999954" />
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/../yingji/api-gateway/internal/utils/log.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1772763430097" />
|
||||
<option name="schema">
|
||||
<list>
|
||||
<option value="NewsOne" />
|
||||
<option value="NewsSave" />
|
||||
<option value="logWriter" />
|
||||
</list>
|
||||
</option>
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/../gin_test/router/homeRouter.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1772003574712" />
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="file://$PROJECT_DIR$/../gin_test/router/router.go">
|
||||
<value>
|
||||
<ScannedPath>
|
||||
<option name="lastModified" value="1772003574719" />
|
||||
</ScannedPath>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</option>
|
||||
<option name="tableStructMapping">
|
||||
<map>
|
||||
<entry key="api" value="api" />
|
||||
<entry key="api" value="Api" />
|
||||
<entry key="api_file" value="ApiFile" />
|
||||
<entry key="base_config" value="BaseConfig" />
|
||||
<entry key="client" value="Client" />
|
||||
<entry key="config" value="Config" />
|
||||
<entry key="connection" value="Connection" />
|
||||
<entry key="connection_info" value="ConnectionInfo" />
|
||||
<entry key="connection_pool" value="ConnectionPool" />
|
||||
<entry key="curd" value="Curd" />
|
||||
<entry key="crud" value="Crud" />
|
||||
<entry key="data_base_config" value="DataBaseConfig" />
|
||||
<entry key="jwt_claims" value="JWTClaims" />
|
||||
<entry key="jwt_config" value="JwtConfig" />
|
||||
<entry key="log_writer" value="logWriter" />
|
||||
<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="res_file" value="resFile" />
|
||||
<entry key="response" value="response" />
|
||||
<entry key="server_config" value="ServerConfig" />
|
||||
<entry key="sq_lite_pool" value="SQLitePool" />
|
||||
<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="1772003052939" />
|
||||
<option name="lastTimeChecked" value="1772761874263" />
|
||||
</component>
|
||||
</project>
|
||||
|
|
@ -60,6 +60,7 @@ func SetDefault() {
|
|||
viper.Set("SERVER.mode", "release")
|
||||
viper.Set("DATABASE.type", "sqlite")
|
||||
viper.Set("DATABASE.dns", gfile.Join(gfile.Pwd(), "db", "database.db"))
|
||||
viper.Set("DATABASE.debug", true)
|
||||
viper.Set("JWT.secret", "SET-YOUR-SECRET")
|
||||
viper.Set("JWT.expire", 86400)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ type ServerConfig struct {
|
|||
|
||||
// DataBaseConfig 数据库配置
|
||||
type DataBaseConfig struct {
|
||||
Dns string `mapstructure:"dns"`
|
||||
Type string `mapstructure:"type"`
|
||||
Dns string `mapstructure:"dns"`
|
||||
Type string `mapstructure:"type"`
|
||||
Debug bool `mapstructure:"debug"` // 是否开启 GORM 查询日志
|
||||
}
|
||||
|
||||
// JwtConfig JWT配置
|
||||
|
|
|
|||
|
|
@ -0,0 +1,149 @@
|
|||
# CRUD 测试用例说明
|
||||
|
||||
## 测试文件
|
||||
|
||||
### 1. `curd_func_test.go` - 不依赖数据库的测试
|
||||
测试所有不依赖数据库的函数,包括:
|
||||
- `TestBuildWhere` - 测试 BuildWhere 方法及其辅助函数
|
||||
- `TestBuildMap` - 测试 BuildMap 方法
|
||||
- `TestClearField` - 测试 ClearField 方法
|
||||
- `TestBuildWhereAndOr` - 测试 BuildWhereAndOr 构建器
|
||||
- `TestPaginateStruct` - 测试 Paginate 结构体
|
||||
- `TestPageInfo` - 测试 pageInfo 常量
|
||||
|
||||
**运行方式:**
|
||||
```bash
|
||||
go test -v ./crud -run "TestBuild|TestClear|TestPaginate|TestPage"
|
||||
```
|
||||
|
||||
### 2. `curd_test.go` - 依赖数据库的测试
|
||||
测试所有需要数据库的 CRUD 操作方法,包括:
|
||||
- `TestCrud_BuildWhere` - 完整的 BuildWhere 测试
|
||||
- `TestCrud_ClearFieldPage` - 分页查询测试
|
||||
- `TestCrud_ClearFieldList` - 列表查询测试
|
||||
- `TestCrud_ClearFieldOne` - 单条查询测试
|
||||
- `TestCrud_Value` - 字段值查询测试
|
||||
- `TestCrud_DeletePri` - 按主键删除测试
|
||||
- `TestCrud_DeleteWhere` - 按条件删除测试
|
||||
- `TestCrud_Sum` - 求和测试
|
||||
- `TestCrud_ArrayField` - 字段数组查询测试
|
||||
- `TestCrud_FindPri` - 按主键查询测试
|
||||
- `TestCrud_First` - 查询第一条测试
|
||||
- `TestCrud_Exists` - 存在性检查测试
|
||||
- `TestCrud_All` - 查询所有测试
|
||||
- `TestCrud_Count` - 统计测试
|
||||
- `TestCrud_Save` - 保存测试
|
||||
- `TestCrud_Update` - 更新测试
|
||||
- `TestCrud_UpdatePri` - 按主键更新测试
|
||||
- `TestCrud_Paginate` - 分页查询测试
|
||||
- `TestHelperFunctions` - 辅助函数测试
|
||||
|
||||
**运行方式:**
|
||||
```bash
|
||||
# 需要 MySQL 服务
|
||||
go test -v ./crud -run TestCrud
|
||||
```
|
||||
|
||||
**注意:** 数据库测试需要 MySQL 服务,连接信息:
|
||||
- Host: 127.0.0.1:3306
|
||||
- User: root
|
||||
- Password: root
|
||||
- Database: test
|
||||
|
||||
如果没有 MySQL 服务,这些测试会自动跳过。
|
||||
|
||||
## 运行所有测试
|
||||
|
||||
```bash
|
||||
# 运行所有测试
|
||||
go test -v ./crud
|
||||
|
||||
# 运行特定测试
|
||||
go test -v ./crud -run TestBuildWhere
|
||||
|
||||
# 显示覆盖率
|
||||
go test -v ./crud -cover
|
||||
```
|
||||
|
||||
## 测试覆盖的函数列表
|
||||
|
||||
### 核心方法
|
||||
- ✅ `BuildWhere` - 构建查询条件 map
|
||||
- ✅ `BuildMap` - 构建变更条件 map
|
||||
- ✅ `BuildWhereAndOr` - AND/OR 查询条件构建器
|
||||
- ✅ `BuildWhereGORM` - GORM 原生语法构建器
|
||||
- ✅ `ClearField` - 清理请求参数
|
||||
- ✅ `ClearFieldPage` - 清理参数 + 分页查询
|
||||
- ✅ `ClearFieldList` - 清理参数 + 列表查询
|
||||
- ✅ `ClearFieldOne` - 清理参数 + 单条查询
|
||||
|
||||
### 查询方法
|
||||
- ✅ `Value` - 查询单个字段值
|
||||
- ✅ `FindPri` - 按主键查询单条记录
|
||||
- ✅ `First` - 按条件查询第一条记录
|
||||
- ✅ `Exists` - 判断记录是否存在
|
||||
- ✅ `All` - 查询所有符合条件的记录
|
||||
- ✅ `Count` - 统计记录总数
|
||||
- ✅ `ArrayField` - 查询指定字段数组
|
||||
- ✅ `Sum` - 字段求和
|
||||
|
||||
### 操作方法
|
||||
- ✅ `Save` - 新增/更新记录
|
||||
- ✅ `Update` - 按条件更新记录
|
||||
- ✅ `UpdatePri` - 按主键更新记录
|
||||
- ✅ `DeletePri` - 按主键删除
|
||||
- ✅ `DeleteWhere` - 按条件删除
|
||||
- ✅ `Paginate` - 分页查询
|
||||
|
||||
### 辅助函数
|
||||
- ✅ `convToMap` - 类型转换为 map
|
||||
- ✅ `isEmpty` - 判断值是否为空
|
||||
- ✅ `strInArray` - 判断字符串是否在数组中
|
||||
- ✅ `caseConvert` - 字段名风格转换
|
||||
|
||||
## 测试示例
|
||||
|
||||
### BuildWhereAndOr 使用示例
|
||||
```go
|
||||
func TestBuildWhereAndOr(t *testing.T) {
|
||||
var crud Crud[interface{}]
|
||||
|
||||
// 混合使用 AND 和 OR
|
||||
where := crud.BuildWhereAndOr().
|
||||
AND(map[string]any{"status": 1}).
|
||||
OR(
|
||||
map[string]any{"age >": 25},
|
||||
map[string]any{"vip": true},
|
||||
).
|
||||
Build()
|
||||
|
||||
if where == nil {
|
||||
t.Error("Expected where to not be nil")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### BuildWhereGORM 使用示例
|
||||
```go
|
||||
func TestBuildWhereGORM(t *testing.T) {
|
||||
var crud Crud[TestModel]
|
||||
|
||||
// 使用 GORM 原生语法
|
||||
var results []*TestModel
|
||||
err := crud.BuildWhereGORM("status = ?", 1).
|
||||
Where("age > ?", 20).
|
||||
Or("vip = ?", true).
|
||||
Find(&results)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 数据库测试需要 MySQL 服务,如果不可用会自动跳过
|
||||
2. 所有测试都是独立的,不会相互影响
|
||||
3. 每个测试都会创建自己的测试数据
|
||||
4. 测试完成后会自动清理资源
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package utils
|
||||
package crud
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
|
@ -16,12 +16,13 @@ type ctx = context.Context
|
|||
|
||||
// IDao -------------------------- 核心接口定义 --------------------------
|
||||
// IDao GORM 版本的Dao接口,提供GORM DB实例和表相关信息
|
||||
type IDao interface {
|
||||
type IDao[T any] 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 // 事务方法
|
||||
Column() *T
|
||||
}
|
||||
|
||||
// Paginate -------------------------- 分页结构体定义 --------------------------
|
||||
|
|
@ -46,10 +47,10 @@ var pageInfo = []string{
|
|||
"page_num",
|
||||
}
|
||||
|
||||
// Curd -------------------------- 泛型CURD核心结构体 --------------------------
|
||||
// Curd GORM 版本的泛型CURD封装,R为对应的模型结构体
|
||||
type Curd[R any] struct {
|
||||
Dao IDao
|
||||
// Crud -------------------------- 泛型CURD核心结构体 --------------------------
|
||||
// Crud GORM 版本的泛型CURD封装,R为对应的模型结构体, C为对应模型结构体的字段结构体
|
||||
type Crud[R any, C any] struct {
|
||||
Dao IDao[C]
|
||||
}
|
||||
|
||||
// -------------------------- 工具方法:字段名转换(保持原代码的命名风格转换) --------------------------
|
||||
|
|
@ -77,8 +78,12 @@ func caseConvert(key string, toSnake bool) string {
|
|||
return result.String()
|
||||
}
|
||||
|
||||
func (c Crud[R, C]) Columns() *C {
|
||||
return c.Dao.Column()
|
||||
}
|
||||
|
||||
// BuildWhere -------------------------- 原BuildWhere对应实现:构建查询条件map --------------------------
|
||||
func (c Curd[R]) BuildWhere(req any, changeWhere any, subWhere any, removeFields []string, isSnake ...bool) map[string]any {
|
||||
func (c Crud[R, C]) BuildWhere(req any, changeWhere any, subWhere any, removeFields []string, isSnake ...bool) map[string]any {
|
||||
// 默认使用小写下划线方式
|
||||
toSnake := true
|
||||
if len(isSnake) > 0 && !isSnake[0] {
|
||||
|
|
@ -169,8 +174,8 @@ func (c Curd[R]) BuildWhere(req any, changeWhere any, subWhere any, removeFields
|
|||
return resultMap
|
||||
}
|
||||
|
||||
// BuildMap -------------------------- 原BuildMap对应实现:构建变更条件map --------------------------
|
||||
func (c Curd[R]) BuildMap(op string, value any, field ...string) map[string]any {
|
||||
// BuildMap -------------------------- 原 BuildMap 对应实现:构建变更条件 map --------------------------
|
||||
func (c Crud[R, C]) BuildMap(op string, value any, field ...string) map[string]any {
|
||||
res := map[string]any{
|
||||
"op": op,
|
||||
"field": "",
|
||||
|
|
@ -182,8 +187,214 @@ func (c Curd[R]) BuildMap(op string, value any, field ...string) map[string]any
|
|||
return res
|
||||
}
|
||||
|
||||
// WhereCondition -------------------------- AND/OR 查询条件结构体 --------------------------
|
||||
// WhereCondition 用于构建复杂的 AND/OR 查询条件
|
||||
type WhereCondition struct {
|
||||
AND []interface{} // AND 条件列表
|
||||
OR []interface{} // OR 条件列表
|
||||
}
|
||||
|
||||
// BuildWhereAndOr -------------------------- 新增:支持 AND 和 OR 的查询条件构建 --------------------------
|
||||
// BuildWhereAndOr 构建支持 AND 和 OR 混合使用的查询条件
|
||||
// 用法示例:
|
||||
//
|
||||
// where := crud.BuildWhereAndOr().
|
||||
// AND(map[string]any{"status": 1}).
|
||||
// OR(
|
||||
// map[string]any{"age": 18},
|
||||
// map[string]any{"name": "test"},
|
||||
// ).
|
||||
// AND(map[string]any{"deleted": 0}).
|
||||
// Build()
|
||||
func (c Crud[R, C]) BuildWhereAndOr() *WhereBuilder[R, C] {
|
||||
return &WhereBuilder[R, C]{
|
||||
conditions: make([]WhereCondition, 0),
|
||||
crud: c,
|
||||
}
|
||||
}
|
||||
|
||||
// WhereBuilder -------------------------- WHERE 条件构建器 --------------------------
|
||||
// WhereBuilder 流式构建 WHERE 条件(R 为模型类型参数)
|
||||
type WhereBuilder[R any, C any] struct {
|
||||
conditions []WhereCondition
|
||||
crud Crud[R, C]
|
||||
}
|
||||
|
||||
// AND 添加 AND 条件
|
||||
func (wb *WhereBuilder[R, C]) AND(conditions ...interface{}) *WhereBuilder[R, C] {
|
||||
if len(conditions) > 0 {
|
||||
wb.conditions = append(wb.conditions, WhereCondition{
|
||||
AND: conditions,
|
||||
})
|
||||
}
|
||||
return wb
|
||||
}
|
||||
|
||||
// OR 添加 OR 条件(OR 条件内部是或关系)
|
||||
func (wb *WhereBuilder[R, C]) OR(conditions ...interface{}) *WhereBuilder[R, C] {
|
||||
if len(conditions) > 0 {
|
||||
wb.conditions = append(wb.conditions, WhereCondition{
|
||||
OR: conditions,
|
||||
})
|
||||
}
|
||||
return wb
|
||||
}
|
||||
|
||||
// Build 构建最终的查询条件
|
||||
// 返回格式:map[string]any 或者可以直接用于 GORM 的 Where 子句
|
||||
func (wb *WhereBuilder[R, C]) Build() interface{} {
|
||||
if len(wb.conditions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果只有一个条件组,直接返回
|
||||
if len(wb.conditions) == 1 {
|
||||
cond := wb.conditions[0]
|
||||
if len(cond.AND) == 1 && len(cond.OR) == 0 {
|
||||
return cond.AND[0]
|
||||
}
|
||||
if len(cond.OR) > 0 && len(cond.AND) == 0 {
|
||||
return wb.buildORCondition(cond.OR)
|
||||
}
|
||||
}
|
||||
|
||||
// 构建复杂的 AND/OR 混合条件
|
||||
var andConditions []interface{}
|
||||
|
||||
for _, cond := range wb.conditions {
|
||||
// 处理 AND 条件
|
||||
for _, andCond := range cond.AND {
|
||||
andConditions = append(andConditions, andCond)
|
||||
}
|
||||
|
||||
// 处理 OR 条件(将 OR 条件作为一个整体添加到 AND 中)
|
||||
if len(cond.OR) > 0 {
|
||||
orCondition := wb.buildORCondition(cond.OR)
|
||||
andConditions = append(andConditions, orCondition)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果只有一个条件,直接返回
|
||||
if len(andConditions) == 1 {
|
||||
return andConditions[0]
|
||||
}
|
||||
|
||||
// 返回 AND 条件数组
|
||||
return andConditions
|
||||
}
|
||||
|
||||
// buildORCondition 构建 OR 条件
|
||||
func (wb *WhereBuilder[R, C]) buildORCondition(orConds []interface{}) map[string]interface{} {
|
||||
if len(orConds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果只有一个 OR 条件,直接返回
|
||||
if len(orConds) == 1 {
|
||||
return map[string]interface{}{
|
||||
"OR": orConds[0],
|
||||
}
|
||||
}
|
||||
|
||||
// 多个 OR 条件
|
||||
return map[string]interface{}{
|
||||
"OR": orConds,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildWhereGORM -------------------------- 新增:GORM 原生语法构建 WHERE 条件(支持 AND/OR) --------------------------
|
||||
// BuildWhereGORM 使用 GORM 原生语法构建复杂的 AND/OR 查询条件
|
||||
// 用法示例 1 - 纯 AND 条件:
|
||||
//
|
||||
// db.Where("age > ?", 18).Where("status = ?", 1)
|
||||
//
|
||||
// 用法示例 2 - OR 条件:
|
||||
//
|
||||
// db.Where(db.Where("name = ?", "john").Or("name = ?", "jane"))
|
||||
//
|
||||
// 用法示例 3 - 混合使用:
|
||||
//
|
||||
// db.Where("status = ?", 1).
|
||||
// Where(db.Where("age >= ?", 18).Or("age < ? AND vip = ?", 18, true)).
|
||||
// Find(&users)
|
||||
func (c Crud[R, C]) BuildWhereGORM(query interface{}, args ...interface{}) *GORMWhereBuilder[R, C] {
|
||||
return &GORMWhereBuilder[R, C]{
|
||||
DB: c.Dao.DB(),
|
||||
crud: c,
|
||||
query: query,
|
||||
args: args,
|
||||
}
|
||||
}
|
||||
|
||||
// GORMWhereBuilder -------------------------- GORM 原生 WHERE 构建器 --------------------------
|
||||
// GORMWhereBuilder 使用 GORM 原生 API 构建复杂查询(R 为模型类型参数)
|
||||
type GORMWhereBuilder[R any, C any] struct {
|
||||
*gorm.DB
|
||||
crud Crud[R, C]
|
||||
query interface{}
|
||||
args []interface{}
|
||||
}
|
||||
|
||||
// Where 添加 WHERE 条件(AND 关系)
|
||||
func (gwb *GORMWhereBuilder[R, C]) Where(query interface{}, args ...interface{}) *GORMWhereBuilder[R, C] {
|
||||
// 如果当前已经有查询条件,先应用
|
||||
if gwb.query != nil {
|
||||
gwb = gwb.Where(gwb.query, gwb.args...)
|
||||
gwb.query = nil
|
||||
gwb.args = nil
|
||||
}
|
||||
return gwb.Where(query, args...)
|
||||
}
|
||||
|
||||
// Or 添加 OR 条件
|
||||
func (gwb *GORMWhereBuilder[R, C]) Or(query interface{}, args ...interface{}) *GORMWhereBuilder[R, C] {
|
||||
// 如果当前有未应用的查询条件,先应用
|
||||
if gwb.query != nil {
|
||||
gwb = gwb.Where(gwb.query, gwb.args...)
|
||||
gwb.query = nil
|
||||
gwb.args = nil
|
||||
}
|
||||
return gwb.Or(query, args...)
|
||||
}
|
||||
|
||||
// Not 添加 NOT 条件
|
||||
func (gwb *GORMWhereBuilder[R, C]) Not(query interface{}, args ...interface{}) *GORMWhereBuilder[R, C] {
|
||||
if gwb.query != nil {
|
||||
gwb = gwb.Where(gwb.query, gwb.args...)
|
||||
gwb.query = nil
|
||||
gwb.args = nil
|
||||
}
|
||||
gwb = gwb.Not(query, args...)
|
||||
return gwb
|
||||
}
|
||||
|
||||
// Find 执行查询并返回结果
|
||||
func (gwb *GORMWhereBuilder[R, C]) Find(items interface{}) error {
|
||||
// 应用剩余的查询条件
|
||||
if gwb.query != nil {
|
||||
gwb = gwb.Where(gwb.query, gwb.args...)
|
||||
}
|
||||
return gwb.Model(new(R)).Find(items).Error
|
||||
}
|
||||
|
||||
// First 查询第一条记录
|
||||
func (gwb *GORMWhereBuilder[R, C]) First(result interface{}) error {
|
||||
if gwb.query != nil {
|
||||
gwb = gwb.Where(gwb.query, gwb.args...)
|
||||
}
|
||||
return gwb.Model(new(R)).First(result).Error
|
||||
}
|
||||
|
||||
// Count 统计记录数
|
||||
func (gwb *GORMWhereBuilder[R, C]) Count(count *int64) error {
|
||||
if gwb.query != nil {
|
||||
gwb = gwb.Where(gwb.query, gwb.args...)
|
||||
}
|
||||
return gwb.Model(new(R)).Count(count).Error
|
||||
}
|
||||
|
||||
// ClearField -------------------------- 原ClearField对应实现:清理请求参数并返回有效map --------------------------
|
||||
func (c Curd[R]) ClearField(req any, delField []string, subField ...map[string]any) map[string]any {
|
||||
func (c Crud[R, C]) ClearField(req any, delField []string, subField ...map[string]any) map[string]any {
|
||||
reqMap := convToMap(req)
|
||||
resultMap := make(map[string]any)
|
||||
|
||||
|
|
@ -212,14 +423,17 @@ func (c Curd[R]) ClearField(req any, delField []string, subField ...map[string]a
|
|||
}
|
||||
|
||||
// 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) {
|
||||
func (c Crud[R, C]) ClearFieldPage(ctx ctx, req any, delField []string, where any, page *Paginate, order any, with map[string]func(db *gorm.DB) *gorm.DB) (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()
|
||||
|
||||
if with != nil {
|
||||
for k, v := range with {
|
||||
db = db.Preload(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 构建查询条件
|
||||
|
|
@ -250,12 +464,14 @@ func (c Curd[R]) ClearFieldPage(ctx ctx, req any, delField []string, where any,
|
|||
}
|
||||
|
||||
// ClearFieldList -------------------------- 原ClearFieldList对应实现:清理参数+列表查询(不分页) --------------------------
|
||||
func (c Curd[R]) ClearFieldList(ctx ctx, req any, delField []string, where any, order any, with bool) (items []*R, err error) {
|
||||
func (c Crud[R, C]) ClearFieldList(ctx ctx, req any, delField []string, where any, order any, with map[string]func(db *gorm.DB) *gorm.DB) (items []*R, err error) {
|
||||
filterMap := c.ClearField(req, delField)
|
||||
db := c.Dao.Ctx(ctx).Model(new(R))
|
||||
|
||||
if with {
|
||||
db = db.Preload("*")
|
||||
if with != nil {
|
||||
for k, v := range with {
|
||||
db = db.Preload(k, v)
|
||||
}
|
||||
}
|
||||
if where != nil {
|
||||
db = db.Where(where)
|
||||
|
|
@ -269,13 +485,15 @@ func (c Curd[R]) ClearFieldList(ctx ctx, req any, delField []string, where any,
|
|||
}
|
||||
|
||||
// ClearFieldOne -------------------------- 原ClearFieldOne对应实现:清理参数+单条查询 --------------------------
|
||||
func (c Curd[R]) ClearFieldOne(ctx ctx, req any, delField []string, where any, order any, with bool) (item *R, err error) {
|
||||
func (c Crud[R, C]) ClearFieldOne(ctx ctx, req any, delField []string, where any, order any, with map[string]func(db *gorm.DB) *gorm.DB) (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 with != nil {
|
||||
for k, v := range with {
|
||||
db = db.Preload(k, v)
|
||||
}
|
||||
}
|
||||
if where != nil {
|
||||
db = db.Where(where)
|
||||
|
|
@ -293,7 +511,7 @@ func (c Curd[R]) ClearFieldOne(ctx ctx, req any, delField []string, where any, o
|
|||
}
|
||||
|
||||
// Value -------------------------- 原Value对应实现:查询单个字段值 --------------------------
|
||||
func (c Curd[R]) Value(ctx ctx, where any, field any) (interface{}, error) {
|
||||
func (c Crud[R, C]) Value(ctx ctx, where any, field any) (interface{}, error) {
|
||||
var result interface{}
|
||||
db := c.Dao.Ctx(ctx).Model(new(R)).Where(where)
|
||||
|
||||
|
|
@ -317,7 +535,7 @@ func (c Curd[R]) Value(ctx ctx, where any, field any) (interface{}, error) {
|
|||
}
|
||||
|
||||
// DeletePri -------------------------- 原DeletePri对应实现:按主键删除 --------------------------
|
||||
func (c Curd[R]) DeletePri(ctx ctx, primaryKey any) error {
|
||||
func (c Crud[R, C]) DeletePri(ctx ctx, primaryKey any) error {
|
||||
db := c.Dao.Ctx(ctx).Model(new(R))
|
||||
// 按主键字段构建查询
|
||||
pk := c.Dao.PrimaryKey()
|
||||
|
|
@ -328,12 +546,12 @@ func (c Curd[R]) DeletePri(ctx ctx, primaryKey any) error {
|
|||
}
|
||||
|
||||
// DeleteWhere -------------------------- 原DeleteWhere对应实现:按条件删除 --------------------------
|
||||
func (c Curd[R]) DeleteWhere(ctx ctx, where any) error {
|
||||
func (c Crud[R, C]) 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 {
|
||||
func (c Crud[R, C]) Sum(ctx ctx, where any, field string) float64 {
|
||||
var sum float64
|
||||
if field == "" {
|
||||
panic("求和字段不能为空")
|
||||
|
|
@ -347,7 +565,7 @@ func (c Curd[R]) Sum(ctx ctx, where any, field string) float64 {
|
|||
}
|
||||
|
||||
// ArrayField -------------------------- 原ArrayField对应实现:查询指定字段数组 --------------------------
|
||||
func (c Curd[R]) ArrayField(ctx ctx, where any, field any) []interface{} {
|
||||
func (c Crud[R, C]) ArrayField(ctx ctx, where any, field any) []interface{} {
|
||||
var result []interface{}
|
||||
db := c.Dao.Ctx(ctx).Model(new(R)).Where(where)
|
||||
|
||||
|
|
@ -371,7 +589,7 @@ func (c Curd[R]) ArrayField(ctx ctx, where any, field any) []interface{} {
|
|||
}
|
||||
|
||||
// FindPri -------------------------- 原FindPri对应实现:按主键查询单条记录 --------------------------
|
||||
func (c Curd[R]) FindPri(ctx ctx, primaryKey any, with bool) (model *R) {
|
||||
func (c Crud[R, C]) FindPri(ctx ctx, primaryKey any, with map[string]func(db *gorm.DB) *gorm.DB) (model *R) {
|
||||
model = new(R)
|
||||
db := c.Dao.Ctx(ctx).Model(model)
|
||||
pk := c.Dao.PrimaryKey()
|
||||
|
|
@ -379,8 +597,10 @@ func (c Curd[R]) FindPri(ctx ctx, primaryKey any, with bool) (model *R) {
|
|||
if pk == "" {
|
||||
panic("主键字段未配置")
|
||||
}
|
||||
if with {
|
||||
db = db.Preload("*")
|
||||
if with != nil {
|
||||
for k, v := range with {
|
||||
db = db.Preload(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// 按主键查询
|
||||
|
|
@ -392,12 +612,14 @@ func (c Curd[R]) FindPri(ctx ctx, primaryKey any, with bool) (model *R) {
|
|||
}
|
||||
|
||||
// -------------------------- 原First对应实现:按条件查询第一条记录 --------------------------
|
||||
func (c Curd[R]) First(ctx ctx, where any, order any, with bool) (model *R) {
|
||||
func (c Crud[R, C]) First(ctx ctx, where any, order any, with map[string]func(db *gorm.DB) *gorm.DB) (model *R) {
|
||||
model = new(R)
|
||||
db := c.Dao.Ctx(ctx).Model(model)
|
||||
|
||||
if with {
|
||||
db = db.Preload("*")
|
||||
if with != nil {
|
||||
for k, v := range with {
|
||||
db = db.Preload(k, v)
|
||||
}
|
||||
}
|
||||
if where != nil {
|
||||
db = db.Where(where)
|
||||
|
|
@ -414,7 +636,7 @@ func (c Curd[R]) First(ctx ctx, where any, order any, with bool) (model *R) {
|
|||
}
|
||||
|
||||
// -------------------------- 原Exists对应实现:判断记录是否存在 --------------------------
|
||||
func (c Curd[R]) Exists(ctx ctx, where any) (exists bool) {
|
||||
func (c Crud[R, C]) 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 {
|
||||
|
|
@ -424,11 +646,13 @@ func (c Curd[R]) Exists(ctx ctx, where any) (exists bool) {
|
|||
}
|
||||
|
||||
// -------------------------- 原All对应实现:查询所有符合条件的记录 --------------------------
|
||||
func (c Curd[R]) All(ctx ctx, where any, order any, with bool) (items []*R) {
|
||||
func (c Crud[R, C]) All(ctx ctx, where any, order any, with map[string]func(db *gorm.DB) *gorm.DB) (items []*R) {
|
||||
db := c.Dao.Ctx(ctx).Model(new(R))
|
||||
|
||||
if with {
|
||||
db = db.Preload("*")
|
||||
if with != nil {
|
||||
for k, v := range with {
|
||||
db = db.Preload(k, v)
|
||||
}
|
||||
}
|
||||
if where != nil {
|
||||
db = db.Where(where)
|
||||
|
|
@ -445,7 +669,7 @@ func (c Curd[R]) All(ctx ctx, where any, order any, with bool) (items []*R) {
|
|||
}
|
||||
|
||||
// -------------------------- 原Count对应实现:统计记录总数 --------------------------
|
||||
func (c Curd[R]) Count(ctx ctx, where any) (count int64) {
|
||||
func (c Crud[R, C]) 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))
|
||||
|
|
@ -454,7 +678,7 @@ func (c Curd[R]) Count(ctx ctx, where any) (count int64) {
|
|||
}
|
||||
|
||||
// -------------------------- 原Save对应实现:新增/更新记录(对应GORM的Save) --------------------------
|
||||
func (c Curd[R]) Save(ctx ctx, data any) {
|
||||
func (c Crud[R, C]) 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))
|
||||
|
|
@ -462,7 +686,7 @@ func (c Curd[R]) Save(ctx ctx, data any) {
|
|||
}
|
||||
|
||||
// -------------------------- 原Update对应实现:按条件更新记录 --------------------------
|
||||
func (c Curd[R]) Update(ctx ctx, where any, data any) (count int64) {
|
||||
func (c Crud[R, C]) 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()))
|
||||
|
|
@ -471,7 +695,7 @@ func (c Curd[R]) Update(ctx ctx, where any, data any) (count int64) {
|
|||
}
|
||||
|
||||
// -------------------------- 原UpdatePri对应实现:按主键更新记录 --------------------------
|
||||
func (c Curd[R]) UpdatePri(ctx ctx, primaryKey any, data any) (count int64) {
|
||||
func (c Crud[R, C]) UpdatePri(ctx ctx, primaryKey any, data any) (count int64) {
|
||||
db := c.Dao.Ctx(ctx).Model(new(R))
|
||||
pk := c.Dao.PrimaryKey()
|
||||
|
||||
|
|
@ -487,7 +711,7 @@ func (c Curd[R]) UpdatePri(ctx ctx, primaryKey any, data any) (count int64) {
|
|||
}
|
||||
|
||||
// -------------------------- 原Paginate对应实现:分页查询 --------------------------
|
||||
func (c Curd[R]) Paginate(ctx context.Context, where any, p Paginate, with bool, order any) (items []*R, total int64) {
|
||||
func (c Crud[R, C]) Paginate(ctx context.Context, where any, p Paginate, with map[string]func(db *gorm.DB) *gorm.DB, order any) (items []*R, total int64) {
|
||||
db := c.Dao.Ctx(ctx).Model(new(R))
|
||||
|
||||
// 1. 构建查询条件
|
||||
|
|
@ -501,8 +725,10 @@ func (c Curd[R]) Paginate(ctx context.Context, where any, p Paginate, with bool,
|
|||
}
|
||||
|
||||
// 3. 关联查询
|
||||
if with {
|
||||
db = db.Preload("*")
|
||||
if with != nil {
|
||||
for k, v := range with {
|
||||
db = db.Preload(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 排序
|
||||
|
|
@ -0,0 +1,299 @@
|
|||
package crud
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestBuildWhere 测试 BuildWhere 方法
|
||||
func TestBuildWhere(t *testing.T) {
|
||||
// 创建一个测试结构体
|
||||
req := struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
Status int `json:"status"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}{
|
||||
Name: "Alice",
|
||||
Age: 25,
|
||||
Status: 1,
|
||||
Page: 1,
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
// 由于 Crud 需要 IDao 实现,我们只测试辅助函数
|
||||
t.Run("TestConvToMap", func(t *testing.T) {
|
||||
result := convToMap(req)
|
||||
|
||||
if result["name"] != "Alice" {
|
||||
t.Errorf("Expected name to be 'Alice', got '%v'", result["name"])
|
||||
}
|
||||
if result["age"] != 25 {
|
||||
t.Errorf("Expected age to be 25, got '%v'", result["age"])
|
||||
}
|
||||
// 分页字段应该被过滤
|
||||
})
|
||||
|
||||
t.Run("TestIsEmpty", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
value interface{}
|
||||
expected bool
|
||||
}{
|
||||
{"", true},
|
||||
{"test", false},
|
||||
{0, true},
|
||||
{1, false},
|
||||
{nil, true},
|
||||
{[]int{}, true},
|
||||
{[]int{1}, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
result := isEmpty(tt.value)
|
||||
if result != tt.expected {
|
||||
t.Errorf("isEmpty(%v) = %v, expected %v", tt.value, result, tt.expected)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TestStrInArray", func(t *testing.T) {
|
||||
arr := []string{"apple", "banana", "orange"}
|
||||
|
||||
if !strInArray(arr, "apple") {
|
||||
t.Error("Expected 'apple' to be in array")
|
||||
}
|
||||
if !strInArray(arr, "APPLE") { // 忽略大小写
|
||||
t.Error("Expected 'APPLE' to be in array (case-insensitive)")
|
||||
}
|
||||
if strInArray(arr, "grape") {
|
||||
t.Error("Expected 'grape' to not be in array")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("TestCaseConvert", func(t *testing.T) {
|
||||
// 驼峰转下划线
|
||||
result := caseConvert("userName", true)
|
||||
if result != "user_name" {
|
||||
t.Errorf("Expected 'user_name', got '%s'", result)
|
||||
}
|
||||
|
||||
result = caseConvert("FirstName", true)
|
||||
if result != "first_name" {
|
||||
t.Errorf("Expected 'first_name', got '%s'", result)
|
||||
}
|
||||
|
||||
// 下划线转小驼峰
|
||||
result = caseConvert("user_name", false)
|
||||
if result != "userName" {
|
||||
t.Errorf("Expected 'userName', got '%s'", result)
|
||||
}
|
||||
|
||||
result = caseConvert("first_name", false)
|
||||
if result != "firstName" {
|
||||
t.Errorf("Expected 'firstName', got '%s'", result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestBuildMap 测试 BuildMap 方法
|
||||
func TestBuildMap(t *testing.T) {
|
||||
// 创建一个空的 Crud 实例用于测试(不需要实际的 Dao)
|
||||
var crud Crud[interface{}, interface{}]
|
||||
|
||||
t.Run("BuildMapWithoutField", func(t *testing.T) {
|
||||
result := crud.BuildMap(">", 18)
|
||||
|
||||
if result["op"] != ">" {
|
||||
t.Errorf("Expected op to be '>', got '%v'", result["op"])
|
||||
}
|
||||
if result["value"] != 18 {
|
||||
t.Errorf("Expected value to be 18, got '%v'", result["value"])
|
||||
}
|
||||
if result["field"] != "" {
|
||||
t.Errorf("Expected field to be empty, got '%v'", result["field"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("BuildMapWithField", func(t *testing.T) {
|
||||
result := crud.BuildMap("LIKE", "%test%", "name")
|
||||
|
||||
if result["op"] != "LIKE" {
|
||||
t.Errorf("Expected op to be 'LIKE', got '%v'", result["op"])
|
||||
}
|
||||
if result["value"] != "%test%" {
|
||||
t.Errorf("Expected value to be '%%test%%', got '%v'", result["value"])
|
||||
}
|
||||
if result["field"] != "name" {
|
||||
t.Errorf("Expected field to be 'name', got '%v'", result["field"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestClearField 测试 ClearField 方法
|
||||
func TestClearField(t *testing.T) {
|
||||
var crud Crud[interface{}, interface{}]
|
||||
|
||||
t.Run("ClearFieldBasic", func(t *testing.T) {
|
||||
req := struct {
|
||||
Name string `json:"name"`
|
||||
Age int `json:"age"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}{
|
||||
Name: "Alice",
|
||||
Age: 25,
|
||||
Page: 1,
|
||||
Limit: 10,
|
||||
}
|
||||
|
||||
result := crud.ClearField(req, nil)
|
||||
|
||||
if result["name"] != "Alice" {
|
||||
t.Errorf("Expected name to be 'Alice', got '%v'", result["name"])
|
||||
}
|
||||
if result["age"] != 25 {
|
||||
t.Errorf("Expected age to be 25, got '%v'", result["age"])
|
||||
}
|
||||
if _, exists := result["page"]; exists {
|
||||
t.Error("Expected page to be removed")
|
||||
}
|
||||
if _, exists := result["limit"]; exists {
|
||||
t.Error("Expected limit to be removed")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ClearFieldWithDelFields", func(t *testing.T) {
|
||||
req := struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Status int `json:"status"`
|
||||
}{
|
||||
Name: "Alice",
|
||||
Email: "alice@example.com",
|
||||
Status: 1,
|
||||
}
|
||||
|
||||
result := crud.ClearField(req, []string{"email"})
|
||||
|
||||
if _, exists := result["email"]; exists {
|
||||
t.Error("Expected email to be removed")
|
||||
}
|
||||
if result["name"] != "Alice" {
|
||||
t.Errorf("Expected name to be 'Alice', got '%v'", result["name"])
|
||||
}
|
||||
if result["status"] != 1 {
|
||||
t.Errorf("Expected status to be 1, got '%v'", result["status"])
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ClearFieldWithSubField", func(t *testing.T) {
|
||||
req := struct {
|
||||
Name string `json:"name"`
|
||||
}{
|
||||
Name: "Alice",
|
||||
}
|
||||
|
||||
subField := map[string]interface{}{
|
||||
"vip": true,
|
||||
}
|
||||
|
||||
result := crud.ClearField(req, nil, subField)
|
||||
|
||||
if result["name"] != "Alice" {
|
||||
t.Errorf("Expected name to be 'Alice', got '%v'", result["name"])
|
||||
}
|
||||
if result["vip"] != true {
|
||||
t.Errorf("Expected vip to be true, got '%v'", result["vip"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestBuildWhereAndOr 测试 BuildWhereAndOr 方法
|
||||
func TestBuildWhereAndOr(t *testing.T) {
|
||||
var crud Crud[interface{}, interface{}]
|
||||
|
||||
t.Run("SimpleAND", func(t *testing.T) {
|
||||
where := crud.BuildWhereAndOr().
|
||||
AND(map[string]any{"status": 1}).
|
||||
AND(map[string]any{"age >": 20}).
|
||||
Build()
|
||||
|
||||
if where == nil {
|
||||
t.Error("Expected where to not be nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("SimpleOR", func(t *testing.T) {
|
||||
where := crud.BuildWhereAndOr().
|
||||
OR(
|
||||
map[string]any{"name": "Alice"},
|
||||
map[string]any{"name": "Bob"},
|
||||
).
|
||||
Build()
|
||||
|
||||
if where == nil {
|
||||
t.Error("Expected where to not be nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MixedANDOR", func(t *testing.T) {
|
||||
where := crud.BuildWhereAndOr().
|
||||
AND(map[string]any{"status": 1}).
|
||||
OR(
|
||||
map[string]any{"age >": 25},
|
||||
map[string]any{"vip": true},
|
||||
).
|
||||
AND(map[string]any{"deleted_at": 0}).
|
||||
Build()
|
||||
|
||||
if where == nil {
|
||||
t.Error("Expected where to not be nil")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyConditions", func(t *testing.T) {
|
||||
where := crud.BuildWhereAndOr().Build()
|
||||
|
||||
if where != nil {
|
||||
t.Error("Expected where to be nil for empty conditions")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestPaginateStruct 测试 Paginate 结构体
|
||||
func TestPaginateStruct(t *testing.T) {
|
||||
p := Paginate{Page: 1, Limit: 10}
|
||||
|
||||
if p.Page != 1 {
|
||||
t.Errorf("Expected Page to be 1, got %d", p.Page)
|
||||
}
|
||||
if p.Limit != 10 {
|
||||
t.Errorf("Expected Limit to be 10, got %d", p.Limit)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPageInfo 测试 pageInfo 常量
|
||||
func TestPageInfo(t *testing.T) {
|
||||
expectedFields := []string{
|
||||
"page",
|
||||
"size",
|
||||
"num",
|
||||
"limit",
|
||||
"pagesize",
|
||||
"pageSize",
|
||||
"page_size",
|
||||
"pageNum",
|
||||
"pagenum",
|
||||
"page_num",
|
||||
}
|
||||
|
||||
if len(pageInfo) != len(expectedFields) {
|
||||
t.Errorf("Expected pageInfo to have %d fields, got %d", len(expectedFields), len(pageInfo))
|
||||
}
|
||||
|
||||
for i, field := range expectedFields {
|
||||
if pageInfo[i] != field {
|
||||
t.Errorf("Expected pageInfo[%d] to be '%s', got '%s'", i, field, pageInfo[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type IdModel struct {
|
||||
Id int `json:"id" gorm:"column:id;type:int(11);common:id"`
|
||||
}
|
||||
type TimeModel struct {
|
||||
CreateTime string `json:"create_time" gorm:"column:create_time;type:varchar(255);common:创建时间"`
|
||||
UpdateTime string `json:"update_time" gorm:"column:update_time;type:varchar(255);common:更新时间"`
|
||||
}
|
||||
|
||||
func (tm *TimeModel) BeforeCreate(scope *gorm.DB) error {
|
||||
scope.Set("create_time", gtime.Datetime())
|
||||
scope.Set("update_time", gtime.Datetime())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tm *TimeModel) BeforeUpdate(scope *gorm.DB) error {
|
||||
scope.Set("update_time", gtime.Datetime())
|
||||
return nil
|
||||
}
|
||||
|
||||
//func (tm *TimeModel) AfterFind(scope *gorm.DB) error {
|
||||
// tm.CreateTime = gtime.New(tm.CreateTime).String()
|
||||
// tm.UpdateTime = gtime.New(tm.UpdateTime).String()
|
||||
// return nil
|
||||
//}
|
||||
|
|
@ -3,6 +3,7 @@ package database
|
|||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/config"
|
||||
"git.magicany.cc/black1552/gin-base/log"
|
||||
|
|
@ -11,6 +12,7 @@ import (
|
|||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
|
|
@ -24,24 +26,39 @@ var (
|
|||
|
||||
func init() {
|
||||
if g.IsEmpty(dns) {
|
||||
log.Error("gormDns未配置", "请检查配置文件")
|
||||
log.Error("gormDns 未配置", "请检查配置文件")
|
||||
return
|
||||
}
|
||||
switch config.GetConfigValue("database.type", "sqlite").String() {
|
||||
case "mysql":
|
||||
log.Info("使用mysql数据库")
|
||||
log.Info("使用 mysql 数据库")
|
||||
mysqlInit()
|
||||
case "sqlite":
|
||||
log.Info("使用sqlite数据库")
|
||||
log.Info("使用 sqlite 数据库")
|
||||
sqliteInit()
|
||||
}
|
||||
Db, err = gorm.Open(Type, &gorm.Config{
|
||||
|
||||
// 构建 GORM 配置
|
||||
gormConfig := &gorm.Config{
|
||||
SkipDefaultTransaction: true,
|
||||
NowFunc: func() time.Time {
|
||||
return time.Now().Local()
|
||||
},
|
||||
// 命名策略:保持与模型一致,避免字段/表名转换问题
|
||||
NamingStrategy: schema.NamingStrategy{
|
||||
SingularTable: true, // 表名禁用复数形式(例如 User 对应 user 表,而非 users)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 根据配置决定是否开启 GORM 查询日志
|
||||
if config.GetConfigValue("database.debug", false).Bool() {
|
||||
log.Info("已开启 GORM 查询日志")
|
||||
gormConfig.Logger = logger.Default.LogMode(logger.Info)
|
||||
} else {
|
||||
gormConfig.Logger = logger.Default.LogMode(logger.Silent)
|
||||
}
|
||||
|
||||
Db, err = gorm.Open(Type, gormConfig)
|
||||
if err != nil {
|
||||
log.Error("数据库连接失败: ", err)
|
||||
return
|
||||
|
|
|
|||
95
go.mod
95
go.mod
|
|
@ -1,78 +1,95 @@
|
|||
module git.magicany.cc/black1552/gin-base
|
||||
|
||||
go 1.25
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
git.magicany.cc/black1552/gf-common v1.0.1017
|
||||
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.12.0
|
||||
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/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/spf13/viper v1.21.0
|
||||
golang.org/x/crypto v0.49.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.10.0 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.21.2 // indirect
|
||||
github.com/fatih/color v1.19.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // 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/golang/protobuf v1.3.5 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/google/uuid v1.6.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/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.21 // 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/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect
|
||||
github.com/olekukonko/errors v1.2.0 // indirect
|
||||
github.com/olekukonko/ll v0.1.8 // indirect
|
||||
github.com/olekukonko/tablewriter v1.1.4 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // 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/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.42.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // 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/text v0.33.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
golang.org/x/arch v0.25.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.43.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // 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
|
||||
modernc.org/libc v1.70.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.48.0 // indirect
|
||||
)
|
||||
|
|
|
|||
255
go.sum
255
go.sum
|
|
@ -1,9 +1,25 @@
|
|||
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/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
git.magicany.cc/black1552/gf-common v1.0.1017 h1:KP0e32CSOzIYg8Nfqj7zwGakO6Co9HYTuiDZupK7LsU=
|
||||
git.magicany.cc/black1552/gf-common v1.0.1017/go.mod h1:ln6bd5oXxPNsktr8xI3itmsqpVBn1j+4W7iaS0g7S0Q=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/clipperhouse/displaywidth v0.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g=
|
||||
github.com/clipperhouse/displaywidth v0.10.0/go.mod h1:XqJajYsaiEwkxOj4bowCTMcT1SgvHo9flfF3jQasdbs=
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0 h1:z0cDbUV+aPASdFb2/ndFnS9ts/WNXgTNNGFoKXuhpos=
|
||||
github.com/clipperhouse/uax29/v2 v2.6.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
@ -13,18 +29,20 @@ github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2I
|
|||
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/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w=
|
||||
github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
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-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
|
||||
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/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
|
||||
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=
|
||||
|
|
@ -32,88 +50,90 @@ 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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
|
||||
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/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
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/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
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/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.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/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
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/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
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.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
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/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
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-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
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-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.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
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 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/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
|
||||
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=
|
||||
github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=
|
||||
github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo=
|
||||
github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.1.8 h1:ysHCJRGHYKzmBSdz9w5AySztx7lG8SQY+naTGYUbsz8=
|
||||
github.com/olekukonko/ll v0.1.8/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw=
|
||||
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
|
||||
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0/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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
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/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/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
|
|
@ -125,65 +145,64 @@ github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjb
|
|||
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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
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.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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
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.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.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/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
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/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/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
|
||||
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
|
||||
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
|
||||
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
|
||||
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
|
||||
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
|
||||
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
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-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.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
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-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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
|
||||
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
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/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
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/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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
@ -191,11 +210,31 @@ 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=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
|
||||
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
|
||||
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.48.0 h1:ElZyLop3Q2mHYk5IFPPXADejZrlHu7APbpB0sF78bq4=
|
||||
modernc.org/sqlite v1.48.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
package log
|
||||
174
log/log.go
174
log/log.go
|
|
@ -5,8 +5,11 @@ import (
|
|||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
|
|
@ -14,39 +17,184 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
logPath string
|
||||
sysLog *log.Logger
|
||||
logPath string
|
||||
sysLog *log.Logger
|
||||
filePath string
|
||||
currentDate string // 当前日志文件对应的日期
|
||||
fileLogger *lumberjack.Logger
|
||||
)
|
||||
|
||||
func init() {
|
||||
logPath = gfile.Join(gfile.Pwd(), "log")
|
||||
filePath := gfile.Join(logPath, fmt.Sprintf("log-%s.log", gtime.Date()))
|
||||
fileLogger := &lumberjack.Logger{
|
||||
const (
|
||||
Reset = "\033[0m"
|
||||
Red = "\033[31m"
|
||||
Green = "\033[32m"
|
||||
Yellow = "\033[33m"
|
||||
Blue = "\033[34m"
|
||||
Purple = "\033[35m"
|
||||
Cyan = "\033[36m"
|
||||
)
|
||||
|
||||
// 正则表达式匹配 ANSI 颜色码
|
||||
var ansiColorRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||
|
||||
// stripAnsiColors 去除字符串中的 ANSI 颜色码
|
||||
func stripAnsiColors(s string) string {
|
||||
return ansiColorRegex.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
// logWriter 自定义 writer,用于分别处理控制台和文件输出
|
||||
type logWriter struct {
|
||||
console io.Writer
|
||||
file io.Writer
|
||||
}
|
||||
|
||||
func (w *logWriter) Write(p []byte) (n int, err error) {
|
||||
// 控制台输出保留颜色
|
||||
_, err = w.console.Write(p)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// 文件输出去除颜色码
|
||||
colorless := stripAnsiColors(string(p))
|
||||
_, err = w.file.Write([]byte(colorless))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// cleanOldLogs 删除指定天数之前的日志文件(包括主文件和备份文件)
|
||||
func cleanOldLogs(days int) {
|
||||
if !gfile.Exists(logPath) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取所有日志文件
|
||||
files, err := gfile.DirNames(logPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, file := range files {
|
||||
path := filepath.Join(logPath, file)
|
||||
if gfile.IsDir(path) {
|
||||
continue
|
||||
}
|
||||
|
||||
var dateStr string
|
||||
var matched bool
|
||||
|
||||
// 匹配主日志文件格式:log-YYYY-MM-DD.log
|
||||
if strings.HasPrefix(file, "log-") && strings.HasSuffix(file, ".log") {
|
||||
// 检查是否是主文件(没有备份时间戳)
|
||||
// 主文件格式:log-2026-04-25.log
|
||||
// 备份文件格式:log-2026-04-25-2026-04-25T10-30-45.123.log
|
||||
parts := strings.Split(strings.TrimSuffix(file, ".log"), "-")
|
||||
if len(parts) == 4 {
|
||||
// 主文件:log-YYYY-MM-DD
|
||||
dateStr = parts[1] + "-" + parts[2] + "-" + parts[3]
|
||||
matched = true
|
||||
} else if len(parts) > 4 {
|
||||
// 备份文件:log-YYYY-MM-DD-YYYY-MM-DDTHH-MM-SS.mmm
|
||||
// 提取主日期部分(第一个日期)
|
||||
dateStr = parts[1] + "-" + parts[2] + "-" + parts[3]
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
|
||||
if !matched {
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析日期
|
||||
fileTime, err := time.Parse("2006-01-02", dateStr)
|
||||
if err != nil {
|
||||
continue // 日期格式不正确,跳过
|
||||
}
|
||||
|
||||
// 计算文件年龄
|
||||
tage := now.Sub(fileTime)
|
||||
if tage.Hours() > float64(days*24) {
|
||||
// 超过指定天数,删除文件
|
||||
err = os.Remove(path)
|
||||
if err == nil {
|
||||
Info(fmt.Sprintf("已删除过期日志文件:%s", file))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkAndRotateLogFile 检查是否需要切换日志文件(跨天时)
|
||||
func checkAndRotateLogFile() {
|
||||
date := gtime.Date()
|
||||
if currentDate != date {
|
||||
// 日期变化,需要重新初始化
|
||||
currentDate = date
|
||||
filePath = gfile.Join(logPath, fmt.Sprintf("log-%s.log", currentDate))
|
||||
fileLogger = &lumberjack.Logger{
|
||||
Filename: filePath,
|
||||
MaxSize: 2, // 单个文件最大 10MB
|
||||
MaxBackups: 5, // 最多保留 5 个备份
|
||||
MaxAge: 30, // 保留 30 天
|
||||
Compress: false, // 启用压缩
|
||||
}
|
||||
// 创建新的 writer
|
||||
multiWriter := &logWriter{
|
||||
console: os.Stdout,
|
||||
file: fileLogger,
|
||||
}
|
||||
sysLog = log.New(multiWriter, "", 0)
|
||||
|
||||
// 清理 30 天前的旧日志
|
||||
cleanOldLogs(30)
|
||||
}
|
||||
}
|
||||
|
||||
func Init() {
|
||||
if sysLog != nil {
|
||||
checkAndRotateLogFile() // 检查是否需要切换文件
|
||||
return
|
||||
}
|
||||
logPath = gfile.Join(gfile.Pwd(), "logs")
|
||||
currentDate = gtime.Date()
|
||||
filePath = gfile.Join(logPath, fmt.Sprintf("log-%s.log", currentDate))
|
||||
fileLogger = &lumberjack.Logger{
|
||||
Filename: filePath,
|
||||
MaxSize: 2, // 单个文件最大 10MB
|
||||
MaxBackups: 5, // 最多保留 5 个备份
|
||||
MaxAge: 30, // 保留 30 天
|
||||
Compress: false, // 启用压缩
|
||||
}
|
||||
// 创建 MultiWriter 实现同时输出到文件和终端
|
||||
multiWriter := io.MultiWriter(fileLogger, os.Stdout)
|
||||
sysLog = log.New(multiWriter, "", log.LstdFlags)
|
||||
// 使用自定义 writer 实现控制台带颜色、文件无颜色的输出
|
||||
multiWriter := &logWriter{
|
||||
console: os.Stdout,
|
||||
file: fileLogger,
|
||||
}
|
||||
sysLog = log.New(multiWriter, "", 0)
|
||||
|
||||
// 启动时清理 30 天前的旧日志
|
||||
cleanOldLogs(30)
|
||||
}
|
||||
|
||||
func Info(v ...any) {
|
||||
sysLog.SetPrefix("[INFO] ")
|
||||
Init()
|
||||
sysLog.SetPrefix(fmt.Sprintf("[%s] %s[INFO]%s ", time.Now().Format("2006-01-02 15:04:05"), Green, Reset))
|
||||
sysLog.Println(fmt.Sprint(v...))
|
||||
}
|
||||
func Error(v ...any) {
|
||||
sysLog.SetPrefix("[ERROR] ")
|
||||
Init()
|
||||
sysLog.SetPrefix(fmt.Sprintf("[%s] %s[ERROR]%s ", time.Now().Format("2006-01-02 15:04:05"), Red, Reset))
|
||||
msg := fmt.Sprint(v...)
|
||||
sysLog.Println(msg, strings.TrimSpace(string(debug.Stack())))
|
||||
}
|
||||
func Warn(v ...any) {
|
||||
sysLog.SetPrefix("[WARN] ")
|
||||
Init()
|
||||
sysLog.SetPrefix(fmt.Sprintf("[%s] %s[WARN]%s ", time.Now().Format("2006-01-02 15:04:05"), Yellow, Reset))
|
||||
sysLog.Println(fmt.Sprint(v...))
|
||||
}
|
||||
func Debug(v ...any) {
|
||||
sysLog.SetPrefix("[DEBUG] ")
|
||||
Init()
|
||||
sysLog.SetPrefix(fmt.Sprintf("[%s] %s[DEBUG]%s ", time.Now().Format("2006-01-02 15:04:05"), Blue, Reset))
|
||||
sysLog.Println(fmt.Sprint(v...))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package pool
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ConnType 连接类型
|
||||
type ConnType string
|
||||
|
||||
const (
|
||||
ConnTypeWebSocket ConnType = "websocket"
|
||||
ConnTypeTCP ConnType = "tcp"
|
||||
)
|
||||
|
||||
// ConnectionInfo 连接信息
|
||||
type ConnectionInfo struct {
|
||||
ID string `json:"id" gorm:"primaryKey"`
|
||||
Type ConnType `json:"type" gorm:"index"`
|
||||
Address string `json:"address"`
|
||||
IsActive bool `json:"isActive" gorm:"index"`
|
||||
LastUsed time.Time `json:"lastUsed"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
// 额外的连接数据,根据不同类型存储不同的信息
|
||||
Data map[string]interface{} `json:"data" gorm:"-"`
|
||||
}
|
||||
|
|
@ -0,0 +1,247 @@
|
|||
package pool
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.magicany.cc/black1552/gf-common/db"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// SQLitePool SQLite连接池
|
||||
type SQLitePool struct {
|
||||
db *gorm.DB
|
||||
mutex sync.RWMutex
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
// 内存缓存,提高并发性能
|
||||
cache map[string]*ConnectionInfo
|
||||
}
|
||||
|
||||
// NewSQLitePool 创建SQLite连接池
|
||||
func NewSQLitePool() (*SQLitePool, error) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// 检查数据库连接是否正常
|
||||
if db.Db == nil {
|
||||
return nil, fmt.Errorf("database connection is not initialized")
|
||||
}
|
||||
|
||||
// 自动迁移ConnectionInfo模型
|
||||
err := db.Db.AutoMigrate(&ConnectionInfo{})
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, fmt.Errorf("failed to migrate connection info model: %w", err)
|
||||
}
|
||||
|
||||
return &SQLitePool{
|
||||
db: db.Db,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
cache: make(map[string]*ConnectionInfo),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close 关闭连接池
|
||||
func (p *SQLitePool) Close() error {
|
||||
p.cancel()
|
||||
// SQLite连接由db包管理,不需要在这里关闭
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add 添加连接
|
||||
func (p *SQLitePool) Add(conn *ConnectionInfo) error {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
// 存储到SQLite
|
||||
result := p.db.Create(conn)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to store connection: %w", result.Error)
|
||||
}
|
||||
|
||||
// 更新内存缓存
|
||||
p.cache[conn.ID] = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get 获取连接
|
||||
func (p *SQLitePool) Get(connID string) (*ConnectionInfo, error) {
|
||||
p.mutex.RLock()
|
||||
// 先从内存缓存获取
|
||||
if conn, ok := p.cache[connID]; ok {
|
||||
p.mutex.RUnlock()
|
||||
return conn, nil
|
||||
}
|
||||
p.mutex.RUnlock()
|
||||
|
||||
// 从SQLite获取
|
||||
var connInfo ConnectionInfo
|
||||
result := p.db.First(&connInfo, "id = ?", connID)
|
||||
if result.Error != nil {
|
||||
if result.Error == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get connection: %w", result.Error)
|
||||
}
|
||||
|
||||
// 更新内存缓存
|
||||
p.mutex.Lock()
|
||||
p.cache[connID] = &connInfo
|
||||
p.mutex.Unlock()
|
||||
|
||||
return &connInfo, nil
|
||||
}
|
||||
|
||||
// Remove 移除连接
|
||||
func (p *SQLitePool) Remove(connID string) error {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
// 从SQLite删除
|
||||
result := p.db.Delete(&ConnectionInfo{}, "id = ?", connID)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to remove connection: %w", result.Error)
|
||||
}
|
||||
|
||||
// 从内存缓存删除
|
||||
delete(p.cache, connID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update 更新连接信息
|
||||
func (p *SQLitePool) Update(conn *ConnectionInfo) error {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
// 存储到SQLite
|
||||
result := p.db.Save(conn)
|
||||
if result.Error != nil {
|
||||
return fmt.Errorf("failed to update connection: %w", result.Error)
|
||||
}
|
||||
|
||||
// 更新内存缓存
|
||||
p.cache[conn.ID] = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAll 获取所有连接
|
||||
func (p *SQLitePool) GetAll() ([]*ConnectionInfo, error) {
|
||||
p.mutex.RLock()
|
||||
// 如果内存缓存不为空,直接返回缓存
|
||||
if len(p.cache) > 0 {
|
||||
conns := make([]*ConnectionInfo, 0, len(p.cache))
|
||||
for _, conn := range p.cache {
|
||||
conns = append(conns, conn)
|
||||
}
|
||||
p.mutex.RUnlock()
|
||||
return conns, nil
|
||||
}
|
||||
p.mutex.RUnlock()
|
||||
|
||||
// 从SQLite获取所有连接
|
||||
var conns []*ConnectionInfo
|
||||
result := p.db.Find(&conns)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("failed to get all connections: %w", result.Error)
|
||||
}
|
||||
|
||||
// 更新内存缓存
|
||||
p.mutex.Lock()
|
||||
for _, conn := range conns {
|
||||
p.cache[conn.ID] = conn
|
||||
}
|
||||
p.mutex.Unlock()
|
||||
|
||||
return conns, nil
|
||||
}
|
||||
|
||||
// GetByType 根据类型获取连接
|
||||
func (p *SQLitePool) GetByType(connType ConnType) ([]*ConnectionInfo, error) {
|
||||
allConns, err := p.GetAll()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var filtered []*ConnectionInfo
|
||||
for _, conn := range allConns {
|
||||
if conn.Type == connType {
|
||||
filtered = append(filtered, conn)
|
||||
}
|
||||
}
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
// Count 获取连接数量
|
||||
func (p *SQLitePool) Count() (int, error) {
|
||||
p.mutex.RLock()
|
||||
// 如果内存缓存不为空,直接返回缓存大小
|
||||
if len(p.cache) > 0 {
|
||||
count := len(p.cache)
|
||||
p.mutex.RUnlock()
|
||||
return count, nil
|
||||
}
|
||||
p.mutex.RUnlock()
|
||||
|
||||
// 从SQLite统计数量
|
||||
var count int64
|
||||
result := p.db.Model(&ConnectionInfo{}).Count(&count)
|
||||
if result.Error != nil {
|
||||
return 0, fmt.Errorf("failed to count connections: %w", result.Error)
|
||||
}
|
||||
|
||||
return int(count), nil
|
||||
}
|
||||
|
||||
// GetAllConnIDs 获取所有在线连接的ID列表
|
||||
func (p *SQLitePool) GetAllConnIDs() ([]string, error) {
|
||||
p.mutex.RLock()
|
||||
// 如果内存缓存不为空,从缓存中提取在线连接的ID
|
||||
if len(p.cache) > 0 {
|
||||
ids := make([]string, 0, len(p.cache))
|
||||
for id, conn := range p.cache {
|
||||
if conn.IsActive {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
}
|
||||
p.mutex.RUnlock()
|
||||
return ids, nil
|
||||
}
|
||||
p.mutex.RUnlock()
|
||||
|
||||
// 从SQLite获取所有在线连接的ID
|
||||
var conns []*ConnectionInfo
|
||||
result := p.db.Where("is_active = ?", true).Find(&conns)
|
||||
if result.Error != nil {
|
||||
return nil, fmt.Errorf("failed to get all connection IDs: %w", result.Error)
|
||||
}
|
||||
|
||||
ids := make([]string, 0, len(conns))
|
||||
for _, conn := range conns {
|
||||
ids = append(ids, conn.ID)
|
||||
}
|
||||
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// CleanupInactive 清理不活跃的连接
|
||||
func (p *SQLitePool) CleanupInactive(duration time.Duration) error {
|
||||
allConns, err := p.GetAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
for _, conn := range allConns {
|
||||
if !conn.IsActive || now.Sub(conn.LastUsed) > duration {
|
||||
if err := p.Remove(conn.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
174
response/code.go
174
response/code.go
|
|
@ -6,78 +6,158 @@ import (
|
|||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// response 是 API 响应的数据结构体
|
||||
type response struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data any `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Code int `json:"code"` // 状态码
|
||||
Msg string `json:"msg"` // 响应消息
|
||||
Data any `json:"data"` // 响应数据
|
||||
Timestamp int64 `json:"timestamp"` // 响应时间戳
|
||||
}
|
||||
|
||||
// api 返回结构体
|
||||
type api struct {
|
||||
JSON *response
|
||||
c *gin.Context
|
||||
// Api 返回结构体
|
||||
// Api 是 Gin 上下文的响应包装器
|
||||
type Api struct {
|
||||
JSON *response // JSON 响应数据
|
||||
c *gin.Context // Gin 上下文对象
|
||||
}
|
||||
|
||||
// Success 函数用于创建一个包含成功响应的 api 对象。
|
||||
// 参数 c 是 Gin 框架的上下文对象,用于处理请求和响应。
|
||||
// 返回值是一个指向 API 结构体的指针,其中包含 JSON 响应数据。
|
||||
func Success(c *gin.Context) *api {
|
||||
// 创建响应结构体,包含状态码和时间戳
|
||||
json := &response{
|
||||
Code: 200, // 成功的状态码
|
||||
Timestamp: time.Now().Unix(), // 当前时间的时间戳
|
||||
}
|
||||
// 创建 api 结构体实例,并将响应数据和上下文对象赋值给它
|
||||
r := &api{
|
||||
JSON: json, // JSON 响应数据
|
||||
c: c, // Gin 上下文对象
|
||||
}
|
||||
return r // 返回 api 对象的指针
|
||||
// resFile 是文件响应的数据结构体
|
||||
type resFile struct {
|
||||
Code int `json:"code"` // 状态码
|
||||
ContentType string `json:"content_type"` // 内容类型
|
||||
Data []byte `json:"data"` // 文件数据
|
||||
}
|
||||
|
||||
// Error 函数用于创建一个包含错误响应的 api 对象。
|
||||
// 参数 c 是 Gin 框架的上下文对象,用于处理请求和响应。
|
||||
// 返回值是一个指向 api 结构体的指针,其中包含 JSON 响应数据。
|
||||
func Error(c *gin.Context) *api {
|
||||
json := &response{
|
||||
Code: 200,
|
||||
Timestamp: time.Now().Unix(),
|
||||
// ApiFile 是 Gin 上下文的文件响应包装器
|
||||
type ApiFile struct {
|
||||
JSON *resFile // 文件响应数据
|
||||
c *gin.Context // Gin 上下文对象
|
||||
}
|
||||
|
||||
// SuccessFile 创建一个包含成功响应的 ApiFile 对象
|
||||
// 参数 c 是 Gin 框架的上下文对象
|
||||
// 返回值是指向 ApiFile 结构体的指针
|
||||
func SuccessFile(c *gin.Context) *ApiFile {
|
||||
json := &resFile{
|
||||
Code: 200,
|
||||
}
|
||||
r := &api{
|
||||
r := &ApiFile{
|
||||
JSON: json,
|
||||
c: c,
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// SetMsg 方法用于设置 api 对象的 JSON 响应数据中的 Msg 字段。
|
||||
// 参数 msg 是要设置的消息字符串。
|
||||
// 返回值是指向 api 结构体的指针,用于链式调用。
|
||||
func (r *api) SetMsg(msg string) *api {
|
||||
// SetContentType 设置文件响应的内容类型
|
||||
// 参数 contentType 是要设置的 MIME 类型字符串
|
||||
// 返回值是指向 ApiFile 结构体的指针,用于链式调用
|
||||
func (af *ApiFile) SetContentType(contentType string) *ApiFile {
|
||||
af.JSON.ContentType = contentType
|
||||
return af
|
||||
}
|
||||
|
||||
func (af *ApiFile) SetPng(data []byte) *ApiFile {
|
||||
af.SetContentType("image/png")
|
||||
af.SetData(data)
|
||||
return af
|
||||
}
|
||||
|
||||
func (af *ApiFile) SetJpeg(data []byte) *ApiFile {
|
||||
af.SetContentType("image/jpeg")
|
||||
af.SetData(data)
|
||||
return af
|
||||
}
|
||||
|
||||
func (af *ApiFile) SetGif(data []byte) *ApiFile {
|
||||
af.SetContentType("image/gif")
|
||||
af.SetData(data)
|
||||
return af
|
||||
}
|
||||
|
||||
func (af *ApiFile) SetPdf(data []byte) *ApiFile {
|
||||
af.SetContentType("application/pdf")
|
||||
af.SetData(data)
|
||||
return af
|
||||
}
|
||||
|
||||
func (af *ApiFile) SetXls(data []byte) *ApiFile {
|
||||
af.SetContentType("application/vnd.ms-excel")
|
||||
af.SetData(data)
|
||||
return af
|
||||
}
|
||||
|
||||
// SetData 设置文件响应的数据
|
||||
// 参数 data 是要设置的字节数据
|
||||
// 返回值是指向 ApiFile 结构体的指针,用于链式调用
|
||||
func (af *ApiFile) SetData(data []byte) *ApiFile {
|
||||
af.JSON.Data = data
|
||||
return af
|
||||
}
|
||||
|
||||
// End 将文件响应数据发送给客户端
|
||||
// 使用 Gin 上下文对象的 Data 方法发送原始数据
|
||||
func (af *ApiFile) End() {
|
||||
af.c.Data(af.JSON.Code, af.JSON.ContentType, af.JSON.Data)
|
||||
}
|
||||
|
||||
// Success 函数用于创建一个包含成功响应的 api 对象。
|
||||
// 参数 c 是 Gin 框架的上下文对象,用于处理请求和响应。
|
||||
// 返回值是一个指向 API 结构体的指针,其中包含 JSON 响应数据。
|
||||
func Success(c *gin.Context) *Api {
|
||||
// 创建响应结构体,包含状态码和时间戳
|
||||
json := &response{
|
||||
Code: 200, // 成功的状态码
|
||||
Timestamp: time.Now().Unix(), // 当前时间的时间戳
|
||||
}
|
||||
// 创建 api 结构体实例,并将响应数据和上下文对象赋值给它
|
||||
r := &Api{
|
||||
JSON: json, // JSON 响应数据
|
||||
c: c, // Gin 上下文对象
|
||||
}
|
||||
return r // 返回 api 对象的指针
|
||||
}
|
||||
|
||||
// Error 创建一个包含错误响应的 Api 对象
|
||||
// 参数 c 是 Gin 框架的上下文对象
|
||||
// 返回值是指向 Api 结构体的指针
|
||||
func Error(c *gin.Context) *Api {
|
||||
json := &response{
|
||||
Code: 200,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
r := &Api{
|
||||
JSON: json,
|
||||
c: c,
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// SetMsg 设置响应的消息
|
||||
// 参数 msg 是要设置的消息字符串
|
||||
// 返回值是指向 Api 结构体的指针,用于链式调用
|
||||
func (r *Api) SetMsg(msg string) *Api {
|
||||
r.JSON.Msg = msg
|
||||
return r
|
||||
}
|
||||
|
||||
// SetData 方法用于设置 api 对象的 JSON 响应数据中的 Data 字段。
|
||||
// 参数 data 是要设置的数据,类型为 any(任意类型)。
|
||||
// 返回值是指向 api 结构体的指针,用于链式调用。
|
||||
func (r *api) SetData(data any) *api {
|
||||
// SetData 设置响应的数据
|
||||
// 参数 data 是要设置的数据,类型为 any(任意类型)
|
||||
// 返回值是指向 Api 结构体的指针,用于链式调用
|
||||
func (r *Api) SetData(data any) *Api {
|
||||
r.JSON.Data = data
|
||||
return r
|
||||
}
|
||||
|
||||
// SetCode 方法用于设置 api 对象的 JSON 响应数据中的 Code 字段。
|
||||
// 参数 code 是要设置的状态码,类型为 int。
|
||||
// 返回值是指向 api 结构体的指针,用于链式调用。
|
||||
func (r *api) SetCode(code int) *api {
|
||||
// SetCode 设置响应的状态码
|
||||
// 参数 code 是要设置的状态码
|
||||
// 返回值是指向 Api 结构体的指针,用于链式调用
|
||||
func (r *Api) SetCode(code int) *Api {
|
||||
r.JSON.Code = code
|
||||
return r
|
||||
}
|
||||
|
||||
// End 方法用于将 api 对象的 JSON 响应数据发送给客户端。
|
||||
// 它使用 Gin 上下文对象的 JSON 方法将 JSON 响应数据发送给客户端。
|
||||
// 参数 r 是指向 api 结构体的指针,包含 JSON 响应数据和 Gin 上下文对象。
|
||||
func (r *api) End() {
|
||||
// End 将 JSON 响应数据发送给客户端
|
||||
// 使用 Gin 上下文对象的 JSON 方法发送响应
|
||||
func (r *Api) End() {
|
||||
r.c.JSON(r.JSON.Code, r.JSON)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/log"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// BuildRequest 创建 HTTP 反向代理 Handler
|
||||
// 支持 HTTP 和 WebSocket 协议
|
||||
// @Param host 目标服务器地址,例如 "http://127.0.0.1:8081"
|
||||
func BuildRequest(host string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
targetURL, err := url.Parse(host)
|
||||
if err != nil {
|
||||
log.Error("parse target host error:", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "invalid target host"})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建反向代理
|
||||
proxy := httputil.NewSingleHostReverseProxy(targetURL)
|
||||
|
||||
// 自定义 Director 修改请求,保留原始路径和查询参数
|
||||
originalDirector := proxy.Director
|
||||
proxy.Director = func(req *http.Request) {
|
||||
originalDirector(req)
|
||||
// 保留原始请求的路径和查询参数
|
||||
req.URL.Path = c.Request.URL.Path
|
||||
req.URL.RawQuery = c.Request.URL.RawQuery
|
||||
req.Host = targetURL.Host
|
||||
}
|
||||
|
||||
// 处理代理错误
|
||||
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
|
||||
log.Error("proxy error:", err)
|
||||
http.Error(w, "proxy error: "+err.Error(), http.StatusBadGateway)
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(c.Writer, c.Request)
|
||||
}
|
||||
}
|
||||
|
|
@ -33,8 +33,9 @@ func New() *gin.Engine {
|
|||
// @Param *gin.Engine 路由实例
|
||||
// 设置监听挂壁
|
||||
func Run(g *gin.Engine) {
|
||||
addr := config.GetConfigValue("server.addr", ":8080").String()
|
||||
s := &http.Server{
|
||||
Addr: config.GetConfigValue("server.addr", ":8080").String(),
|
||||
Addr: addr,
|
||||
Handler: g,
|
||||
ReadTimeout: 60 * time.Second,
|
||||
ReadHeaderTimeout: 60 * time.Second,
|
||||
|
|
@ -44,15 +45,24 @@ func Run(g *gin.Engine) {
|
|||
}
|
||||
go func() {
|
||||
if err := s.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Error("服务器启动失败:", err)
|
||||
// 检查是否是端口被占用的错误
|
||||
if strings.Contains(err.Error(), "address already in use") ||
|
||||
strings.Contains(err.Error(), "Only one usage of each socket address") {
|
||||
log.Error(fmt.Sprintf("服务器启动失败:%s 端口已被占用,请检查是否有其他程序正在使用该端口", addr))
|
||||
} else {
|
||||
log.Error("服务器启动失败:", err)
|
||||
}
|
||||
os.Exit(1) // 启动失败则退出程序
|
||||
}
|
||||
}()
|
||||
time.Sleep(time.Second)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,11 @@ func Example() {
|
|||
}
|
||||
|
||||
// 创建TCP服务器
|
||||
server := NewTCPServer("0.0.0.0:8888", config)
|
||||
server, err := NewTCPServer("0.0.0.0:8888", config)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create server: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 设置消息处理函数
|
||||
server.SetMessageHandler(func(conn *TcpConnection, msg *TcpMessage) error {
|
||||
|
|
@ -45,3 +49,48 @@ func Example() {
|
|||
|
||||
fmt.Println("TCP server stopped.")
|
||||
}
|
||||
|
||||
// TestTCP 测试TCP连接
|
||||
func TestTCP() {
|
||||
fmt.Println("=== 测试TCP连接 ===")
|
||||
fmt.Println("1. 创建TCP服务器配置")
|
||||
config := &TcpPoolConfig{
|
||||
BufferSize: 2048,
|
||||
MaxConnections: 100000,
|
||||
ConnectTimeout: time.Second * 5,
|
||||
ReadTimeout: time.Second * 30,
|
||||
WriteTimeout: time.Second * 10,
|
||||
MaxIdleTime: time.Minute * 5,
|
||||
}
|
||||
fmt.Println("2. 创建TCP服务器")
|
||||
server, err := NewTCPServer("0.0.0.0:8888", config)
|
||||
if err != nil {
|
||||
fmt.Printf("创建服务器失败:%v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("3. 服务器创建成功")
|
||||
fmt.Println("4. 获取在线连接数")
|
||||
count := server.Connection.Count()
|
||||
fmt.Printf("当前在线连接数:%d\n", count)
|
||||
fmt.Println("5. 获取所有在线连接ID")
|
||||
connIDs, err := server.GetAllConnIDs()
|
||||
if err != nil {
|
||||
fmt.Printf("获取在线连接ID失败:%v\n", err)
|
||||
} else {
|
||||
fmt.Printf("在线连接ID:%v\n", connIDs)
|
||||
}
|
||||
fmt.Println("6. 启动服务器")
|
||||
if err := server.Start(); err != nil {
|
||||
fmt.Printf("启动服务器失败:%v\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Println("7. 服务器启动成功,运行2秒后停止")
|
||||
time.Sleep(time.Second * 2)
|
||||
fmt.Println("8. 停止服务器")
|
||||
if err := server.Stop(); err != nil {
|
||||
fmt.Printf("停止服务器失败:%v\n", err)
|
||||
} else {
|
||||
fmt.Println("服务器停止成功")
|
||||
}
|
||||
fmt.Println("=== TCP测试完成 ===")
|
||||
}
|
||||
|
|
|
|||
167
tcp/tcp.go
167
tcp/tcp.go
|
|
@ -2,10 +2,12 @@ package tcp
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/pool"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/gtcp"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
|
|
@ -32,18 +34,26 @@ type TCPServer struct {
|
|||
// ConnectionPool 连接池结构
|
||||
type ConnectionPool struct {
|
||||
connections map[string]*TcpConnection
|
||||
sqlitePool *pool.SQLitePool
|
||||
mutex sync.RWMutex
|
||||
config *TcpPoolConfig
|
||||
logger *glog.Logger
|
||||
}
|
||||
|
||||
// NewTCPServer 创建一个新的TCP服务器
|
||||
func NewTCPServer(address string, config *TcpPoolConfig) *TCPServer {
|
||||
func NewTCPServer(address string, config *TcpPoolConfig) (*TCPServer, error) {
|
||||
logger := g.Log(address)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
pool := &ConnectionPool{
|
||||
// 初始化SQLite连接池
|
||||
sqlitePool, err := pool.NewSQLitePool()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create sqlite connPool: %w", err)
|
||||
}
|
||||
|
||||
connPool := &ConnectionPool{
|
||||
connections: make(map[string]*TcpConnection),
|
||||
sqlitePool: sqlitePool,
|
||||
config: config,
|
||||
logger: logger,
|
||||
}
|
||||
|
|
@ -51,14 +61,14 @@ func NewTCPServer(address string, config *TcpPoolConfig) *TCPServer {
|
|||
server := &TCPServer{
|
||||
Address: address,
|
||||
Config: config,
|
||||
Connection: pool,
|
||||
Connection: connPool,
|
||||
Logger: logger,
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
server.Listener = gtcp.NewServer(address, server.handleConnection)
|
||||
return server
|
||||
return server, nil
|
||||
}
|
||||
|
||||
// SetMessageHandler 设置消息处理函数
|
||||
|
|
@ -86,6 +96,11 @@ func (s *TCPServer) Stop() error {
|
|||
s.Listener.Close()
|
||||
s.wg.Wait()
|
||||
s.Connection.Clear()
|
||||
// 关闭SQLite连接池
|
||||
if err := s.Connection.sqlitePool.Close(); err != nil {
|
||||
s.Logger.Error(s.ctx, fmt.Sprintf("Failed to close SQLite pool: %v", err))
|
||||
// 不影响服务器停止,仅记录错误
|
||||
}
|
||||
s.Logger.Info(s.ctx, "TCP server stopped")
|
||||
return nil
|
||||
}
|
||||
|
|
@ -109,6 +124,23 @@ func (s *TCPServer) handleConnection(conn *gtcp.Conn) {
|
|||
s.Connection.Add(tcpConn)
|
||||
s.Logger.Info(s.ctx, fmt.Sprintf("New connection established: %s", connID))
|
||||
|
||||
// 存储到SQLite
|
||||
connInfo := &pool.ConnectionInfo{
|
||||
ID: connID,
|
||||
Type: pool.ConnTypeTCP,
|
||||
Address: conn.RemoteAddr().String(),
|
||||
IsActive: true,
|
||||
LastUsed: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
Data: map[string]interface{}{
|
||||
"localAddress": conn.LocalAddr().String(),
|
||||
},
|
||||
}
|
||||
if err := s.Connection.sqlitePool.Add(connInfo); err != nil {
|
||||
s.Logger.Error(s.ctx, fmt.Sprintf("Failed to store connection to SQLite: %v", err))
|
||||
// 不影响连接建立,仅记录错误
|
||||
}
|
||||
|
||||
// 启动消息接收协程
|
||||
go s.receiveMessages(tcpConn)
|
||||
}
|
||||
|
|
@ -121,6 +153,11 @@ func (s *TCPServer) receiveMessages(conn *TcpConnection) {
|
|||
}
|
||||
s.Connection.Remove(conn.Id)
|
||||
conn.Server.Close()
|
||||
// 从SQLite移除
|
||||
if err := s.Connection.sqlitePool.Remove(conn.Id); err != nil {
|
||||
s.Logger.Error(s.ctx, fmt.Sprintf("Failed to remove connection from SQLite: %v", err))
|
||||
// 不影响连接关闭,仅记录错误
|
||||
}
|
||||
s.Logger.Info(s.ctx, fmt.Sprintf("Connection closed: %s", conn.Id))
|
||||
}()
|
||||
|
||||
|
|
@ -142,37 +179,79 @@ func (s *TCPServer) receiveMessages(conn *TcpConnection) {
|
|||
|
||||
if n > 0 {
|
||||
// 更新最后使用时间
|
||||
now := time.Now()
|
||||
conn.Mutex.Lock()
|
||||
conn.LastUsed = time.Now()
|
||||
conn.LastUsed = now
|
||||
// 将读取的数据添加到连接的缓冲区
|
||||
conn.buffer = append(conn.buffer, buffer[:n]...)
|
||||
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,
|
||||
// 更新SQLite中的连接信息
|
||||
connInfo, err := s.Connection.sqlitePool.Get(conn.Id)
|
||||
if err == nil && connInfo != nil {
|
||||
connInfo.LastUsed = now
|
||||
if err := s.Connection.sqlitePool.Update(connInfo); err != nil {
|
||||
s.Logger.Error(s.ctx, fmt.Sprintf("Failed to update connection in SQLite: %v", err))
|
||||
// 不影响消息处理,仅记录错误
|
||||
}
|
||||
}
|
||||
|
||||
// 使用协程池处理消息,避免阻塞
|
||||
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))
|
||||
})
|
||||
// 解析消息帧
|
||||
s.parseMessageFrames(conn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// parseMessageFrames 解析消息帧
|
||||
func (s *TCPServer) parseMessageFrames(conn *TcpConnection) {
|
||||
conn.Mutex.Lock()
|
||||
defer conn.Mutex.Unlock()
|
||||
|
||||
for {
|
||||
// 检查缓冲区是否有足够的数据来读取长度前缀
|
||||
if len(conn.buffer) < messageLengthPrefixSize {
|
||||
// 数据不足,等待下一次读取
|
||||
return
|
||||
}
|
||||
|
||||
// 读取长度前缀
|
||||
length := binary.BigEndian.Uint32(conn.buffer[:messageLengthPrefixSize])
|
||||
|
||||
// 检查缓冲区是否有足够的数据来读取完整的消息
|
||||
if len(conn.buffer) < messageLengthPrefixSize+int(length) {
|
||||
// 数据不足,等待下一次读取
|
||||
return
|
||||
}
|
||||
|
||||
// 提取消息数据
|
||||
data := conn.buffer[messageLengthPrefixSize : messageLengthPrefixSize+int(length)]
|
||||
|
||||
// 移除已处理的消息数据
|
||||
conn.buffer = conn.buffer[messageLengthPrefixSize+int(length):]
|
||||
|
||||
// 创建消息对象
|
||||
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)
|
||||
|
|
@ -202,14 +281,33 @@ func (s *TCPServer) sendMessage(conn *TcpConnection, data []byte) error {
|
|||
// 设置写入超时
|
||||
conn.Server.SetWriteDeadline(time.Now().Add(s.Config.WriteTimeout))
|
||||
|
||||
// 创建消息帧:4字节长度前缀 + 消息数据
|
||||
frame := make([]byte, messageLengthPrefixSize+len(data))
|
||||
// 写入长度前缀(大端序)
|
||||
binary.BigEndian.PutUint32(frame[:messageLengthPrefixSize], uint32(len(data)))
|
||||
// 写入消息数据
|
||||
copy(frame[messageLengthPrefixSize:], data)
|
||||
|
||||
// 发送数据
|
||||
_, err := conn.Server.Write(data)
|
||||
_, err := conn.Server.Write(frame)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新最后使用时间
|
||||
conn.LastUsed = time.Now()
|
||||
now := time.Now()
|
||||
conn.LastUsed = now
|
||||
|
||||
// 更新SQLite中的连接信息
|
||||
connInfo, err := s.Connection.sqlitePool.Get(conn.Id)
|
||||
if err == nil && connInfo != nil {
|
||||
connInfo.LastUsed = now
|
||||
if err := s.Connection.sqlitePool.Update(connInfo); err != nil {
|
||||
s.Logger.Error(s.ctx, fmt.Sprintf("Failed to update connection in SQLite: %v", err))
|
||||
// 不影响消息发送,仅记录错误
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -224,11 +322,21 @@ func (s *TCPServer) Kick(connID string) error {
|
|||
conn.Server.Close()
|
||||
// 从连接池移除
|
||||
s.Connection.Remove(connID)
|
||||
// 从SQLite移除
|
||||
if err := s.Connection.sqlitePool.Remove(connID); err != nil {
|
||||
s.Logger.Error(s.ctx, fmt.Sprintf("Failed to remove connection from SQLite: %v", err))
|
||||
// 不影响连接关闭,仅记录错误
|
||||
}
|
||||
|
||||
s.Logger.Info(s.ctx, fmt.Sprintf("Kicked connection: %s", connID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllConnIDs 获取所有在线连接的ID列表
|
||||
func (s *TCPServer) GetAllConnIDs() ([]string, error) {
|
||||
return s.Connection.GetAllConnIDs()
|
||||
}
|
||||
|
||||
// Add 添加连接到连接池
|
||||
func (p *ConnectionPool) Add(conn *TcpConnection) {
|
||||
p.mutex.Lock()
|
||||
|
|
@ -278,3 +386,8 @@ func (p *ConnectionPool) Count() int {
|
|||
defer p.mutex.RUnlock()
|
||||
return len(p.connections)
|
||||
}
|
||||
|
||||
// GetAllConnIDs 获取所有在线连接的ID列表
|
||||
func (p *ConnectionPool) GetAllConnIDs() ([]string, error) {
|
||||
return p.sqlitePool.GetAllConnIDs()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ import (
|
|||
"github.com/gogf/gf/v2/net/gtcp"
|
||||
)
|
||||
|
||||
// 消息帧格式:4字节长度前缀 + 消息数据
|
||||
const (
|
||||
messageLengthPrefixSize = 4 // 消息长度前缀大小(4字节)
|
||||
)
|
||||
|
||||
// TcpPoolConfig TCP连接池配置
|
||||
type TcpPoolConfig struct {
|
||||
BufferSize int `json:"bufferSize"` // 缓冲区大小
|
||||
|
|
@ -26,6 +31,7 @@ type TcpConnection struct {
|
|||
LastUsed time.Time `json:"lastUsed"` // 最后使用时间
|
||||
CreatedAt time.Time `json:"createdAt"` // 创建时间
|
||||
Mutex sync.RWMutex `json:"-"` // 读写锁
|
||||
buffer []byte `json:"-"` // 用于存储未处理的字节数据
|
||||
}
|
||||
|
||||
// TcpMessage TCP消息结构
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/crypto/gmd5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
hashPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("加密失败:%v", err)
|
||||
}
|
||||
return string(hashPassword), nil
|
||||
}
|
||||
|
||||
func ValidPassword(hashPassword, password string) error {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hashPassword), []byte(password))
|
||||
if err != nil {
|
||||
return fmt.Errorf("密码不匹配:%v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Md5Password(password string) string {
|
||||
md5pass, err := gmd5.EncryptString(password)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return md5pass
|
||||
}
|
||||
|
|
@ -1,7 +1,13 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/log"
|
||||
"github.com/gogf/gf/v2/container/garray"
|
||||
"github.com/gogf/gf/v2/os/gfile"
|
||||
"github.com/gogf/gf/v2/text/gstr"
|
||||
)
|
||||
|
||||
func FileExists(path string) bool {
|
||||
|
|
@ -11,3 +17,38 @@ func FileExists(path string) bool {
|
|||
func EmptyFile(path string) bool {
|
||||
return gfile.IsEmpty(path)
|
||||
}
|
||||
|
||||
func GetResourceAllPath(path string) []string {
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
basePath, _ := os.Getwd()
|
||||
filePath := filepath.Join(basePath, "resource")
|
||||
if path != "/" {
|
||||
filePath += filepath.Join(filePath, path)
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
log.Error("open file error", err)
|
||||
panic(err)
|
||||
}
|
||||
paths, err := f.Readdirnames(-1)
|
||||
_ = f.Close()
|
||||
if err != nil {
|
||||
log.Error("read file error", err)
|
||||
panic(err)
|
||||
}
|
||||
pathArr := garray.NewStrArray()
|
||||
for _, v := range paths {
|
||||
if gstr.Contains(v, ".") {
|
||||
if path != "/" {
|
||||
pathArr.Append("/static/" + path + "/" + v)
|
||||
} else {
|
||||
pathArr.Append("/static/" + v)
|
||||
}
|
||||
} else {
|
||||
pathArr.Append(v)
|
||||
}
|
||||
}
|
||||
return pathArr.Slice()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package utils
|
||||
|
||||
import "time"
|
||||
|
||||
// NowTime 获取当前时间字符串
|
||||
func NowTime() string {
|
||||
return time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
// NowTimeUnix 获取当前时间戳
|
||||
func NowTimeUnix() int64 {
|
||||
return time.Now().Unix()
|
||||
}
|
||||
|
||||
// NowTimeUnixMilli 获取当前时间戳毫秒
|
||||
func NowTimeUnixMilli() int64 {
|
||||
return time.Now().UnixMilli()
|
||||
}
|
||||
|
||||
// NowTimeUnixNano 获取当前时间戳纳秒
|
||||
func NowTimeUnixNano() int64 {
|
||||
return time.Now().UnixNano()
|
||||
}
|
||||
|
||||
func AddTimeToString(duration time.Duration) string {
|
||||
return time.Now().Add(duration).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func SubTimeToString(duration time.Duration) string {
|
||||
return time.Now().Add(-duration).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
func AddTimeToUnix(duration time.Duration) int64 {
|
||||
return time.Now().Add(duration).Unix()
|
||||
}
|
||||
|
||||
func SubTimeToUnix(duration time.Duration) int64 {
|
||||
return time.Now().Add(-duration).Unix()
|
||||
}
|
||||
|
||||
func AddTimeToUnixMilli(duration time.Duration) int64 {
|
||||
return time.Now().Add(duration).UnixMilli()
|
||||
}
|
||||
|
||||
func SubTimeToUnixMilli(duration time.Duration) int64 {
|
||||
return time.Now().Add(-duration).UnixMilli()
|
||||
}
|
||||
|
||||
func AddTimeToUnixNano(duration time.Duration) int64 {
|
||||
return time.Now().Add(duration).UnixNano()
|
||||
}
|
||||
|
||||
func SubTimeToUnixNano(duration time.Duration) int64 {
|
||||
return time.Now().Add(-duration).UnixNano()
|
||||
}
|
||||
104
valid/valid.go
104
valid/valid.go
|
|
@ -15,20 +15,8 @@ import (
|
|||
"github.com/gogf/gf/v2/util/gconv"
|
||||
)
|
||||
|
||||
// ValidToStruct 验证参数并返回结构体
|
||||
func ValidToStruct[T any](c *gin.Context) (object *T) {
|
||||
obj := new(T)
|
||||
if err := c.ShouldBind(obj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := g.Validator().Data(obj).Run(c); err != nil {
|
||||
panic(gerror.Current(err).Error())
|
||||
}
|
||||
return obj
|
||||
}
|
||||
|
||||
// CustomBind 自定义参数绑定,不使用Gin的ShouldBind
|
||||
func CustomBind[T any](c *gin.Context) (*T, error) {
|
||||
func CustomBind[T any](c *gin.Context) *T {
|
||||
obj := new(T)
|
||||
|
||||
// 获取请求方法
|
||||
|
|
@ -43,90 +31,56 @@ func CustomBind[T any](c *gin.Context) (*T, error) {
|
|||
contentType := c.GetHeader("Content-Type")
|
||||
if strings.Contains(contentType, "application/json") {
|
||||
// JSON格式请求体
|
||||
if err := bindFromJSON(c, obj); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bindFromJSON(c, obj)
|
||||
} else if strings.Contains(contentType, "application/x-www-form-urlencoded") ||
|
||||
strings.Contains(contentType, "multipart/form-data") {
|
||||
// 表单格式请求体
|
||||
if err := c.Request.ParseForm(); err != nil {
|
||||
return nil, fmt.Errorf("解析表单数据失败: %w", err)
|
||||
panic(err)
|
||||
}
|
||||
params = c.Request.PostForm
|
||||
} else {
|
||||
// 默认尝试解析为表单
|
||||
if err := c.Request.ParseForm(); err != nil {
|
||||
return nil, fmt.Errorf("解析请求数据失败: %w", err)
|
||||
panic(err)
|
||||
}
|
||||
params = c.Request.PostForm
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的请求方法: %s", method)
|
||||
panic(fmt.Sprintf("不支持的请求方法: %s", method))
|
||||
}
|
||||
|
||||
// 如果不是JSON请求,则从params绑定
|
||||
if params != nil {
|
||||
if err := bindFromParams(obj, params); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bindFromParams(c, obj, params)
|
||||
}
|
||||
|
||||
// 验证参数
|
||||
if err := g.Validator().Data(obj).Run(c); err != nil {
|
||||
return nil, gerror.Current(err)
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
// ValidToMap 验证参数并返回结构体
|
||||
func ValidToMap[T any](c *gin.Context) (object map[string]any) {
|
||||
obj := new(T)
|
||||
if err := c.ShouldBind(obj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := g.Validator().Data(obj).Run(c); err != nil {
|
||||
panic(gerror.Current(err).Error())
|
||||
}
|
||||
return gconv.Map(obj)
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
// CustomBindToMap 自定义参数绑定到map
|
||||
func CustomBindToMap[T any](c *gin.Context) (map[string]any, error) {
|
||||
obj, err := CustomBind[T](c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gconv.Map(obj), nil
|
||||
}
|
||||
|
||||
// ValidToStructAndMap 验证参数并返回map
|
||||
func ValidToStructAndMap[T any](c *gin.Context) (stru *T, object map[string]any) {
|
||||
obj := new(T)
|
||||
if err := c.ShouldBind(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)
|
||||
func CustomBindToMap[T any](c *gin.Context) map[string]any {
|
||||
obj := CustomBind[T](c)
|
||||
return gconv.Map(obj)
|
||||
}
|
||||
|
||||
// CustomBindStructAndMap 自定义参数绑定并返回结构体和map
|
||||
func CustomBindStructAndMap[T any](c *gin.Context) (*T, map[string]any, error) {
|
||||
obj, err := CustomBind[T](c)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return obj, gconv.Map(obj), nil
|
||||
func CustomBindStructAndMap[T any](c *gin.Context) (*T, map[string]any) {
|
||||
obj := CustomBind[T](c)
|
||||
return obj, gconv.Map(obj)
|
||||
}
|
||||
|
||||
// bindFromJSON 从JSON请求体绑定参数
|
||||
func bindFromJSON[T any](c *gin.Context, obj *T) error {
|
||||
func bindFromJSON[T any](c *gin.Context, obj *T) {
|
||||
// 读取请求体
|
||||
body, err := io.ReadAll(c.Request.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("读取请求体失败: %w", err)
|
||||
panic(fmt.Sprintf("JSON解析失败:%v", err))
|
||||
}
|
||||
|
||||
// 恢复请求体,以便后续使用
|
||||
|
|
@ -134,22 +88,20 @@ func bindFromJSON[T any](c *gin.Context, obj *T) error {
|
|||
|
||||
// 解析JSON
|
||||
if err := json.Unmarshal(body, obj); err != nil {
|
||||
return fmt.Errorf("JSON解析失败: %w", err)
|
||||
panic(fmt.Sprintf("JSON解析失败:%v", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// bindFromParams 从参数映射绑定到结构体
|
||||
func bindFromParams(obj any, params map[string][]string) error {
|
||||
func bindFromParams(c *gin.Context, obj any, params map[string][]string) {
|
||||
objValue := reflect.ValueOf(obj)
|
||||
if objValue.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf("目标必须是指针类型")
|
||||
panic("目标必须为指针")
|
||||
}
|
||||
|
||||
objValue = objValue.Elem()
|
||||
if objValue.Kind() != reflect.Struct {
|
||||
return fmt.Errorf("目标必须是指向结构体的指针")
|
||||
panic("目标必须指向结构体")
|
||||
}
|
||||
|
||||
objType := objValue.Type()
|
||||
|
|
@ -192,11 +144,9 @@ func bindFromParams(obj any, params map[string][]string) error {
|
|||
|
||||
// 根据字段类型进行转换
|
||||
if err := setFieldValue(fieldValue, paramValue); err != nil {
|
||||
return fmt.Errorf("设置字段 %s 失败: %w", paramName, err)
|
||||
panic(fmt.Sprintf("参数 %s 转换失败: %v", paramName, err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setFieldValue 设置字段值
|
||||
|
|
@ -207,25 +157,25 @@ func setFieldValue(field reflect.Value, value string) error {
|
|||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
intValue, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
panic(err)
|
||||
}
|
||||
field.SetInt(intValue)
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
uintValue, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
panic(err)
|
||||
}
|
||||
field.SetUint(uintValue)
|
||||
case reflect.Float32, reflect.Float64:
|
||||
floatValue, err := strconv.ParseFloat(value, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
panic(err)
|
||||
}
|
||||
field.SetFloat(floatValue)
|
||||
case reflect.Bool:
|
||||
boolValue, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return err
|
||||
panic(err)
|
||||
}
|
||||
field.SetBool(boolValue)
|
||||
case reflect.Slice:
|
||||
|
|
@ -235,11 +185,11 @@ func setFieldValue(field reflect.Value, value string) error {
|
|||
// 创建元素并设置值
|
||||
element := newValue.Index(0)
|
||||
if err := setFieldValue(element, value); err != nil {
|
||||
return err
|
||||
panic(err)
|
||||
}
|
||||
field.Set(newValue)
|
||||
default:
|
||||
return fmt.Errorf("不支持的字段类型: %v", field.Kind())
|
||||
panic(fmt.Sprintf("不支持的字段类型: %v", field.Kind()))
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -19,7 +19,10 @@ func NewWs() *Manager {
|
|||
}
|
||||
|
||||
// 2. 创建管理器
|
||||
m := NewManager(customConfig)
|
||||
m, err := NewManager(customConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create manager: %v", err)
|
||||
}
|
||||
|
||||
// 3. 覆盖业务回调(核心:自定义消息处理逻辑)
|
||||
// 连接建立回调
|
||||
|
|
@ -71,3 +74,35 @@ func main() {
|
|||
log.Println("WebSocket服务启动:http://localhost:8080/ws")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
// TestWebSocket 测试WebSocket连接
|
||||
func TestWebSocket() {
|
||||
log.Println("=== 测试WebSocket连接 ===")
|
||||
log.Println("1. 创建WebSocket管理器")
|
||||
m, err := NewManager(DefaultConfig())
|
||||
if err != nil {
|
||||
log.Fatalf("创建管理器失败:%v", err)
|
||||
}
|
||||
log.Println("2. 管理器创建成功")
|
||||
log.Println("3. 获取在线连接数")
|
||||
count, err := m.sqlitePool.Count()
|
||||
if err != nil {
|
||||
log.Printf("获取在线连接数失败:%v", err)
|
||||
} else {
|
||||
log.Printf("当前在线连接数:%d", count)
|
||||
}
|
||||
log.Println("4. 获取所有在线连接ID")
|
||||
connIDs, err := m.GetAllConnIDs()
|
||||
if err != nil {
|
||||
log.Printf("获取在线连接ID失败:%v", err)
|
||||
} else {
|
||||
log.Printf("在线连接ID:%v", connIDs)
|
||||
}
|
||||
log.Println("5. 关闭管理器")
|
||||
if err := m.Close(); err != nil {
|
||||
log.Printf("关闭管理器失败:%v", err)
|
||||
} else {
|
||||
log.Println("管理器关闭成功")
|
||||
}
|
||||
log.Println("=== WebSocket测试完成 ===")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"git.magicany.cc/black1552/gin-base/pool"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
|
|
@ -20,20 +21,20 @@ import (
|
|||
|
||||
// 常量定义:默认配置
|
||||
const (
|
||||
// DefaultReadBufferSize 默认读写缓冲区大小(字节)
|
||||
// 默认读写缓冲区大小(字节)
|
||||
DefaultReadBufferSize = 1024
|
||||
DefaultWriteBufferSize = 1024
|
||||
// DefaultHeartbeatInterval 默认心跳间隔(秒):每30秒发送一次心跳
|
||||
// 默认心跳间隔(秒):每30秒发送一次心跳
|
||||
DefaultHeartbeatInterval = 30 * time.Second
|
||||
// DefaultHeartbeatTimeout 默认心跳超时(秒):60秒未收到客户端心跳响应则关闭连接
|
||||
// 默认心跳超时(秒):60秒未收到客户端心跳响应则关闭连接
|
||||
DefaultHeartbeatTimeout = 60 * time.Second
|
||||
// DefaultReadTimeout 默认读写超时(秒)
|
||||
// 默认读写超时(秒)
|
||||
DefaultReadTimeout = 60 * time.Second
|
||||
DefaultWriteTimeout = 10 * time.Second
|
||||
// MessageTypeText 消息类型
|
||||
// 消息类型
|
||||
MessageTypeText = websocket.TextMessage
|
||||
MessageTypeBinary = websocket.BinaryMessage
|
||||
// HeartbeatMaxRetry 心跳最大重试次数
|
||||
// 心跳最大重试次数
|
||||
HeartbeatMaxRetry = 3
|
||||
)
|
||||
|
||||
|
|
@ -92,7 +93,8 @@ type Connection struct {
|
|||
type Manager struct {
|
||||
config *Config // 配置
|
||||
upgrader *websocket.Upgrader // HTTP升级器
|
||||
connections map[string]*Connection // 所有在线连接(connID -> Connection)
|
||||
connections map[string]*Connection // 内存中的连接(connID -> Connection)
|
||||
sqlitePool *pool.SQLitePool // SQLite连接池
|
||||
mutex sync.RWMutex // 读写锁(保护connections)
|
||||
// 业务回调:收到消息时触发(用户自定义处理逻辑)
|
||||
OnMessage func(connID string, msgType int, data any)
|
||||
|
|
@ -148,16 +150,16 @@ func (c *Config) Merge(other *Config) *Config {
|
|||
}
|
||||
|
||||
// NewManager 创建连接管理器
|
||||
func NewManager(config *Config) *Manager {
|
||||
func NewManager(config *Config) (*Manager, error) {
|
||||
defaultConfig := DefaultConfig()
|
||||
finalConfig := defaultConfig.Merge(config)
|
||||
// 初始化升级器
|
||||
upgrader := &websocket.Upgrader{
|
||||
ReadBufferSize: config.ReadBufferSize,
|
||||
WriteBufferSize: config.WriteBufferSize,
|
||||
ReadBufferSize: finalConfig.ReadBufferSize,
|
||||
WriteBufferSize: finalConfig.WriteBufferSize,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
// 跨域检查
|
||||
if config.AllowAllOrigins {
|
||||
if finalConfig.AllowAllOrigins {
|
||||
return true
|
||||
}
|
||||
origin := r.Header.Get("Origin")
|
||||
|
|
@ -170,10 +172,17 @@ func NewManager(config *Config) *Manager {
|
|||
},
|
||||
}
|
||||
|
||||
// 初始化SQLite连接池
|
||||
sqlitePool, err := pool.NewSQLitePool()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create sqlite pool: %w", err)
|
||||
}
|
||||
|
||||
return &Manager{
|
||||
config: finalConfig,
|
||||
upgrader: upgrader,
|
||||
connections: make(map[string]*Connection),
|
||||
sqlitePool: sqlitePool,
|
||||
mutex: sync.RWMutex{},
|
||||
// 默认回调(用户可覆盖)
|
||||
OnMessage: func(connID string, msgType int, data any) {
|
||||
|
|
@ -185,7 +194,7 @@ func NewManager(config *Config) *Manager {
|
|||
OnDisconnect: func(connID string, err error) {
|
||||
log.Printf("[默认回调] 连接[%s]已关闭:%v", connID, err)
|
||||
},
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Upgrade HTTP升级为WebSocket连接
|
||||
|
|
@ -237,6 +246,24 @@ func (m *Manager) Upgrade(w http.ResponseWriter, r *http.Request, connID string)
|
|||
m.connections[connID] = wsConn
|
||||
m.mutex.Unlock()
|
||||
|
||||
// 存储到SQLite
|
||||
connInfo := &pool.ConnectionInfo{
|
||||
ID: connID,
|
||||
Type: pool.ConnTypeWebSocket,
|
||||
Address: r.RemoteAddr,
|
||||
IsActive: true,
|
||||
LastUsed: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
Data: map[string]interface{}{
|
||||
"origin": r.Header.Get("Origin"),
|
||||
"userAgent": r.Header.Get("User-Agent"),
|
||||
},
|
||||
}
|
||||
if err := m.sqlitePool.Add(connInfo); err != nil {
|
||||
log.Printf("[错误] 存储连接到SQLite失败:%v", err)
|
||||
// 不影响连接建立,仅记录错误
|
||||
}
|
||||
|
||||
// 触发连接建立回调
|
||||
m.OnConnect(connID)
|
||||
|
||||
|
|
@ -282,6 +309,18 @@ func (c *Connection) ReadPump() {
|
|||
return
|
||||
}
|
||||
|
||||
// 更新最后使用时间
|
||||
now := time.Now()
|
||||
// 从SQLite获取连接信息并更新
|
||||
connInfo, err := c.manager.sqlitePool.Get(c.connID)
|
||||
if err == nil && connInfo != nil {
|
||||
connInfo.LastUsed = now
|
||||
if err := c.manager.sqlitePool.Update(connInfo); err != nil {
|
||||
log.Printf("[错误] 更新SQLite连接信息失败:%v", err)
|
||||
// 不影响消息处理,仅记录错误
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试解析JSON格式的心跳消息(精准判断,替代包含判断)
|
||||
isHeartbeat := false
|
||||
// 先尝试解析为JSON对象
|
||||
|
|
@ -369,6 +408,19 @@ func (c *Connection) Send(data []byte) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("发送消息失败:%w", err)
|
||||
}
|
||||
|
||||
// 更新最后使用时间
|
||||
now := time.Now()
|
||||
// 从SQLite获取连接信息并更新
|
||||
connInfo, err := c.manager.sqlitePool.Get(c.connID)
|
||||
if err == nil && connInfo != nil {
|
||||
connInfo.LastUsed = now
|
||||
if err := c.manager.sqlitePool.Update(connInfo); err != nil {
|
||||
log.Printf("[错误] 更新SQLite连接信息失败:%v", err)
|
||||
// 不影响消息发送,仅记录错误
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -394,6 +446,12 @@ func (c *Connection) Close(err error) {
|
|||
delete(c.manager.connections, c.connID)
|
||||
c.manager.mutex.Unlock()
|
||||
|
||||
// 从SQLite移除
|
||||
if err := c.manager.sqlitePool.Remove(c.connID); err != nil {
|
||||
log.Printf("[错误] 从SQLite移除连接失败:%v", err)
|
||||
// 不影响连接关闭,仅记录错误
|
||||
}
|
||||
|
||||
// 触发断开回调
|
||||
c.manager.OnDisconnect(c.connID, err)
|
||||
|
||||
|
|
@ -462,12 +520,18 @@ func (m *Manager) GetAllConn() map[string]*Connection {
|
|||
return connCopy
|
||||
}
|
||||
|
||||
// GetConn 获取指定连接
|
||||
func (m *Manager) GetConn(connID string) *Connection {
|
||||
m.mutex.RLock()
|
||||
defer m.mutex.RUnlock()
|
||||
return m.connections[connID]
|
||||
}
|
||||
|
||||
// GetAllConnIDs 获取所有在线连接的ID列表
|
||||
func (m *Manager) GetAllConnIDs() ([]string, error) {
|
||||
return m.sqlitePool.GetAllConnIDs()
|
||||
}
|
||||
|
||||
// CloseAll 关闭所有连接
|
||||
func (m *Manager) CloseAll() {
|
||||
m.mutex.RLock()
|
||||
|
|
@ -486,3 +550,14 @@ func (m *Manager) CloseAll() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭管理器,清理资源
|
||||
func (m *Manager) Close() error {
|
||||
// 关闭所有连接
|
||||
m.CloseAll()
|
||||
// 关闭SQLite连接池
|
||||
if m.sqlitePool != nil {
|
||||
return m.sqlitePool.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue