From 87098a9180df4637af9a1114cdc1feebb972f60c Mon Sep 17 00:00:00 2001
From: hoteas <925970985@qq.com>
Date: Sat, 14 Mar 2026 13:06:32 +0800
Subject: [PATCH] =?UTF-8?q?refactor(db):=20=E5=A2=9E=E5=BC=BA=E6=B5=8B?=
=?UTF-8?q?=E8=AF=95=E4=BA=8B=E5=8A=A1=E5=92=8C=E7=BC=93=E5=AD=98=E7=AE=A1?=
=?UTF-8?q?=E7=90=86?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 在 HoTimeDB 中新增 testMu 互斥锁,确保 testTx 的串行访问,解决并发冲突问题
- 在 BeginTestTx 中初始化 testMu,确保测试事务的安全性
- 在 HoTimeCache 中新增 DisableDbCache 方法,测试模式下禁用 DB 和 Redis 缓存,避免锁等待超时
- 更新 Select 方法,确保在 testTx 激活时跳过缓存逻辑,提升测试稳定性
- 优化 Swagger 生成逻辑,支持模块化输出和导航页生成
- 移除冗余的调试日志代码,提升代码整洁性
---
.../api测试框架完整实现_ce7fcbde.plan.md | 131 ++++++++++++++++--
cache/cache.go | 121 +---------------
db/crud.go | 7 +-
db/db.go | 4 +-
db/query.go | 17 ++-
db/transaction.go | 7 +
session.go | 43 ------
testing_helper.go | 4 +
testing_swagger.go | 125 +++++++++++++----
9 files changed, 254 insertions(+), 205 deletions(-)
diff --git a/.cursor/plans/api测试框架完整实现_ce7fcbde.plan.md b/.cursor/plans/api测试框架完整实现_ce7fcbde.plan.md
index 3d57502..d977787 100644
--- a/.cursor/plans/api测试框架完整实现_ce7fcbde.plan.md
+++ b/.cursor/plans/api测试框架完整实现_ce7fcbde.plan.md
@@ -83,7 +83,86 @@ func (that *HoTimeDB) Action(action func(db HoTimeDB) (isSuccess bool)) (isSucce
}
```
-**改动量:** 3 个文件,约 50 行,生产代码零影响(testTx 默认 nil)。
+### 3.1 testTx 并发保护(调试发现的关键问题)
+
+**问题现象:** 测试运行时出现 `[mysql] busy buffer`、`bad connection` 错误,导致测试挂起。
+
+**根因分析:** `testTx` 是单个 `*sql.Tx` 连接,MySQL 驱动不允许在同一连接上并发执行查询。业务代码中存在 `go func()` 启动的 goroutine(如 `syncGoodsToYst`、`stats.go` 中的统计协程),这些 goroutine 会并发访问 `testTx`,导致驱动层的 `busy buffer` 错误。
+
+**解决方案:** 在 `HoTimeDB` 中新增 `testMu *sync.Mutex`,在 `BeginTestTx()` 时初始化。所有通过 `testTx` 执行的 Query、Exec、SAVEPOINT 操作都通过 `testMu` 加锁保护,确保串行化访问。
+
+```go
+// db/db.go - HoTimeDB 新增字段
+testMu *sync.Mutex // 保护 testTx 单连接不被并发访问
+
+// db/db.go - BeginTestTx 中初始化
+that.testMu = &sync.Mutex{}
+
+// db/query.go - Query 中加锁
+if that.testTx != nil {
+ if that.testMu != nil { that.testMu.Lock() }
+ resl, err = that.testTx.Query(query, processedArgs...)
+ // ... 消费 result set ...
+ if that.testMu != nil { that.testMu.Unlock() }
+}
+
+// db/query.go - Exec 中加锁
+if that.testTx != nil {
+ if that.testMu != nil { that.testMu.Lock() }
+ resl, e = that.testTx.Exec(query, processedArgs...)
+ if that.testMu != nil { that.testMu.Unlock() }
+}
+
+// db/transaction.go - SAVEPOINT 操作加锁
+if that.testMu != nil { that.testMu.Lock() }
+_, _ = that.testTx.Exec("SAVEPOINT " + spName)
+if that.testMu != nil { that.testMu.Unlock() }
+```
+
+**注意事项:**
+- 生产环境不受影响(`testMu` 默认 nil,所有锁检查都有 nil 保护)
+- 互斥锁确保 goroutine 中的 DB 操作排队执行而非并发冲突
+- Query 锁的范围包括结果集消费(`that.Row(resl)`),防止未读完数据就发起新查询
+
+**改动量:** 3 个文件,约 50 行核心改动 + mutex 保护约 30 行,生产代码零影响(testTx/testMu 默认 nil)。
+
+### 3.2 测试模式下禁用 DB/Redis 缓存(调试发现的关键问题)
+
+**问题现象:** 测试运行时出现 `Error 1205: Lock wait timeout exceeded` 错误,指向 `DELETE FROM cached` 操作。
+
+**根因分析:** 框架的 `HoTimeCache` 支持三级缓存(memory > redis > db)。在测试模式下,DB 缓存的读写操作(如 `DELETE FROM cached`)会尝试使用独立连接,而 `testTx` 持有事务锁,导致 `Lock wait timeout`。即使走 `testTx`,缓存的并发读写也与事务隔离产生冲突。
+
+**解决方案:**
+
+1. 在 `cache/cache.go` 中新增 `DisableDbCache()` 方法:
+```go
+func (that *HoTimeCache) DisableDbCache() {
+ that.dbCache = nil
+ that.redisCache = nil
+}
+```
+
+2. 在 `testing_helper.go` 的 `NewTestApp` 中调用:
+```go
+if app.HoTimeCache != nil {
+ app.HoTimeCache.DisableDbCache()
+}
+```
+
+3. 在 `db/crud.go` 的 `Select` 方法中,当 `testTx` 激活时跳过缓存逻辑:
+```go
+if that.testTx == nil {
+ // 原有缓存读写逻辑
+}
+```
+
+**影响范围:** 测试期间仅使用 memory 缓存,不影响生产环境。Session 在测试中通过 `WithSession()` 直接注入,不依赖 DB/Redis 缓存。
+
+### 3.3 测试模式下 Query 的重试策略调整
+
+**问题:** `queryWithRetry` 和 `execWithRetry` 在遇到连接错误时会调用 `db.Ping()` 尝试重连,但 `testTx` 是单连接事务,Ping 会创建新连接导致 `busy buffer`。
+
+**解决:** 当 `testTx != nil` 时,跳过 `db.Ping()` 和重试逻辑,直接返回错误。
## 二、链式测试 API
@@ -168,13 +247,45 @@ func TestApi(t *testing.T) {
## 五、文件改动汇总
-- **修改** `d:/work/hotimev1.5/db/db.go` -- 加 testTx 字段 + BeginTestTx/RollbackTestTx
-- **修改** `d:/work/hotimev1.5/db/query.go` -- Query/Exec 各加 testTx 判断
-- **修改** `d:/work/hotimev1.5/db/transaction.go` -- Action 加 SAVEPOINT 分支 + testTx 传递
-- **新增** `d:/work/hotimev1.5/testing_helper.go` -- TestApp、RunTests、覆盖率
-- **新增** `d:/work/hotimev1.5/testing_api.go` -- 链式 API + TestCollector
-- **新增** `d:/work/hotimev1.5/testing_swagger.go` -- Swagger 生成
-- **修改** `d:/work/xbc/app/user.go` -- 末尾添加 UserTest
-- **修改** `d:/work/xbc/app/init.go` -- 添加 ProjectTest
-- **新增** `d:/work/xbc/app/app_test.go` -- 测试入口
+### hotimev1.5 框架层
+- **修改** `db/db.go` -- 加 testTx + testMu 字段,BeginTestTx(初始化 mutex)/RollbackTestTx
+- **修改** `db/query.go` -- Query/Exec 各加 testTx 判断 + testMu 互斥锁保护 + 跳过重试逻辑
+- **修改** `db/transaction.go` -- Action 加 SAVEPOINT 分支 + testTx/testMu 传递 + SAVEPOINT 操作加锁
+- **修改** `db/crud.go` -- Select 中 testTx 激活时跳过缓存逻辑
+- **修改** `cache/cache.go` -- 新增 DisableDbCache() 方法 + 新增 SessionsGet/SessionsSet/SessionsDelete 批量操作
+- **修改** `session.go` -- 对接批量 Session 缓存操作
+- **修改** `testing_helper.go` -- NewTestApp 中调用 DisableDbCache()
+- **新增** `testing_helper.go` -- TestApp、RunTests、覆盖率
+- **新增** `testing_api.go` -- 链式 API + TestCollector
+- **新增** `testing_swagger.go` -- Swagger 生成
+
+### xbc 业务层
+- **修改** `app/user.go` -- 末尾添加 UserTest
+- **修改** `app/init.go` -- 添加 ProjectTest
+- **新增** `app/app_test.go` -- 测试入口(含 Swagger 生成)
+- **新增** `app/*_test.go` -- 30+ 个控制器测试文件
+- **新增** `wx/wx_test.go` + `wx/*_test.go` -- 微信端测试入口 + 7 个控制器测试
+- **新增** `dd/dd_test.go` + `dd/*_test.go` -- 钉钉端测试入口 + 6 个控制器测试
+- **新增** `customer/customer_h5_test.go` + `customer/*_test.go` -- H5 客户端测试
+- **新增** `sms/sms_test.go` + `sms/sms_ctr_test.go` -- 短信服务测试
+- **修改** `app/wechat.go` -- 修复 log.Println -> log.Printf(go vet 警告)
+- **修改** `customer/init.go` -- 修复 log.Println -> log.Printf(go vet 警告)
+
+## 六、已知问题与调试经验
+
+### 问题一:busy buffer / bad connection
+- **现象:** 测试运行数秒后出现 `[mysql] busy buffer`,随后 `bad connection`,测试挂起
+- **根因:** 业务代码中 `go func()` 启动的 goroutine(如 `app/goods.go:syncGoodsToYst`、`app/stats.go` 中的统计协程)并发访问 `testTx` 单连接
+- **解决:** 在 `HoTimeDB` 中新增 `testMu *sync.Mutex`,所有 testTx 的 Query/Exec/SAVEPOINT 操作加互斥锁
+- **验证方式:** 通过在 query.go 中插入带时间戳和 goroutine 计数的日志,观察到操作严格串行化,无重叠
+
+### 问题二:Lock wait timeout
+- **现象:** `Error 1205: Lock wait timeout exceeded`,指向 `DELETE FROM cached`
+- **根因:** `HoTimeCache` 的 DB 缓存通过独立连接操作 `cached` 表,与 `testTx` 事务锁冲突
+- **解决:** 测试启动时调用 `DisableDbCache()` 禁用 DB/Redis 缓存,仅保留 memory 缓存
+
+### 问题三:go vet 编译失败
+- **现象:** `go test` 因 `go vet` 警告 `log.Println call has possible Printf formatting directive %v` 而拒绝运行
+- **根因:** `app/wechat.go` 和 `customer/init.go` 中误用 `log.Println` 传入 `%v` 格式化指令
+- **解决:** 改为 `log.Printf`
diff --git a/cache/cache.go b/cache/cache.go
index 12ea5a5..83ae733 100644
--- a/cache/cache.go
+++ b/cache/cache.go
@@ -1,36 +1,11 @@
package cache
import (
- "encoding/json"
"errors"
- "os"
- "time"
. "code.hoteas.com/golang/hotime/common"
)
-const debugLogPath = `d:\work\hotimev1.5\.cursor\debug.log`
-
-// debugLog 写入调试日志
-func debugLog(hypothesisId, location, message string, data map[string]interface{}) {
- // #region agent log
- logFile, _ := os.OpenFile(debugLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
- if logFile != nil {
- logEntry, _ := json.Marshal(map[string]interface{}{
- "sessionId": "cache-debug",
- "runId": "test-run",
- "hypothesisId": hypothesisId,
- "location": location,
- "message": message,
- "data": data,
- "timestamp": time.Now().UnixMilli(),
- })
- logFile.Write(append(logEntry, '\n'))
- logFile.Close()
- }
- // #endregion
-}
-
// HoTimeCache 可配置memory,db,redis,默认启用memory,默认优先级为memory>redis>db,memory与数据库缓存设置项一致,
// 缓存数据填充会自动反方向反哺,加入memory缓存过期将自动从redis更新,但memory永远不会更新redis,如果是集群建议不要开启memory,配置即启用
type HoTimeCache struct {
@@ -214,26 +189,12 @@ func (that *HoTimeCache) SessionsGet(keys []string) Map {
return Map{}
}
- // #region agent log
- debugLog("D", "cache.go:SessionsGet:start", "SessionsGet开始", map[string]interface{}{
- "keys_count": len(keys),
- "has_memory": that.memoryCache != nil,
- "has_redis": that.redisCache != nil,
- "has_db": that.dbCache != nil,
- })
- // #endregion
-
result := make(Map, len(keys))
missingKeys := keys
// 从 memory 获取
if that.memoryCache != nil && that.memoryCache.SessionSet {
memResult := that.memoryCache.CachesGet(keys)
- // #region agent log
- debugLog("D", "cache.go:SessionsGet:memory", "从Memory获取", map[string]interface{}{
- "found_count": len(memResult),
- })
- // #endregion
for k, v := range memResult {
result[k] = v
}
@@ -249,20 +210,9 @@ func (that *HoTimeCache) SessionsGet(keys []string) Map {
// 从 redis 获取未命中的
if len(missingKeys) > 0 && that.redisCache != nil && that.redisCache.SessionSet {
redisResult := that.redisCache.CachesGet(missingKeys)
- // #region agent log
- debugLog("D", "cache.go:SessionsGet:redis", "从Redis获取", map[string]interface{}{
- "missing_count": len(missingKeys),
- "found_count": len(redisResult),
- })
- // #endregion
// 反哺到 memory
if that.memoryCache != nil && that.memoryCache.SessionSet && len(redisResult) > 0 {
that.memoryCache.CachesSet(redisResult)
- // #region agent log
- debugLog("D", "cache.go:SessionsGet:backfill_redis_to_mem", "Redis数据反哺到Memory", map[string]interface{}{
- "backfill_count": len(redisResult),
- })
- // #endregion
}
for k, v := range redisResult {
result[k] = v
@@ -280,29 +230,13 @@ func (that *HoTimeCache) SessionsGet(keys []string) Map {
// 从 db 获取未命中的
if len(missingKeys) > 0 && that.dbCache != nil && that.dbCache.SessionSet {
dbResult := that.dbCache.CachesGet(missingKeys)
- // #region agent log
- debugLog("D", "cache.go:SessionsGet:db", "从DB获取", map[string]interface{}{
- "missing_count": len(missingKeys),
- "found_count": len(dbResult),
- })
- // #endregion
// 反哺到 memory 和 redis
if len(dbResult) > 0 {
if that.memoryCache != nil && that.memoryCache.SessionSet {
that.memoryCache.CachesSet(dbResult)
- // #region agent log
- debugLog("D", "cache.go:SessionsGet:backfill_db_to_mem", "DB数据反哺到Memory", map[string]interface{}{
- "backfill_count": len(dbResult),
- })
- // #endregion
}
if that.redisCache != nil && that.redisCache.SessionSet {
that.redisCache.CachesSet(dbResult)
- // #region agent log
- debugLog("D", "cache.go:SessionsGet:backfill_db_to_redis", "DB数据反哺到Redis", map[string]interface{}{
- "backfill_count": len(dbResult),
- })
- // #endregion
}
}
for k, v := range dbResult {
@@ -310,12 +244,6 @@ func (that *HoTimeCache) SessionsGet(keys []string) Map {
}
}
- // #region agent log
- debugLog("D", "cache.go:SessionsGet:end", "SessionsGet完成", map[string]interface{}{
- "total_found": len(result),
- })
- // #endregion
-
return result
}
@@ -326,37 +254,15 @@ func (that *HoTimeCache) SessionsSet(data Map) {
return
}
- // #region agent log
- debugLog("A", "cache.go:SessionsSet:start", "SessionsSet开始", map[string]interface{}{
- "data_count": len(data),
- "has_memory": that.memoryCache != nil,
- "has_redis": that.redisCache != nil,
- "has_db": that.dbCache != nil,
- })
- // #endregion
-
if that.memoryCache != nil && that.memoryCache.SessionSet {
that.memoryCache.CachesSet(data)
- // #region agent log
- debugLog("A", "cache.go:SessionsSet:memory", "写入Memory完成", map[string]interface{}{"count": len(data)})
- // #endregion
}
if that.redisCache != nil && that.redisCache.SessionSet {
that.redisCache.CachesSet(data)
- // #region agent log
- debugLog("A", "cache.go:SessionsSet:redis", "写入Redis完成", map[string]interface{}{"count": len(data)})
- // #endregion
}
if that.dbCache != nil && that.dbCache.SessionSet {
that.dbCache.CachesSet(data)
- // #region agent log
- debugLog("A", "cache.go:SessionsSet:db", "写入DB完成", map[string]interface{}{"count": len(data)})
- // #endregion
}
-
- // #region agent log
- debugLog("A", "cache.go:SessionsSet:end", "SessionsSet完成", nil)
- // #endregion
}
// SessionsDelete 批量删除 Session 缓存
@@ -365,37 +271,15 @@ func (that *HoTimeCache) SessionsDelete(keys []string) {
return
}
- // #region agent log
- debugLog("C", "cache.go:SessionsDelete:start", "SessionsDelete开始", map[string]interface{}{
- "keys_count": len(keys),
- "has_memory": that.memoryCache != nil,
- "has_redis": that.redisCache != nil,
- "has_db": that.dbCache != nil,
- })
- // #endregion
-
if that.memoryCache != nil && that.memoryCache.SessionSet {
that.memoryCache.CachesDelete(keys)
- // #region agent log
- debugLog("C", "cache.go:SessionsDelete:memory", "从Memory删除完成", map[string]interface{}{"count": len(keys)})
- // #endregion
}
if that.redisCache != nil && that.redisCache.SessionSet {
that.redisCache.CachesDelete(keys)
- // #region agent log
- debugLog("C", "cache.go:SessionsDelete:redis", "从Redis删除完成", map[string]interface{}{"count": len(keys)})
- // #endregion
}
if that.dbCache != nil && that.dbCache.SessionSet {
that.dbCache.CachesDelete(keys)
- // #region agent log
- debugLog("C", "cache.go:SessionsDelete:db", "从DB删除完成", map[string]interface{}{"count": len(keys)})
- // #endregion
}
-
- // #region agent log
- debugLog("C", "cache.go:SessionsDelete:end", "SessionsDelete完成", nil)
- // #endregion
}
// CachesGet 批量获取普通缓存
@@ -602,3 +486,8 @@ func (that *HoTimeCache) Init(config Map, hotimeDb HoTimeDBInterface, err ...*Er
}
}
+
+func (that *HoTimeCache) DisableDbCache() {
+ that.dbCache = nil
+ that.redisCache = nil
+}
diff --git a/db/crud.go b/db/crud.go
index 0e87641..b7ae69e 100644
--- a/db/crud.go
+++ b/db/crud.go
@@ -135,23 +135,20 @@ func (that *HoTimeDB) Select(table string, qu ...interface{}) []Map {
qs = append(qs, resWhere...)
md5 := that.md5(query, qs...)
- if that.HoTimeCache != nil && table != "cached" {
- // 如果缓存有则从缓存取
+ if that.testTx == nil && that.HoTimeCache != nil && table != "cached" {
cacheData := that.HoTimeCache.Db(table + ":" + md5)
if cacheData != nil && cacheData.Data != nil {
return cacheData.ToMapArray()
}
}
- // 无缓存则数据库取
res := that.Query(query, qs...)
if res == nil {
res = []Map{}
}
- // 缓存
- if that.HoTimeCache != nil && table != "cached" {
+ if that.testTx == nil && that.HoTimeCache != nil && table != "cached" {
_ = that.HoTimeCache.Db(table+":"+md5, res)
}
diff --git a/db/db.go b/db/db.go
index b6c3bcc..15f99f5 100644
--- a/db/db.go
+++ b/db/db.go
@@ -32,7 +32,8 @@ type HoTimeDB struct {
mu sync.RWMutex
limitMu sync.Mutex
Dialect Dialect // 数据库方言适配器
- testTx *sql.Tx // 测试事务:设置后所有操作都在此事务内,测试结束回滚
+ testTx *sql.Tx // 测试事务:设置后所有操作都在此事务内,测试结束回滚
+ testMu *sync.Mutex // 保护 testTx 单连接不被并发访问
}
// SetConnect 设置数据库配置连接
@@ -154,6 +155,7 @@ func (that *HoTimeDB) BeginTestTx() error {
return err
}
that.testTx = tx
+ that.testMu = &sync.Mutex{}
return nil
}
diff --git a/db/query.go b/db/query.go
index db6d1ac..052495d 100644
--- a/db/query.go
+++ b/db/query.go
@@ -62,7 +62,16 @@ func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interfa
processedArgs := that.processArgs(args)
if that.testTx != nil {
+ if that.testMu != nil { that.testMu.Lock() }
resl, err = that.testTx.Query(query, processedArgs...)
+ that.LastErr.SetError(err)
+ if err != nil {
+ if that.testMu != nil { that.testMu.Unlock() }
+ return nil
+ }
+ result := that.Row(resl)
+ if that.testMu != nil { that.testMu.Unlock() }
+ return result
} else if that.Tx != nil {
resl, err = that.Tx.Query(query, processedArgs...)
} else {
@@ -71,7 +80,6 @@ func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interfa
that.LastErr.SetError(err)
if err != nil {
- // 如果还没重试过,尝试 Ping 后重试一次
if !retried {
if pingErr := db.Ping(); pingErr == nil {
return that.queryWithRetry(query, true, args...)
@@ -120,7 +128,9 @@ func (that *HoTimeDB) execWithRetry(query string, retried bool, args ...interfac
processedArgs := that.processArgs(args)
if that.testTx != nil {
+ if that.testMu != nil { that.testMu.Lock() }
resl, e = that.testTx.Exec(query, processedArgs...)
+ if that.testMu != nil { that.testMu.Unlock() }
} else if that.Tx != nil {
resl, e = that.Tx.Exec(query, processedArgs...)
} else {
@@ -128,8 +138,11 @@ func (that *HoTimeDB) execWithRetry(query string, retried bool, args ...interfac
}
that.LastErr.SetError(e)
- // 判断是否连接断开了,如果还没重试过,尝试重试一次
if e != nil {
+ // testTx 模式下不走连接池重试,避免 busy buffer
+ if that.testTx != nil {
+ return resl, that.LastErr
+ }
if !retried {
if pingErr := that.DB.Ping(); pingErr == nil {
return that.execWithRetry(query, true, args...)
diff --git a/db/transaction.go b/db/transaction.go
index f1ca70d..65a57df 100644
--- a/db/transaction.go
+++ b/db/transaction.go
@@ -32,17 +32,24 @@ func (that *HoTimeDB) Action(action func(db HoTimeDB) (isSuccess bool)) (isSucce
mu: sync.RWMutex{},
limitMu: sync.Mutex{},
testTx: that.testTx,
+ testMu: that.testMu,
}
if that.testTx != nil {
spName := fmt.Sprintf("sp_%d", atomic.AddUint64(&savepointCounter, 1))
+ if that.testMu != nil { that.testMu.Lock() }
_, _ = that.testTx.Exec("SAVEPOINT " + spName)
+ if that.testMu != nil { that.testMu.Unlock() }
db.Tx = that.testTx
isSuccess = action(db)
if !isSuccess {
+ if that.testMu != nil { that.testMu.Lock() }
_, _ = that.testTx.Exec("ROLLBACK TO SAVEPOINT " + spName)
+ if that.testMu != nil { that.testMu.Unlock() }
} else {
+ if that.testMu != nil { that.testMu.Lock() }
_, _ = that.testTx.Exec("RELEASE SAVEPOINT " + spName)
+ if that.testMu != nil { that.testMu.Unlock() }
}
return isSuccess
}
diff --git a/session.go b/session.go
index b511b09..7a7e9f0 100644
--- a/session.go
+++ b/session.go
@@ -1,10 +1,7 @@
package hotime
import (
- "encoding/json"
- "os"
"sync"
- "time"
. "code.hoteas.com/golang/hotime/cache"
. "code.hoteas.com/golang/hotime/common"
@@ -21,15 +18,6 @@ type SessionIns struct {
// set 保存 session 到缓存,必须在锁内调用或传入深拷贝的 map
func (that *SessionIns) setWithCopy() {
- // #region agent log
- logFile, _ := os.OpenFile(`d:\work\hotimev1.5\.cursor\debug.log`, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
- if logFile != nil {
- logEntry, _ := json.Marshal(map[string]interface{}{"sessionId": "debug-session", "runId": "run1", "hypothesisId": "A", "location": "session.go:setWithCopy", "message": "Session写入数据库触发", "data": map[string]interface{}{"session_id": that.SessionId, "map_size": len(that.Map)}, "timestamp": time.Now().UnixMilli()})
- logFile.Write(append(logEntry, '\n'))
- logFile.Close()
- }
- // #endregion
-
// 深拷贝 Map 防止并发修改
that.mutex.RLock()
copyMap := make(Map, len(that.Map))
@@ -49,15 +37,6 @@ func (that *SessionIns) Session(key string, data ...interface{}) *Obj {
that.mutex.Unlock()
if len(data) != 0 {
- // #region agent log
- logFile, _ := os.OpenFile(`d:\work\hotimev1.5\.cursor\debug.log`, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
- if logFile != nil {
- logEntry, _ := json.Marshal(map[string]interface{}{"sessionId": "debug-session", "runId": "run1", "hypothesisId": "B", "location": "session.go:Session", "message": "Session.Set调用", "data": map[string]interface{}{"key": key, "is_delete": data[0] == nil}, "timestamp": time.Now().UnixMilli()})
- logFile.Write(append(logEntry, '\n'))
- logFile.Close()
- }
- // #endregion
-
that.mutex.Lock()
if data[0] == nil {
delete(that.Map, key)
@@ -85,19 +64,6 @@ func (that *SessionIns) SessionsSet(data Map) {
return
}
- // #region agent log
- logFile, _ := os.OpenFile(`d:\work\hotimev1.5\.cursor\debug.log`, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
- if logFile != nil {
- keys := make([]string, 0, len(data))
- for k := range data {
- keys = append(keys, k)
- }
- logEntry, _ := json.Marshal(map[string]interface{}{"sessionId": "debug-session", "runId": "run1", "hypothesisId": "C", "location": "session.go:SessionsSet", "message": "SessionsSet批量设置", "data": map[string]interface{}{"keys": keys, "count": len(data)}, "timestamp": time.Now().UnixMilli()})
- logFile.Write(append(logEntry, '\n'))
- logFile.Close()
- }
- // #endregion
-
that.mutex.Lock()
if that.Map == nil {
that.getWithoutLock()
@@ -124,15 +90,6 @@ func (that *SessionIns) SessionsDelete(keys ...string) {
return
}
- // #region agent log
- logFile, _ := os.OpenFile(`d:\work\hotimev1.5\.cursor\debug.log`, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
- if logFile != nil {
- logEntry, _ := json.Marshal(map[string]interface{}{"sessionId": "debug-session", "runId": "run1", "hypothesisId": "C", "location": "session.go:SessionsDelete", "message": "SessionsDelete批量删除", "data": map[string]interface{}{"keys": keys, "count": len(keys)}, "timestamp": time.Now().UnixMilli()})
- logFile.Write(append(logEntry, '\n'))
- logFile.Close()
- }
- // #endregion
-
that.mutex.Lock()
if that.Map == nil {
that.getWithoutLock()
diff --git a/testing_helper.go b/testing_helper.go
index 529c3e3..cd4703a 100644
--- a/testing_helper.go
+++ b/testing_helper.go
@@ -89,6 +89,10 @@ func NewTestApp(configPath string, projects TestProj, listeners ...func(*Context
}
app.SetupForTest(router)
+ if app.HoTimeCache != nil {
+ app.HoTimeCache.DisableDbCache()
+ }
+
return &TestApp{
Application: app,
projs: projects,
diff --git a/testing_swagger.go b/testing_swagger.go
index f97f1e7..9c816f3 100644
--- a/testing_swagger.go
+++ b/testing_swagger.go
@@ -27,8 +27,8 @@ func (that *TestApp) GenerateSwagger(title, version, outputDir string) error {
if outputDir == "" {
outputDir = "tpt"
}
- swaggerDir := filepath.Join(outputDir, "swagger")
- if err := os.MkdirAll(swaggerDir, os.ModePerm); err != nil {
+ swaggerRoot := filepath.Join(outputDir, "swagger")
+ if err := os.MkdirAll(swaggerRoot, os.ModePerm); err != nil {
return fmt.Errorf("创建 swagger 目录失败: %w", err)
}
@@ -42,9 +42,13 @@ func (that *TestApp) GenerateSwagger(title, version, outputDir string) error {
pathRecords[r.Path] = append(pathRecords[r.Path], r)
}
- endpoints := []map[string]interface{}{}
-
for projName, projDef := range that.projs {
+ moduleDir := filepath.Join(swaggerRoot, projName)
+ if err := os.MkdirAll(moduleDir, os.ModePerm); err != nil {
+ return fmt.Errorf("创建模块目录 %s 失败: %w", projName, err)
+ }
+
+ endpoints := []map[string]interface{}{}
for ctrName, ctr := range projDef.Proj {
for methodName := range ctr {
apiPath := "/" + projName + "/" + ctrName + "/" + methodName
@@ -99,33 +103,91 @@ func (that *TestApp) GenerateSwagger(title, version, outputDir string) error {
})
}
}
+
+ sort.Slice(endpoints, func(i, j int) bool {
+ return endpoints[i]["path"].(string) < endpoints[j]["path"].(string)
+ })
+
+ spec := map[string]interface{}{
+ "title": title,
+ "version": version,
+ "endpoints": endpoints,
+ }
+
+ specJSON, err := json.MarshalIndent(spec, "", " ")
+ if err != nil {
+ return fmt.Errorf("序列化 JSON 失败: %w", err)
+ }
+ if err := os.WriteFile(filepath.Join(moduleDir, "api-spec.json"), specJSON, os.ModePerm); err != nil {
+ return fmt.Errorf("写入 %s/api-spec.json 失败: %w", projName, err)
+ }
+ if err := os.WriteFile(filepath.Join(moduleDir, "index.html"), []byte(apiConsoleHTML()), os.ModePerm); err != nil {
+ return fmt.Errorf("写入 %s/index.html 失败: %w", projName, err)
+ }
+ fmt.Printf("Swagger 文档已生成: %s\n", moduleDir)
}
- sort.Slice(endpoints, func(i, j int) bool {
- return endpoints[i]["path"].(string) < endpoints[j]["path"].(string)
- })
-
- spec := map[string]interface{}{
- "title": title,
- "version": version,
- "endpoints": endpoints,
+ if err := generateSwaggerPortal(swaggerRoot); err != nil {
+ return fmt.Errorf("生成导航页失败: %w", err)
}
-
- specJSON, err := json.MarshalIndent(spec, "", " ")
- if err != nil {
- return fmt.Errorf("序列化 JSON 失败: %w", err)
- }
- if err := os.WriteFile(filepath.Join(swaggerDir, "api-spec.json"), specJSON, os.ModePerm); err != nil {
- return fmt.Errorf("写入 api-spec.json 失败: %w", err)
- }
- if err := os.WriteFile(filepath.Join(swaggerDir, "index.html"), []byte(apiConsoleHTML()), os.ModePerm); err != nil {
- return fmt.Errorf("写入 index.html 失败: %w", err)
- }
-
- fmt.Printf("Swagger 文档已生成: %s\n", swaggerDir)
return nil
}
+func generateSwaggerPortal(swaggerRoot string) error {
+ entries, err := os.ReadDir(swaggerRoot)
+ if err != nil {
+ return err
+ }
+
+ var modules []struct{ Name, Title string }
+ for _, e := range entries {
+ if !e.IsDir() {
+ continue
+ }
+ specPath := filepath.Join(swaggerRoot, e.Name(), "api-spec.json")
+ title := e.Name()
+ if data, err := os.ReadFile(specPath); err == nil {
+ var spec map[string]interface{}
+ if json.Unmarshal(data, &spec) == nil {
+ if t, ok := spec["title"].(string); ok && t != "" {
+ title = t
+ }
+ }
+ }
+ modules = append(modules, struct{ Name, Title string }{e.Name(), title})
+ }
+ sort.Slice(modules, func(i, j int) bool { return modules[i].Name < modules[j].Name })
+
+ html := swaggerPortalHTML(modules)
+ return os.WriteFile(filepath.Join(swaggerRoot, "index.html"), []byte(html), os.ModePerm)
+}
+
+func swaggerPortalHTML(modules []struct{ Name, Title string }) string {
+ var cards string
+ for _, m := range modules {
+ cards += `