refactor(db): 增强测试事务和缓存管理
- 在 HoTimeDB 中新增 testMu 互斥锁,确保 testTx 的串行访问,解决并发冲突问题 - 在 BeginTestTx 中初始化 testMu,确保测试事务的安全性 - 在 HoTimeCache 中新增 DisableDbCache 方法,测试模式下禁用 DB 和 Redis 缓存,避免锁等待超时 - 更新 Select 方法,确保在 testTx 激活时跳过缓存逻辑,提升测试稳定性 - 优化 Swagger 生成逻辑,支持模块化输出和导航页生成 - 移除冗余的调试日志代码,提升代码整洁性
This commit is contained in:
+97
-28
@@ -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 += `<a class="card" href="` + m.Name + `/index.html"><div class="icon">` + strings.ToUpper(m.Name[:1]) + m.Name[1:] + `</div><div class="title">` + m.Title + `</div><div class="sub">` + m.Name + `/</div></a>`
|
||||
}
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN"><head><meta charset="utf-8"><title>API 文档中心</title>
|
||||
<style>
|
||||
:root{--bg:#1a1a2e;--bg2:#16213e;--bg3:#0f3460;--fg:#e0e0e0;--fg2:#888;--accent:#4fc3f7;--border:#2a2a4a}
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{min-height:100vh;background:var(--bg);color:var(--fg);font:14px/1.6 -apple-system,sans-serif;display:flex;flex-direction:column;align-items:center;padding:60px 20px}
|
||||
h1{font-size:24px;color:var(--accent);margin-bottom:8px}
|
||||
.desc{color:var(--fg2);margin-bottom:40px;font-size:13px}
|
||||
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:16px;width:100%;max-width:900px}
|
||||
.card{display:block;background:var(--bg2);border:1px solid var(--border);border-radius:8px;padding:24px 20px;text-decoration:none;color:var(--fg);transition:all .2s}
|
||||
.card:hover{border-color:var(--accent);transform:translateY(-2px);box-shadow:0 4px 20px rgba(79,195,247,.15)}
|
||||
.icon{font-size:20px;font-weight:700;color:var(--accent);margin-bottom:8px}
|
||||
.title{font-size:15px;font-weight:600;margin-bottom:4px}
|
||||
.sub{font-size:12px;color:var(--fg2)}
|
||||
</style></head><body>
|
||||
<h1>API 文档中心</h1>
|
||||
<div class="desc">选择一个模块查看接口文档与调试控制台</div>
|
||||
<div class="grid">` + cards + `</div>
|
||||
</body></html>`
|
||||
}
|
||||
|
||||
func apiConsoleHTML() string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="zh-CN"><head><meta charset="utf-8"><title>API 调试控制台</title>
|
||||
@@ -217,6 +279,8 @@ pre.j{background:#0a0a1a;padding:6px 8px;border-radius:var(--r);font-size:12px;l
|
||||
<button class="on" onclick="sf('all',this)">全部</button>
|
||||
<button onclick="sf('yes',this)">已测试</button>
|
||||
<button onclick="sf('no',this)">未测试</button>
|
||||
<button onclick="sf('pass',this)">通过</button>
|
||||
<button onclick="sf('fail',this)">未通过</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="tree"></div>
|
||||
@@ -238,10 +302,15 @@ fetch('./api-spec.json').then(r=>r.json()).then(d=>{
|
||||
D=d;document.getElementById('dt').textContent=d.title+' v'+d.version;document.title=d.title;render();
|
||||
});
|
||||
|
||||
function epPass(e){if(!e.cases?.length)return false;return e.cases.every(x=>x.passed);}
|
||||
function epFail(e){if(!e.cases?.length)return e.tested;return e.cases.some(x=>!x.passed);}
|
||||
function render(){
|
||||
if(!D)return;const q=document.getElementById('q').value.toLowerCase();const T={};
|
||||
for(const e of D.endpoints){
|
||||
if(F==='yes'&&!e.tested||F==='no'&&e.tested)continue;
|
||||
if(F==='yes'&&!e.tested)continue;
|
||||
if(F==='no'&&e.tested)continue;
|
||||
if(F==='pass'&&!epPass(e))continue;
|
||||
if(F==='fail'&&!epFail(e))continue;
|
||||
if(q&&!e.summary.toLowerCase().includes(q)&&!e.path.toLowerCase().includes(q)&&!e.ctr.toLowerCase().includes(q))continue;
|
||||
if(!T[e.project])T[e.project]={};if(!T[e.project][e.ctr])T[e.project][e.ctr]=[];T[e.project][e.ctr].push(e);
|
||||
}
|
||||
@@ -254,7 +323,7 @@ function render(){
|
||||
const mn=e.path.split('/').pop();const lbl=e.tested&&e.summary!==mn?mn+' '+e.summary:e.summary;
|
||||
eh+='<div class="ep'+(C?.path===e.path?' act':'')+'" data-p="'+e.path+'" onclick="go(this)"><span class="mt mt-'+m+'">'+e.method+'</span><span class="nm" title="'+e.path+'">'+lbl+'</span>'+tg+'</div>';
|
||||
}
|
||||
ph+='<div class="l2h" onclick="tog(this)"><span class="ar o">▶</span>'+c+'<span class="cn">'+es.length+'</span></div><div class="sub o">'+eh+'</div>';
|
||||
ph+='<div class="l2h" onclick="tog(this)"><span class="ar">▶</span>'+c+'<span class="cn">'+es.length+'</span></div><div class="sub">'+eh+'</div>';
|
||||
}
|
||||
h+='<div class="l1"><div class="l1h" onclick="tog(this)"><span class="ar o">▶</span>'+p+'</div><div class="sub o">'+ph+'</div></div>';
|
||||
}
|
||||
@@ -434,7 +503,7 @@ async function send(){
|
||||
if(token&&authType==='header')opts.headers['Authorization']=token;
|
||||
|
||||
const fileEls=document.querySelectorAll('#file-rows .kv');
|
||||
const files=[];fileEls.forEach(r=>{const fn=r.querySelector('input[type="text"]').value.trim();const fi=r.querySelector('input[type="file"]');if(fn&&fi.files.length)files.push({field:fn,file:fi.files[0]});});
|
||||
const files=[];fileEls.forEach(r=>{const fnEl=r.querySelector('input:not([type="file"])');const fi=r.querySelector('input[type="file"]');if(!fnEl||!fi)return;const fn=fnEl.value.trim();if(fn&&fi.files.length)files.push({field:fn,file:fi.files[0]});});
|
||||
|
||||
const isJson=document.getElementById('bt-json')?.classList.contains('on');
|
||||
const bodyEl=document.getElementById('xb');const formKV=getKV('f-rows');let curlParts='';
|
||||
|
||||
Reference in New Issue
Block a user