fix(db): 修复事务模式下的错误处理与回滚机制
- 在 `queryWithRetry` 和 `execWithRetry` 方法中添加对 `txFailed` 的状态管理,确保在 SQL 错误时正确标记事务失败 - 增加新的测试用例以验证事务模式下的错误处理逻辑,确保在 SQL 错误发生时能够正确回滚 - 优化测试数据库的初始化方法,支持事务测试场景
This commit is contained in:
@@ -77,6 +77,9 @@ func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interfa
|
||||
resl, err = that.testTx.Query(query, processedArgs...)
|
||||
that.LastErr.SetError(err)
|
||||
if err != nil {
|
||||
if that.txFailed != nil {
|
||||
*that.txFailed = true
|
||||
}
|
||||
if that.testMu != nil {
|
||||
that.testMu.Unlock()
|
||||
}
|
||||
@@ -172,6 +175,9 @@ func (that *HoTimeDB) execWithRetry(query string, retried bool, args ...interfac
|
||||
if e != nil {
|
||||
// testTx 模式下不走连接池重试,避免 busy buffer
|
||||
if that.testTx != nil {
|
||||
if that.txFailed != nil {
|
||||
*that.txFailed = true
|
||||
}
|
||||
return resl, that.LastErr
|
||||
}
|
||||
// 事务内不做连接级重试:死锁等错误会导致 MySQL 自动回滚事务,
|
||||
|
||||
@@ -3,6 +3,7 @@ package db
|
||||
import (
|
||||
. "code.hoteas.com/golang/hotime/common"
|
||||
"database/sql"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -34,6 +35,18 @@ func newTestDB(t *testing.T) *HoTimeDB {
|
||||
return db
|
||||
}
|
||||
|
||||
func newTestDBWithTestTx(t *testing.T) *HoTimeDB {
|
||||
t.Helper()
|
||||
db := newTestDB(t)
|
||||
tx, err := db.DB.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db.testTx = tx
|
||||
db.testMu = &sync.Mutex{}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestAction_TxFailedForceRollback(t *testing.T) {
|
||||
db := newTestDB(t)
|
||||
defer db.DB.Close()
|
||||
@@ -133,3 +146,105 @@ func TestAction_SqlErrorThenReturnTrue_MustRollback(t *testing.T) {
|
||||
t.Fatalf("value should be rolled back to 100 (original), got %d", val)
|
||||
}
|
||||
}
|
||||
|
||||
// --- testTx (SAVEPOINT) 模式测试 ---
|
||||
|
||||
func TestAction_TestTx_SqlErrorForceRollbackSavepoint(t *testing.T) {
|
||||
db := newTestDBWithTestTx(t)
|
||||
defer db.testTx.Rollback()
|
||||
defer db.DB.Close()
|
||||
|
||||
db.testTx.Exec("UPDATE test_item SET value = 100 WHERE id = 1")
|
||||
|
||||
result := db.Action(func(tx HoTimeDB) (isSuccess bool) {
|
||||
tx.Update("test_item", Map{"value": 888}, Map{"id": 1})
|
||||
|
||||
tx.Exec("THIS IS INVALID SQL")
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if result != false {
|
||||
t.Fatalf("Action (testTx mode) should return false when SQL error occurred, got true")
|
||||
}
|
||||
|
||||
db.testMu.Lock()
|
||||
rows, err := db.testTx.Query("SELECT value FROM test_item WHERE id = 1")
|
||||
db.testMu.Unlock()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
t.Fatal("no row found")
|
||||
}
|
||||
var val int64
|
||||
rows.Scan(&val)
|
||||
if val != 100 {
|
||||
t.Fatalf("value should be rolled back to 100 via SAVEPOINT, got %d", val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAction_TestTx_NormalCommitSavepoint(t *testing.T) {
|
||||
db := newTestDBWithTestTx(t)
|
||||
defer db.testTx.Rollback()
|
||||
defer db.DB.Close()
|
||||
|
||||
result := db.Action(func(tx HoTimeDB) (isSuccess bool) {
|
||||
tx.Update("test_item", Map{"value": 777}, Map{"id": 1})
|
||||
return true
|
||||
})
|
||||
|
||||
if result != true {
|
||||
t.Fatalf("Action (testTx mode) should return true on success, got false")
|
||||
}
|
||||
|
||||
db.testMu.Lock()
|
||||
rows, err := db.testTx.Query("SELECT value FROM test_item WHERE id = 1")
|
||||
db.testMu.Unlock()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
t.Fatal("no row found")
|
||||
}
|
||||
var val int64
|
||||
rows.Scan(&val)
|
||||
if val != 777 {
|
||||
t.Fatalf("value should be 777 after RELEASE SAVEPOINT, got %d", val)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAction_TestTx_NormalRollbackSavepoint(t *testing.T) {
|
||||
db := newTestDBWithTestTx(t)
|
||||
defer db.testTx.Rollback()
|
||||
defer db.DB.Close()
|
||||
|
||||
db.testTx.Exec("UPDATE test_item SET value = 100 WHERE id = 1")
|
||||
|
||||
result := db.Action(func(tx HoTimeDB) (isSuccess bool) {
|
||||
tx.Update("test_item", Map{"value": 666}, Map{"id": 1})
|
||||
return false
|
||||
})
|
||||
|
||||
if result != false {
|
||||
t.Fatalf("Action (testTx mode) should return false, got true")
|
||||
}
|
||||
|
||||
db.testMu.Lock()
|
||||
rows, err := db.testTx.Query("SELECT value FROM test_item WHERE id = 1")
|
||||
db.testMu.Unlock()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
t.Fatal("no row found")
|
||||
}
|
||||
var val int64
|
||||
rows.Scan(&val)
|
||||
if val != 100 {
|
||||
t.Fatalf("value should be rolled back to 100 via SAVEPOINT, got %d", val)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"code.hoteas.com/golang/hotime"
|
||||
@@ -11,26 +9,6 @@ import (
|
||||
. "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{}) {
|
||||
logFile, _ := os.OpenFile(debugLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if logFile != nil {
|
||||
logEntry, _ := json.Marshal(map[string]interface{}{
|
||||
"sessionId": "batch-cache-test",
|
||||
"runId": "test-run",
|
||||
"hypothesisId": hypothesisId,
|
||||
"location": location,
|
||||
"message": message,
|
||||
"data": data,
|
||||
"timestamp": time.Now().UnixMilli(),
|
||||
})
|
||||
logFile.Write(append(logEntry, '\n'))
|
||||
logFile.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// TestBatchCacheOperations 测试所有批量缓存操作
|
||||
func TestBatchCacheOperations(app *hotime.Application) {
|
||||
fmt.Println("\n========== 批量缓存操作测试开始 ==========")
|
||||
@@ -64,10 +42,6 @@ func TestBatchCacheOperations(app *hotime.Application) {
|
||||
|
||||
// testCacheMemoryBatch 测试内存缓存批量操作
|
||||
func testCacheMemoryBatch(app *hotime.Application) {
|
||||
// #region agent log
|
||||
debugLog("E", "batch_cache_test.go:testCacheMemoryBatch:start", "开始测试CacheMemory批量操作", nil)
|
||||
// #endregion
|
||||
|
||||
memCache := &cache.CacheMemory{TimeOut: 3600, DbSet: true, SessionSet: true}
|
||||
memCache.SetError(&Error{})
|
||||
|
||||
@@ -79,22 +53,10 @@ func testCacheMemoryBatch(app *hotime.Application) {
|
||||
}
|
||||
memCache.CachesSet(testData)
|
||||
|
||||
// #region agent log
|
||||
debugLog("E", "batch_cache_test.go:testCacheMemoryBatch:afterSet", "CacheMemory.CachesSet完成", map[string]interface{}{"count": len(testData)})
|
||||
// #endregion
|
||||
|
||||
// 测试 CachesGet
|
||||
keys := []string{"mem_key1", "mem_key2", "mem_key3", "mem_key_not_exist"}
|
||||
result := memCache.CachesGet(keys)
|
||||
|
||||
// #region agent log
|
||||
debugLog("E", "batch_cache_test.go:testCacheMemoryBatch:afterGet", "CacheMemory.CachesGet完成", map[string]interface{}{
|
||||
"requested_keys": keys,
|
||||
"result_count": len(result),
|
||||
"result_keys": getMapKeys(result),
|
||||
})
|
||||
// #endregion
|
||||
|
||||
if len(result) != 3 {
|
||||
fmt.Printf(" [FAIL] CacheMemory.CachesGet: 期望3个结果,实际%d个\n", len(result))
|
||||
} else {
|
||||
@@ -105,14 +67,6 @@ func testCacheMemoryBatch(app *hotime.Application) {
|
||||
memCache.CachesDelete([]string{"mem_key1", "mem_key2"})
|
||||
result2 := memCache.CachesGet(keys)
|
||||
|
||||
// #region agent log
|
||||
debugLog("E", "batch_cache_test.go:testCacheMemoryBatch:afterDelete", "CacheMemory.CachesDelete完成", map[string]interface{}{
|
||||
"deleted_keys": []string{"mem_key1", "mem_key2"},
|
||||
"remaining_count": len(result2),
|
||||
"remaining_keys": getMapKeys(result2),
|
||||
})
|
||||
// #endregion
|
||||
|
||||
if len(result2) != 1 || result2["mem_key3"] == nil {
|
||||
fmt.Printf(" [FAIL] CacheMemory.CachesDelete: 删除后期望1个结果,实际%d个\n", len(result2))
|
||||
} else {
|
||||
@@ -122,10 +76,6 @@ func testCacheMemoryBatch(app *hotime.Application) {
|
||||
|
||||
// testCacheDbBatch 测试数据库缓存批量操作
|
||||
func testCacheDbBatch(app *hotime.Application) {
|
||||
// #region agent log
|
||||
debugLog("E", "batch_cache_test.go:testCacheDbBatch:start", "开始测试CacheDb批量操作", nil)
|
||||
// #endregion
|
||||
|
||||
// 使用应用的数据库连接
|
||||
dbCache := &cache.CacheDb{
|
||||
TimeOut: 3600,
|
||||
@@ -147,23 +97,10 @@ func testCacheDbBatch(app *hotime.Application) {
|
||||
}
|
||||
dbCache.CachesSet(testData)
|
||||
|
||||
// #region agent log
|
||||
debugLog("E", "batch_cache_test.go:testCacheDbBatch:afterSet", "CacheDb.CachesSet完成", map[string]interface{}{"count": len(testData)})
|
||||
// #endregion
|
||||
|
||||
// 测试 CachesGet
|
||||
keys := []string{"db_batch_key1", "db_batch_key2", "db_batch_key3", "db_not_exist"}
|
||||
result := dbCache.CachesGet(keys)
|
||||
|
||||
// #region agent log
|
||||
debugLog("E", "batch_cache_test.go:testCacheDbBatch:afterGet", "CacheDb.CachesGet完成", map[string]interface{}{
|
||||
"requested_keys": keys,
|
||||
"result_count": len(result),
|
||||
"result_keys": getMapKeys(result),
|
||||
"result_values": result,
|
||||
})
|
||||
// #endregion
|
||||
|
||||
if len(result) != 3 {
|
||||
fmt.Printf(" [FAIL] CacheDb.CachesGet: 期望3个结果,实际%d个\n", len(result))
|
||||
} else {
|
||||
@@ -181,14 +118,6 @@ func testCacheDbBatch(app *hotime.Application) {
|
||||
dbCache.CachesDelete([]string{"db_batch_key1", "db_batch_key2"})
|
||||
result2 := dbCache.CachesGet(keys)
|
||||
|
||||
// #region agent log
|
||||
debugLog("E", "batch_cache_test.go:testCacheDbBatch:afterDelete", "CacheDb.CachesDelete完成", map[string]interface{}{
|
||||
"deleted_keys": []string{"db_batch_key1", "db_batch_key2"},
|
||||
"remaining_count": len(result2),
|
||||
"remaining_keys": getMapKeys(result2),
|
||||
})
|
||||
// #endregion
|
||||
|
||||
if len(result2) != 1 {
|
||||
fmt.Printf(" [FAIL] CacheDb.CachesDelete: 删除后期望1个结果,实际%d个\n", len(result2))
|
||||
} else {
|
||||
@@ -201,10 +130,6 @@ func testCacheDbBatch(app *hotime.Application) {
|
||||
|
||||
// testHoTimeCacheBatch 测试 HoTimeCache 三级缓存批量操作
|
||||
func testHoTimeCacheBatch(app *hotime.Application) {
|
||||
// #region agent log
|
||||
debugLog("D", "batch_cache_test.go:testHoTimeCacheBatch:start", "开始测试HoTimeCache三级缓存批量操作", nil)
|
||||
// #endregion
|
||||
|
||||
htCache := app.HoTimeCache
|
||||
|
||||
// 清理测试数据
|
||||
@@ -222,10 +147,6 @@ func testHoTimeCacheBatch(app *hotime.Application) {
|
||||
}
|
||||
htCache.SessionsSet(testData)
|
||||
|
||||
// #region agent log
|
||||
debugLog("D", "batch_cache_test.go:testHoTimeCacheBatch:afterSet", "HoTimeCache.SessionsSet完成", map[string]interface{}{"count": len(testData)})
|
||||
// #endregion
|
||||
|
||||
// 测试 SessionsGet
|
||||
keys := []string{
|
||||
hotime.HEAD_SESSION_ADD + "ht_batch_key1",
|
||||
@@ -235,14 +156,6 @@ func testHoTimeCacheBatch(app *hotime.Application) {
|
||||
}
|
||||
result := htCache.SessionsGet(keys)
|
||||
|
||||
// #region agent log
|
||||
debugLog("D", "batch_cache_test.go:testHoTimeCacheBatch:afterGet", "HoTimeCache.SessionsGet完成", map[string]interface{}{
|
||||
"requested_keys": len(keys),
|
||||
"result_count": len(result),
|
||||
"result_keys": getMapKeys(result),
|
||||
})
|
||||
// #endregion
|
||||
|
||||
if len(result) != 3 {
|
||||
fmt.Printf(" [FAIL] HoTimeCache.SessionsGet: 期望3个结果,实际%d个\n", len(result))
|
||||
} else {
|
||||
@@ -256,12 +169,6 @@ func testHoTimeCacheBatch(app *hotime.Application) {
|
||||
})
|
||||
result2 := htCache.SessionsGet(keys)
|
||||
|
||||
// #region agent log
|
||||
debugLog("D", "batch_cache_test.go:testHoTimeCacheBatch:afterDelete", "HoTimeCache.SessionsDelete完成", map[string]interface{}{
|
||||
"remaining_count": len(result2),
|
||||
})
|
||||
// #endregion
|
||||
|
||||
if len(result2) != 1 {
|
||||
fmt.Printf(" [FAIL] HoTimeCache.SessionsDelete: 删除后期望1个结果,实际%d个\n", len(result2))
|
||||
} else {
|
||||
@@ -274,10 +181,6 @@ func testHoTimeCacheBatch(app *hotime.Application) {
|
||||
|
||||
// testSessionInsBatch 测试 SessionIns 批量操作
|
||||
func testSessionInsBatch(app *hotime.Application) {
|
||||
// #region agent log
|
||||
debugLog("B", "batch_cache_test.go:testSessionInsBatch:start", "开始测试SessionIns批量操作", nil)
|
||||
// #endregion
|
||||
|
||||
// 创建一个模拟的 SessionIns
|
||||
session := &hotime.SessionIns{
|
||||
SessionId: "test_batch_session_" + ObjToStr(time.Now().UnixNano()),
|
||||
@@ -292,24 +195,9 @@ func testSessionInsBatch(app *hotime.Application) {
|
||||
}
|
||||
session.SessionsSet(testData)
|
||||
|
||||
// #region agent log
|
||||
debugLog("B", "batch_cache_test.go:testSessionInsBatch:afterSet", "SessionIns.SessionsSet完成", map[string]interface{}{
|
||||
"session_id": session.SessionId,
|
||||
"count": len(testData),
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// 测试 SessionsGet
|
||||
result := session.SessionsGet("field1", "field2", "field3", "not_exist")
|
||||
|
||||
// #region agent log
|
||||
debugLog("B", "batch_cache_test.go:testSessionInsBatch:afterGet", "SessionIns.SessionsGet完成", map[string]interface{}{
|
||||
"result_count": len(result),
|
||||
"result_keys": getMapKeys(result),
|
||||
"result": result,
|
||||
})
|
||||
// #endregion
|
||||
|
||||
if len(result) != 3 {
|
||||
fmt.Printf(" [FAIL] SessionIns.SessionsGet: 期望3个结果,实际%d个\n", len(result))
|
||||
} else {
|
||||
@@ -334,13 +222,6 @@ func testSessionInsBatch(app *hotime.Application) {
|
||||
session.SessionsDelete("field1", "field2")
|
||||
result2 := session.SessionsGet("field1", "field2", "field3")
|
||||
|
||||
// #region agent log
|
||||
debugLog("B", "batch_cache_test.go:testSessionInsBatch:afterDelete", "SessionIns.SessionsDelete完成", map[string]interface{}{
|
||||
"remaining_count": len(result2),
|
||||
"remaining_keys": getMapKeys(result2),
|
||||
})
|
||||
// #endregion
|
||||
|
||||
if len(result2) != 1 {
|
||||
fmt.Printf(" [FAIL] SessionIns.SessionsDelete: 删除后期望1个结果,实际%d个\n", len(result2))
|
||||
} else {
|
||||
@@ -350,10 +231,6 @@ func testSessionInsBatch(app *hotime.Application) {
|
||||
|
||||
// testCacheBackfill 测试缓存反哺机制
|
||||
func testCacheBackfill(app *hotime.Application) {
|
||||
// #region agent log
|
||||
debugLog("D", "batch_cache_test.go:testCacheBackfill:start", "开始测试缓存反哺机制", nil)
|
||||
// #endregion
|
||||
|
||||
htCache := app.HoTimeCache
|
||||
|
||||
// 直接写入数据库缓存(绕过 memory)模拟只有 db 有数据的情况
|
||||
@@ -372,24 +249,10 @@ func testCacheBackfill(app *hotime.Application) {
|
||||
// 直接写入 db
|
||||
dbCache.Cache(testKey, testValue)
|
||||
|
||||
// #region agent log
|
||||
debugLog("D", "batch_cache_test.go:testCacheBackfill:dbWritten", "数据直接写入DB", map[string]interface{}{
|
||||
"key": testKey,
|
||||
"value": testValue,
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// 通过 HoTimeCache 批量获取,应该触发反哺到 memory
|
||||
keys := []string{testKey}
|
||||
result := htCache.CachesGet(keys)
|
||||
|
||||
// #region agent log
|
||||
debugLog("D", "batch_cache_test.go:testCacheBackfill:afterGet", "HoTimeCache.CachesGet完成", map[string]interface{}{
|
||||
"result_count": len(result),
|
||||
"has_key": result[testKey] != nil,
|
||||
})
|
||||
// #endregion
|
||||
|
||||
if len(result) != 1 || result[testKey] == nil {
|
||||
fmt.Println(" [FAIL] 缓存反哺: 从 DB 读取失败")
|
||||
} else {
|
||||
@@ -402,10 +265,6 @@ func testCacheBackfill(app *hotime.Application) {
|
||||
|
||||
// testBatchEfficiency 测试批量操作效率
|
||||
func testBatchEfficiency(app *hotime.Application) {
|
||||
// #region agent log
|
||||
debugLog("A", "batch_cache_test.go:testBatchEfficiency:start", "开始测试批量操作效率", nil)
|
||||
// #endregion
|
||||
|
||||
session := &hotime.SessionIns{
|
||||
SessionId: "efficiency_test_" + ObjToStr(time.Now().UnixNano()),
|
||||
}
|
||||
@@ -423,13 +282,6 @@ func testBatchEfficiency(app *hotime.Application) {
|
||||
|
||||
batchDuration := time.Since(startTime)
|
||||
|
||||
// #region agent log
|
||||
debugLog("A", "batch_cache_test.go:testBatchEfficiency:batchSet", "批量设置完成", map[string]interface{}{
|
||||
"count": len(testData),
|
||||
"duration_ms": batchDuration.Milliseconds(),
|
||||
})
|
||||
// #endregion
|
||||
|
||||
// 对比单个设置
|
||||
session2 := &hotime.SessionIns{
|
||||
SessionId: "efficiency_test_single_" + ObjToStr(time.Now().UnixNano()),
|
||||
@@ -442,13 +294,6 @@ func testBatchEfficiency(app *hotime.Application) {
|
||||
}
|
||||
singleDuration := time.Since(startTime2)
|
||||
|
||||
// #region agent log
|
||||
debugLog("A", "batch_cache_test.go:testBatchEfficiency:singleSet", "单个设置完成", map[string]interface{}{
|
||||
"count": 10,
|
||||
"duration_ms": singleDuration.Milliseconds(),
|
||||
})
|
||||
// #endregion
|
||||
|
||||
fmt.Printf(" 批量设置10个字段耗时: %v\n", batchDuration)
|
||||
fmt.Printf(" 单个设置10个字段耗时: %v\n", singleDuration)
|
||||
|
||||
@@ -467,13 +312,6 @@ func testBatchEfficiency(app *hotime.Application) {
|
||||
session.SessionsGet(keys...)
|
||||
batchGetDuration := time.Since(startTime3)
|
||||
|
||||
// #region agent log
|
||||
debugLog("A", "batch_cache_test.go:testBatchEfficiency:batchGet", "批量获取完成", map[string]interface{}{
|
||||
"count": 10,
|
||||
"duration_ms": batchGetDuration.Milliseconds(),
|
||||
})
|
||||
// #endregion
|
||||
|
||||
fmt.Printf(" 批量获取10个字段耗时: %v\n", batchGetDuration)
|
||||
}
|
||||
|
||||
|
||||
+547
-1126
File diff suppressed because it is too large
Load Diff
@@ -93,6 +93,12 @@ pre.j.resp-j{max-height:calc(100vh - 280px);min-height:120px}
|
||||
.bd-bd textarea{width:100%;min-height:36px;max-height:40vh;padding:5px 8px;border:1px solid var(--border);border-radius:var(--r);background:var(--bg2);color:var(--fg);font-size:12px;font-family:Consolas,Monaco,monospace;resize:vertical;outline:none;overflow-y:auto}
|
||||
.bd-bd textarea:focus{border-color:var(--accent)}
|
||||
.req-star{color:var(--red);font-size:10px;font-weight:700;margin-right:2px;flex-shrink:0;line-height:1.2;align-self:center}
|
||||
.jp-panel{margin-bottom:6px;border:1px solid var(--border);border-radius:var(--r);overflow:hidden}
|
||||
.jp-hd{padding:3px 8px;background:var(--bg2);cursor:pointer;display:flex;align-items:center;font-size:11px;color:var(--fg2);user-select:none}
|
||||
.jp-bd{padding:6px 8px;display:flex;flex-wrap:wrap;gap:4px;border-top:1px solid var(--border)}
|
||||
.jp-tag{font-size:11px;padding:1px 6px;background:var(--bg3);border:1px solid var(--border);border-radius:var(--r);color:var(--fg2);display:inline-flex;align-items:center;gap:2px}
|
||||
.jp-req{color:var(--accent);border-color:rgba(79,195,247,.3)}
|
||||
.jp-type{color:#555;font-size:10px}
|
||||
</style></head><body>
|
||||
|
||||
<div id="side">
|
||||
@@ -264,7 +270,24 @@ function buildLeft(e){
|
||||
l+='<div class="bd-hd'+(defBody==='form'?' on':'')+'" data-t="form" onclick="togBody(\'form\')"><span class="ar'+(defBody==='form'?' o':'')+'">▶</span>表单 (Form)<button class="addbtn" onclick="event.stopPropagation();setAcc(\'form\');addKV(\'f-rows\')">+ 添加</button></div>';
|
||||
l+='<div id="bt-form" class="bd-bd'+(defBody==='form'?' on':'')+'"><div id="f-rows"><div class="kv"><input placeholder="key"><input placeholder="value"><button onclick="this.parentElement.remove()">×</button></div></div></div>';
|
||||
l+='<div class="bd-hd'+(defBody==='json'?' on':'')+'" data-t="json" onclick="togBody(\'json\')"><span class="ar'+(defBody==='json'?' o':'')+'">▶</span>JSON</div>';
|
||||
l+='<div id="bt-json" class="bd-bd'+(defBody==='json'?' on':'')+'"><textarea id="xb" oninput="autoH(this)" placeholder="'+(jsonPH?esc(jsonPH):'{"key":"value"}')+'"></textarea></div>';
|
||||
l+='<div id="bt-json" class="bd-bd'+(defBody==='json'?' on':'')+'">';
|
||||
if(e.params){
|
||||
const jp=e.params.filter(p=>p.in==='json');
|
||||
if(jp.length){
|
||||
const jpVis=localStorage.getItem('json_params_vis')!=='0';
|
||||
l+='<div class="jp-panel"><div class="jp-hd" onclick="togJP()"><span class="ar jp-ar'+(jpVis?' o':'')+'">▶</span><span>JSON 参数</span><span style="margin-left:auto;font-size:10px" class="jp-toggle">'+(jpVis?'收起':'展开')+'</span></div>';
|
||||
l+='<div class="jp-bd"'+(jpVis?'':' style="display:none"')+'>';
|
||||
for(const p of jp){
|
||||
const star=p.required?'<span class="req-star">*</span>':'';
|
||||
const cls=p.required?'jp-tag jp-req':'jp-tag';
|
||||
l+=star+'<span class="'+cls+'">'+esc(p.name);
|
||||
if(p.type)l+=' <span class="jp-type">'+p.type+'</span>';
|
||||
l+='</span>';
|
||||
}
|
||||
l+='</div></div>';
|
||||
}
|
||||
}
|
||||
l+='<textarea id="xb" oninput="autoH(this)" placeholder="'+(jsonPH?esc(jsonPH):'{"key":"value"}')+'"></textarea></div>';
|
||||
l+='</div></div>';
|
||||
return l;
|
||||
}
|
||||
@@ -390,7 +413,7 @@ function pfFill(v){
|
||||
if(nb){if(c.note){nb.textContent='备注: '+c.note;nb.style.display='block';}else{nb.textContent='';nb.style.display='none';}}
|
||||
if(c.query){
|
||||
for(const[k,val]of Object.entries(c.query)){
|
||||
const req=C?.params?.find(p=>p.name===k&&p.in==='query')?.required||false;
|
||||
const req=C?.params?.find(p=>p.name===k)?.required||false;
|
||||
addKVReq('q-rows',k,String(val),req);
|
||||
}
|
||||
}
|
||||
@@ -398,7 +421,7 @@ function pfFill(v){
|
||||
if(c.json){
|
||||
setAcc('json');const el=document.getElementById('xb');
|
||||
if(el){
|
||||
const rq=new Set((C?.params||[]).filter(p=>p.required&&p.in==='json').map(p=>p.name));
|
||||
const rq=new Set((C?.params||[]).filter(p=>p.required).map(p=>p.name));
|
||||
let obj=c.json;
|
||||
if(rq.size&&typeof obj==='object'&&obj!==null&&!Array.isArray(obj)){
|
||||
const sorted={};
|
||||
@@ -412,7 +435,7 @@ function pfFill(v){
|
||||
if(fr){
|
||||
fr.innerHTML='';
|
||||
for(const[k,val]of Object.entries(c.form)){
|
||||
const req=C?.params?.find(p=>p.name===k&&p.in==='form')?.required||false;
|
||||
const req=C?.params?.find(p=>p.name===k)?.required||false;
|
||||
addKVReq('f-rows',k,String(val),req);
|
||||
}
|
||||
addKV('f-rows');
|
||||
@@ -488,6 +511,7 @@ function copyEl(id){navigator.clipboard.writeText(document.getElementById(id).te
|
||||
function cpNext(btn){let el=btn.closest('h5').nextElementSibling;let t='';while(el&&el.tagName!=='H5'){if(el.tagName==='PRE')t+=(t?'\n':'')+el.textContent;el=el.nextElementSibling;}navigator.clipboard.writeText(t).then(()=>{btn.textContent='已复制';setTimeout(()=>{btn.textContent='复制'},1200);});}
|
||||
function getExpVis(){const v=localStorage.getItem('expect_vis');return v===null||v==='1';}
|
||||
function toggleExpect(){const vis=getExpVis();const nv=!vis;localStorage.setItem('expect_vis',nv?'1':'0');document.querySelectorAll('.resp-wrap').forEach(w=>{w.classList.toggle('exp-on',nv);});document.querySelectorAll('.exp-btn').forEach(b=>{b.textContent=nv?'收起 预期响应':'展开 预期响应';});}
|
||||
function togJP(){const bd=document.querySelector('.jp-bd');const ar=document.querySelector('.jp-ar');const tg=document.querySelector('.jp-toggle');if(!bd)return;const vis=bd.style.display!=='none';bd.style.display=vis?'none':'flex';if(ar)ar.classList.toggle('o',!vis);if(tg)tg.textContent=vis?'展开':'收起';localStorage.setItem('json_params_vis',vis?'0':'1');}
|
||||
function fj(o){return esc(JSON.stringify(o,null,2));}
|
||||
function fjp(o,params){
|
||||
if(!params||!params.length||typeof o!=='object'||o===null||Array.isArray(o))return fj(o);
|
||||
|
||||
Reference in New Issue
Block a user