api生成器初版确定提交

This commit is contained in:
张家训
2026-07-28 19:52:30 +08:00
parent 476141ebdc
commit acd74107f9
7 changed files with 1194 additions and 11 deletions
@@ -40,7 +40,7 @@ body{font-family:'Microsoft YaHei',-apple-system,sans-serif;color:#1d1d1f;backgr
.nav-group-title.open .arrow{transform:rotate(90deg)}
.nav-group-title small{font-weight:400;color:#86909c;font-size:11px}
.nav-group-items{overflow:hidden;max-height:0;transition:max-height .3s ease}
.nav-group-title.open+.nav-group-items{max-height:2000px}
.nav-group-title.open+.nav-group-items{max-height:99999px}
.nav-item{display:flex;align-items:center;gap:8px;padding:6px 16px 6px 32px;font-size:12px;color:#4e5969;text-decoration:none;transition:.15s;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.nav-item:hover,.nav-item.active{color:#1677ff;background:#e8f3ff}
.nav-item-icon{font-size:14px;flex-shrink:0}
@@ -182,6 +182,57 @@ body{font-family:'Microsoft YaHei',-apple-system,sans-serif;color:#1d1d1f;backgr
.api-card.search-hidden,.nav-item.search-hidden,.nav-group.search-hidden{display:none!important}
.method-card.search-hidden{display:none!important}
.search-highlight{background:#fff3cd;padding:1px 2px;border-radius:2px}
/* ===== Dubbo 调试按钮 ===== */
.method-copy-btn.dubbo{background:#722ed1;color:#fff;border-color:#722ed1}
.method-copy-btn.dubbo:hover{background:#531dab}
/* ===== Dubbo 调试弹窗(全屏双栏) ===== */
.dubbo-modal-mask{position:fixed;inset:0;background:rgb(0 0 0/40%);z-index:9999;display:flex}
.dubbo-modal{background:#fff;width:100vw;height:100vh;display:flex;flex-direction:column;border-radius:0;box-shadow:none}
.dubbo-modal-header{display:flex;align-items:center;justify-content:space-between;padding:10px 20px;border-bottom:1px solid #e5e6eb;flex-shrink:0;background:#fafbfc}
.dubbo-modal-title{font-size:15px;font-weight:700;color:#1d2129}
.dubbo-modal-close{width:28px;height:28px;border:none;background:#f2f3f5;border-radius:4px;font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;color:#4e5969}
.dubbo-modal-close:hover{background:#e5e6eb}
/* 双栏主体 */
.dubbo-modal-body{flex:1;display:flex;overflow:hidden;min-height:0}
.dubbo-modal-left{flex:1;overflow-y:auto;padding:16px 20px;min-width:0;display:flex;flex-direction:column}
.dubbo-modal-right{width:42%;min-width:360px;max-width:600px;border-left:1px solid #e5e6eb;display:flex;flex-direction:column;background:#fafbfc;overflow:hidden}
.dubbo-modal-body label{display:block;font-size:13px;font-weight:600;color:#4e5969;margin-bottom:4px}
.dubbo-modal-body label .hint{font-weight:400;color:#86909c}
.dubbo-modal-body input,.dubbo-modal-body textarea,.dubbo-modal-body select{width:100%;padding:8px 12px;border:1px solid #d0d5dd;border-radius:4px;font-size:13px;font-family:'Consolas','Courier New',monospace;box-sizing:border-box}
.dubbo-modal-body textarea{resize:vertical;min-height:120px;line-height:1.5}
.dubbo-modal-body input:focus,.dubbo-modal-body textarea:focus,.dubbo-modal-body select:focus{outline:none;border-color:#1677ff;box-shadow:0 0 0 2px rgb(22 119 255/15%)}
.dubbo-field-row{display:flex;gap:12px;margin-bottom:12px}
.dubbo-field-row>div{flex:1;min-width:0}
/* 右栏响应区域 */
.dubbo-response-panel{flex:1;display:flex;flex-direction:column;overflow:hidden}
.dubbo-response-panel-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid #e5e6eb;font-size:12px;background:#fff;flex-shrink:0}
.dubbo-response-panel-empty{flex:1;display:flex;align-items:center;justify-content:center;color:#c9cdd4;font-size:14px}
.dubbo-response-status{font-weight:700}
.dubbo-response-status.ok{color:#00b42a}
.dubbo-response-status.err{color:#f53f3f}
.dubbo-response-content{flex:1;overflow-y:auto;padding:12px;font-size:12px;font-family:'Consolas','Courier New',monospace;white-space:pre-wrap;word-break:break-all;line-height:1.6;background:#fff;border-top:1px solid #e5e6eb}
.dubbo-response-content-time{font-size:11px;color:#86909c;padding:4px 12px;background:#fafbfc;border-top:1px solid #f2f3f5}
/* 底部按钮栏 */
.dubbo-modal-footer{display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:10px 20px;border-top:1px solid #e5e6eb;flex-shrink:0;background:#fafbfc}
.dubbo-btn{padding:8px 20px;border-radius:4px;font-size:13px;font-weight:600;cursor:pointer;border:1px solid #d0d5dd;background:#fff;color:#4e5969;transition:.15s}
.dubbo-btn:hover{background:#f7f8fa}
.dubbo-btn.primary{background:#722ed1;color:#fff;border-color:#722ed1}
.dubbo-btn.primary:hover{background:#531dab}
.dubbo-btn.send{background:#1677ff;color:#fff;border-color:#1677ff}
.dubbo-btn.send:hover{background:#0958d9}
.dubbo-btn.send:disabled{background:#94bfff;border-color:#94bfff;cursor:not-allowed}
.dubbo-spinner{display:inline-block;width:14px;height:14px;border:2px solid #d0d5dd;border-top-color:#1677ff;border-radius:50%;animation:dubboSpin .6s linear infinite;vertical-align:middle;margin-right:4px}
@keyframes dubboSpin{to{transform:rotate(360deg)}}
/* 响应式——窄屏时隐藏右栏 */
@media(max-width:800px){.dubbo-modal-right{display:none}}
.search-match{box-shadow:0 0 0 2px rgba(22,119,255,.25)}
/* ===== 响应式 ===== */
@@ -7,7 +7,8 @@
<style>
{{CSS}}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
<script>var DUBBO_PROXY_URL='{{PROXY_URL}}';</script>
<script>{{HTML2CANVAS}}</script>
</head>
<body>
@@ -270,3 +270,818 @@ document.querySelectorAll('.api-card .api-body, .method-body').forEach(function(
e.preventDefault();
});
});
/* ============================================================
* Dubbo 接口调试
* ============================================================ */
/** 默认网关 URL */
var DUBBO_GW_URL_KEY = 'dubbo_gw_url';
function getGwUrl() {
return localStorage.getItem(DUBBO_GW_URL_KEY) || 'https://gw.test.bestpay.net/open/1.0/GenericFacade/genericInvoke';
}
function setGwUrl(url) { localStorage.setItem(DUBBO_GW_URL_KEY, url); }
/** 默认 header */
function defaultHeaders() {
return {
'Content-Type': 'application/json',
'User-Agent': 'ApiDoc-Debugger/1.0',
'Accept': '*/*'
};
}
/** 构建 eventContext 的值 — 注意内外 env 是不同概念 */
function buildEventContext(tntId, env, evtEnv) {
return JSON.stringify({
tntId:tntId||'0101', env:evtEnv||env||'DEV', arNo:'', evtCd:'', pdCd:'', pdPath:''
});
}
/** 根据 env 下拉选中值计算默认 eventContext.env */
function getDefaultEvtEnv(envVal) {
if (envVal === 'DEV') return 'DEV_1156339215001361525';
if (envVal === 'SIT') {
var now = new Date();
var y = now.getFullYear();
var m = pad2(now.getMonth() + 1);
var d = pad2(now.getDate());
return 'SIT-' + y + m + d + '01';
}
// STB 或其他 → 空
return '';
}
/** env 下拉变更时自动填充 eventContext.env */
function onEnvChange() {
var envEl = document.getElementById('dubboEnv');
var evtEl = document.getElementById('dubboEvtEnv');
if (!envEl || !evtEl) return;
evtEl.value = getDefaultEvtEnv(envEl.value);
}
/** 打开 Dubbo 调试弹窗 */
function openDubboDebug(card) {
window._dubboActiveCard = card;
var iface = card.getAttribute('data-dubbo-interface');
var method = card.getAttribute('data-dubbo-method');
var paramTypes = card.getAttribute('data-dubbo-param-types') || '[]';
var sampleArgs = card.getAttribute('data-dubbo-sample-args') || '[{}]';
var stateKey = getDubboStateKey(iface, method);
var saved = loadDubboState(stateKey);
var gwUrl = (saved && saved.gwUrl) ? saved.gwUrl : getGwUrl();
var argsVal = (saved && saved.args) ? saved.args : formatJsonPretty(sampleArgs);
var tntVal = (saved && saved.tntId) ? saved.tntId : '0101';
var envVal = (saved && saved.env) ? saved.env : 'DEV';
var appVal = (saved && saved.appId) ? saved.appId : '980010000114002';
var evtEnvVal = (saved && saved.evtEnv) ? saved.evtEnv : getDefaultEvtEnv(envVal);
var envOpts = ['DEV','SIT','STB'].map(function(v) {
return '<option' + (v === envVal ? ' selected' : '') + '>' + v + '</option>';
}).join('');
var lastRespHtml = '';
if (saved && saved.lastResponse) {
lastRespHtml = '<div class="dubbo-response-panel" id="dubboResponsePanel">' +
'<div class="dubbo-response-panel-header"><span>响应结果</span>' +
'<span class="dubbo-response-status ' + (saved.lastResponse.ok ? 'ok' : 'err') + '" id="dubboStatus">' +
escHtml(saved.lastResponse.status) + '</span></div>' +
(saved.lastResponse.time ? '<div class="dubbo-response-content-time">📅 ' + escHtml(saved.lastResponse.time) + '</div>' : '') +
'<pre class="dubbo-response-content" id="dubboRespContent">' + escHtml(saved.lastResponse.body) + '</pre></div>';
} else {
lastRespHtml = '<div class="dubbo-response-panel" id="dubboResponsePanel">' +
'<div class="dubbo-response-panel-header"><span>响应结果</span><span class="dubbo-response-status" id="dubboStatus"></span></div>' +
'<div class="dubbo-response-panel-empty">点击发送请求查看结果</div></div>';
}
var html = '<div class="dubbo-modal-mask" id="dubboMask" onclick="if(event.target===this)closeDubboDebug()">' +
'<div class="dubbo-modal">' +
'<div class="dubbo-modal-header">' +
'<span class="dubbo-modal-title">🔧 ' + escHtml(method) + ' — 接口调试</span>' +
'<button class="dubbo-modal-close" onclick="closeDubboDebug()">✕</button>' +
'</div>' +
'<div class="dubbo-modal-body">' +
// 左栏:表单
'<div class="dubbo-modal-left">' +
'<div class="dubbo-field-row">' +
'<div><label>接口 <span class="hint">interfacePath</span></label><input value="' + escHtml(iface) + '" readonly style="background:#f7f8fa;color:#86909c"></div>' +
'<div style="max-width:280px"><label>方法 <span class="hint">method</span></label><input value="' + escHtml(method) + '" readonly style="background:#f7f8fa;color:#86909c"></div>' +
'</div>' +
'<div class="dubbo-field-row"><div><label>参数类型 <span class="hint">parameterTypes</span></label><input value="' + escHtml(paramTypes) + '" readonly style="background:#f7f8fa;color:#86909c"></div></div>' +
'<div style="flex:1;display:flex;flex-direction:column;min-height:0;margin-bottom:12px">' +
'<label>请求参数 <span class="hint">args</span>' +
' <button class="dubbo-btn" style="padding:2px 8px;font-size:11px" onclick="formatDubboArgs()">格式化</button>' +
' <button class="dubbo-btn" style="padding:2px 8px;font-size:11px" onclick="resetDubboArgs()" title="恢复默认示例">重置</button>' +
' <button class="dubbo-btn" style="padding:2px 8px;font-size:11px" onclick="showLogExtractPopup()" title="从日志提取入参">📋 日志提取</button></label>' +
'<textarea id="dubboArgs" spellcheck="false" style="flex:1;min-height:200px">' + escHtml(argsVal) + '</textarea>' +
'</div>' +
'<div class="dubbo-field-row">' +
'<div><label>网关 URL</label><input id="dubboGwUrl" value="' + escHtml(gwUrl) + '"></div>' +
'</div>' +
'<div class="dubbo-field-row">' +
'<div style="max-width:140px"><label>tnt_id</label><input id="dubboTntId" value="' + escHtml(tntVal) + '"></div>' +
'<div style="max-width:140px"><label>env</label><select id="dubboEnv" onchange="onEnvChange()">' + envOpts + '</select></div>' +
'<div style="max-width:160px"><label>app_id</label><input id="dubboAppId" value="' + escHtml(appVal) + '"></div>' +
'<div style="max-width:220px"><label>eventContext.env <span class="hint">实例ID</span></label><input id="dubboEvtEnv" value="' + escHtml(evtEnvVal) + '" placeholder="如 DEV_XQYFGL170183"></div>' +
'</div>' +
'</div>' +
// 右栏:响应
'<div class="dubbo-modal-right">' +
lastRespHtml +
'</div>' +
'</div>' +
'<div class="dubbo-modal-footer">' +
'<button class="dubbo-btn" onclick="copyDubboCurl()">📋 复制 cURL</button>' +
'<button class="dubbo-btn" onclick="copyDubboBody()">📄 复制请求体</button>' +
'<button class="dubbo-btn send" id="dubboSendBtn" onclick="sendDubboRequest()">▶ 发送请求</button>' +
'</div>' +
'</div></div>';
// 移除已有弹窗
var old = document.getElementById('dubboMask');
if (old) old.remove();
document.body.insertAdjacentHTML('beforeend', html);
// 如果有保存的响应结果,展开显示
if (saved && saved.lastResponse) {
var respContent = document.getElementById('dubboRespContent');
if (respContent) { respContent.style.display = ''; }
var emptyEl = document.querySelector('.dubbo-response-panel-empty');
if (emptyEl) { emptyEl.style.display = 'none'; }
}
}
/** 关闭弹窗 */
function closeDubboDebug() {
var mask = document.getElementById('dubboMask');
if (mask) mask.remove();
}
/** localStorage key */
function getDubboStateKey(iface, method) {
return 'dubbo_debug_' + (iface||'') + '_' + (method||'');
}
/** 保存当前调试状态到 localStorage */
function saveDubboState() {
var card = getActiveDubboCard();
if (!card) return;
var iface = card.getAttribute('data-dubbo-interface');
var method = card.getAttribute('data-dubbo-method');
var key = getDubboStateKey(iface, method);
var state = {
gwUrl: (document.getElementById('dubboGwUrl') || {}).value || '',
args: (document.getElementById('dubboArgs') || {}).value || '',
tntId: (document.getElementById('dubboTntId') || {}).value || '',
env: (document.getElementById('dubboEnv') || {}).value || '',
appId: (document.getElementById('dubboAppId') || {}).value || '',
evtEnv: (document.getElementById('dubboEvtEnv') || {}).value || ''
};
// 保留已有的响应结果(如果有)
var existing = loadDubboState(key);
if (existing && existing.lastResponse) {
state.lastResponse = existing.lastResponse;
}
try { localStorage.setItem(key, JSON.stringify(state)); } catch(e) {}
}
/** 读取调试状态 */
function loadDubboState(key) {
try {
var raw = localStorage.getItem(key);
return raw ? JSON.parse(raw) : null;
} catch(e) { return null; }
}
/** 保存响应结果 */
function saveDubboResponse(statusText, body, ok) {
var card = getActiveDubboCard();
if (!card) return;
var iface = card.getAttribute('data-dubbo-interface');
var method = card.getAttribute('data-dubbo-method');
var key = getDubboStateKey(iface, method);
var state = loadDubboState(key) || {};
state.lastResponse = { status: statusText, body: body, ok: ok, time: formatLocalDateTime() };
// 同时保存当前表单字段
state.gwUrl = (document.getElementById('dubboGwUrl') || {}).value || state.gwUrl || '';
state.args = (document.getElementById('dubboArgs') || {}).value || state.args || '';
state.tntId = (document.getElementById('dubboTntId') || {}).value || state.tntId || '';
state.env = (document.getElementById('dubboEnv') || {}).value || state.env || '';
state.appId = (document.getElementById('dubboAppId') || {}).value || state.appId || '';
state.evtEnv = (document.getElementById('dubboEvtEnv') || {}).value || state.evtEnv || '';
try { localStorage.setItem(key, JSON.stringify(state)); } catch(e) {}
}
/** 重置 args 为默认示例 */
function resetDubboArgs() {
var card = getActiveDubboCard();
if (!card) return;
var sampleArgs = card.getAttribute('data-dubbo-sample-args') || '[{}]';
var ta = document.getElementById('dubboArgs');
if (ta) ta.value = formatJsonPretty(sampleArgs);
}
// 去除 JSON 中的单行和多行注释(兼容 Apifox 等工具复制出来的格式)
function stripJsonComments(jsonStr) {
if (!jsonStr) return '';
return jsonStr
.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, function(m, g) { return g ? '' : m; })
.replace(/,\s*\}/g, '}').replace(/,\s*\]/g, ']'); // 去掉尾部逗号
}
/** 格式化 JSON */
function formatJsonPretty(jsonStr) {
try {
var obj = JSON.parse(stripJsonComments(jsonStr));
return JSON.stringify(obj, null, 2);
} catch(e) { return jsonStr; }
}
/** 对 textarea 中的 JSON 进行格式化 */
function formatDubboArgs() {
var ta = document.getElementById('dubboArgs');
if (!ta) return;
ta.value = formatJsonPretty(ta.value);
}
/* ============================================================
* 日志提取功能 — 从日志中解析 DTO 参数填入调试面板
* ============================================================ */
/** 显示日志提取弹窗 */
function showLogExtractPopup() {
var old = document.getElementById('logExtractOverlay');
if (old) old.remove();
var overlay = document.createElement('div');
overlay.id = 'logExtractOverlay';
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:100000;display:flex;align-items:center;justify-content:center';
overlay.innerHTML =
'<div style="background:#fff;border-radius:8px;box-shadow:0 8px 40px rgba(0,0,0,.2);width:680px;max-width:92vw;max-height:85vh;display:flex;flex-direction:column">' +
'<div style="padding:14px 20px;border-bottom:1px solid #e5e6eb;display:flex;align-items:center;justify-content:space-between">' +
'<span style="font-size:15px;font-weight:600">📋 从日志提取入参</span>' +
'<button onclick="closeLogExtractPopup()" style="background:none;border:none;font-size:22px;cursor:pointer;color:#86909c;padding:0;line-height:1">&times;</button>' +
'</div>' +
'<div style="padding:16px 20px;flex:1;display:flex;flex-direction:column;min-height:0">' +
'<div style="color:#86909c;font-size:12px;margin-bottom:8px;line-height:1.6">粘贴日志内容,自动识别 <code style="background:#f2f3f5;padding:1px 6px;border-radius:3px">接口入参:ClassName(field=value,...)</code> 格式,<br>也支持只粘贴 DTO 参数部分。</div>' +
'<textarea id="logExtractInput" placeholder="示例:QuerySplitAccountInfoReqDTO(loanNo=211254457192, seasonList=[QuerySplitAccountInfoReqDTO.Season(season=202305, subjectList=[...])])" style="flex:1;min-height:200px;border:1px solid #d0d5dd;border-radius:6px;padding:10px 12px;font-family:Consolas,Monaco,monospace;font-size:12px;resize:vertical;outline:none;line-height:1.6" spellcheck="false"></textarea>' +
'<div style="margin-top:12px;display:flex;gap:8px">' +
'<button onclick="doExtractLogArgs()" style="background:#165DFF;color:#fff;border:none;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer">🔄 解析并比对</button>' +
'<button onclick="closeLogExtractPopup()" style="background:#f2f3f5;border:1px solid #e5e6eb;border-radius:4px;padding:7px 18px;font-size:13px;cursor:pointer;color:#4e5969">取消</button>' +
'</div>' +
'</div></div>';
document.body.appendChild(overlay);
overlay.addEventListener('click', function(e) { if (e.target === overlay) closeLogExtractPopup(); });
setTimeout(function() { var ta = document.getElementById('logExtractInput'); if (ta) ta.focus(); }, 100);
}
function closeLogExtractPopup() {
var el = document.getElementById('logExtractOverlay');
if (el) el.remove();
}
/** 执行日志解析 → 弹出比对窗口 */
function doExtractLogArgs() {
var raw = (document.getElementById('logExtractInput') || {}).value;
if (!raw || !raw.trim()) { showToast('⚠ 请粘贴日志内容'); return; }
raw = raw.trim();
var dtoText = raw;
var markers = ['接口入参:', '接口入参:', '入参:', '入参:'];
for (var i = 0; i < markers.length; i++) {
var idx = raw.lastIndexOf(markers[i]);
if (idx >= 0) { dtoText = raw.substring(idx + markers[i].length).trim(); break; }
}
try {
var logObj = flattenSuperChain(parseDtoExpr(dtoText));
var card = getActiveDubboCard();
var sampleObj = {};
if (card) {
try {
var sampleArr = JSON.parse(card.getAttribute('data-dubbo-sample-args') || '[{}]');
if (Array.isArray(sampleArr) && sampleArr.length > 0) sampleObj = sampleArr[0];
} catch (ignored) {}
}
closeLogExtractPopup();
showParamComparePopup(sampleObj, logObj);
} catch(e) {
showToast('❌ 解析失败:' + e.message);
}
}
/** 参数比对弹窗:默认参考值 vs 日志提取值 */
function showParamComparePopup(sampleObj, logObj) {
var old = document.getElementById('paramCompareOverlay');
if (old) old.remove();
var diffs = diffObjects(sampleObj, logObj, '');
var changedCount = 0, sampleOnlyCount = 0;
diffs.forEach(function(d) {
if (d.status === 'changed') changedCount++;
else if (d.status === 'sample_only') sampleOnlyCount++;
});
var rowsHtml = '';
diffs.forEach(function(d, idx) {
var badge, rowStyle;
if (d.status === 'changed') {
badge = '<span style="background:#fff7e6;color:#d46b08;padding:1px 8px;border-radius:3px;font-size:11px;font-weight:500">变更</span>';
rowStyle = 'background:#fffbe6';
} else if (d.status === 'sample_only') {
badge = '<span style="background:#e6f7ff;color:#096dd9;padding:1px 8px;border-radius:3px;font-size:11px;font-weight:500">仅默认</span>';
rowStyle = 'background:#f0f8ff';
} else if (d.status === 'log_only') {
badge = '<span style="background:#f6ffed;color:#389e0d;padding:1px 8px;border-radius:3px;font-size:11px;font-weight:500">仅日志</span>';
rowStyle = 'background:#f6ffed';
} else {
badge = '<span style="color:#bbb;font-size:11px">一致</span>';
rowStyle = '';
}
var sampleDisplay = d.sampleVal === undefined ? '<span style="color:#bbb">—</span>' : escHtml(fmtDiffVal(d.sampleVal));
var logDisplay = d.logVal === undefined ? '<span style="color:#bbb">—</span>' : escHtml(fmtDiffVal(d.logVal));
rowsHtml += '<tr style="' + rowStyle + '">' +
'<td style="padding:5px 10px;font-size:11px">' + badge + '</td>' +
'<td style="padding:5px 10px;font-family:Consolas,Monaco,monospace;font-size:12px;max-width:300px;word-break:break-all">' + escHtml(d.path || '(根)') + '</td>' +
'<td style="padding:5px 10px;font-family:Consolas,Monaco,monospace;font-size:12px;max-width:280px;word-break:break-all;' + (d.status==='changed'?'color:#d46b08':'') + '">' + sampleDisplay + '</td>' +
'<td style="padding:5px 10px;font-family:Consolas,Monaco,monospace;font-size:12px;max-width:280px;word-break:break-all;' + (d.status==='changed'?'color:#389e0d;font-weight:500':'') + '">' + logDisplay + '</td>' +
'</tr>';
});
var overlay = document.createElement('div');
overlay.id = 'paramCompareOverlay';
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:100001;display:flex;align-items:center;justify-content:center';
overlay.innerHTML =
'<div style="background:#fff;border-radius:10px;box-shadow:0 12px 60px rgba(0,0,0,.25);width:1100px;max-width:96vw;max-height:90vh;display:flex;flex-direction:column">' +
'<div style="padding:16px 24px;border-bottom:1px solid #e5e6eb;display:flex;align-items:center;justify-content:space-between;flex-shrink:0">' +
'<div><span style="font-size:16px;font-weight:600">🔄 参数比对 — 默认值 vs 日志值</span>' +
'<span style="margin-left:12px;font-size:12px;color:#86909c">' +
(changedCount > 0 ? ' <span style="color:#d46b08;font-weight:500">' + changedCount + ' 项变更</span>' : '') +
(sampleOnlyCount > 0 ? ' <span style="color:#096dd9;font-weight:500;margin-left:8px">' + sampleOnlyCount + ' 项仅默认</span>' : '') +
'</span></div>' +
'<button onclick="closeParamComparePopup()" style="background:none;border:none;font-size:22px;cursor:pointer;color:#86909c;padding:0;line-height:1">&times;</button>' +
'</div>' +
'<div style="flex:1;overflow:auto;min-height:0">' +
'<table style="width:100%;border-collapse:collapse">' +
'<thead><tr style="position:sticky;top:0;background:#fafafa;z-index:1">' +
'<th style="text-align:left;padding:8px 10px;font-size:12px;color:#86909c;border-bottom:2px solid #e5e6eb;width:60px">状态</th>' +
'<th style="text-align:left;padding:8px 10px;font-size:12px;color:#86909c;border-bottom:2px solid #e5e6eb">参数路径</th>' +
'<th style="text-align:left;padding:8px 10px;font-size:12px;color:#86909c;border-bottom:2px solid #e5e6eb">默认参考值</th>' +
'<th style="text-align:left;padding:8px 10px;font-size:12px;color:#86909c;border-bottom:2px solid #e5e6eb">日志提取值</th>' +
'</tr></thead>' +
'<tbody>' + rowsHtml + '</tbody>' +
'</table>' +
(diffs.length === 0 ? '<div style="text-align:center;padding:40px;color:#86909c">无差异,参数完全一致</div>' : '') +
'</div>' +
'<div style="padding:14px 24px;border-top:1px solid #e5e6eb;display:flex;gap:10px;flex-shrink:0">' +
'<button onclick="applyCompareResult(\'merged\')" style="background:#165DFF;color:#fff;border:none;border-radius:6px;padding:8px 22px;font-size:13px;cursor:pointer">✅ 使用合并值(默认 + 日志)</button>' +
'<button onclick="applyCompareResult(\'log\')" style="background:#f2f3f5;border:1px solid #d0d5dd;border-radius:6px;padding:8px 22px;font-size:13px;cursor:pointer;color:#4e5969">📋 仅使用日志值</button>' +
'<button onclick="closeParamComparePopup()" style="background:#fff;border:1px solid #d0d5dd;border-radius:6px;padding:8px 22px;font-size:13px;cursor:pointer;color:#4e5969;margin-left:auto">取消</button>' +
'</div></div>';
document.body.appendChild(overlay);
overlay.addEventListener('click', function(e) { if (e.target === overlay) closeParamComparePopup(); });
// 暂存原始对象
overlay._sampleObj = sampleObj;
overlay._logObj = logObj;
overlay._diffCount = diffs.length;
}
function closeParamComparePopup() {
var el = document.getElementById('paramCompareOverlay');
if (el) el.remove();
}
/** 比对结果应用 */
var _compareResultLogObj = null, _compareResultSampleObj = null;
function applyCompareResult(mode) {
var overlay = document.getElementById('paramCompareOverlay');
var sampleObj = overlay._sampleObj || {};
var logObj = overlay._logObj || {};
var finalObj;
if (mode === 'merged') {
finalObj = deepMerge(sampleObj, logObj);
} else {
finalObj = logObj;
}
var json = JSON.stringify([finalObj], null, 2);
var ta = document.getElementById('dubboArgs');
if (ta) { ta.value = json; showToast('✅ 参数已填入(共 ' + (overlay._diffCount || 0) + ' 个字段参与比对)'); }
closeParamComparePopup();
}
/** 递归对比两个对象,返回差异列表 [{path, sampleVal, logVal, status}] */
function diffObjects(sample, log, path) {
var results = [];
var sampleIsObj = sample && typeof sample === 'object' && !Array.isArray(sample);
var logIsObj = log && typeof log === 'object' && !Array.isArray(log);
if (!sampleIsObj && !logIsObj) {
// 都不是对象,直接比较值
var sv = sample;
var lv = log;
if (sv === undefined && lv !== undefined) results.push({path: path, sampleVal: undefined, logVal: lv, status: 'log_only'});
else if (lv === undefined && sv !== undefined) results.push({path: path, sampleVal: sv, logVal: undefined, status: 'sample_only'});
else if (!eqVal(sv, lv)) results.push({path: path, sampleVal: sv, logVal: lv, status: 'changed'});
return results;
}
// 处理:一个对象一个非对象
if (sampleIsObj !== logIsObj) {
if (!eqVal(sample, log)) results.push({path: path, sampleVal: sample, logVal: log, status: 'changed'});
return results;
}
// 都是对象,递归比较 key
var seen = {};
for (var key in sample) {
if (!sample.hasOwnProperty(key)) continue;
seen[key] = true;
var childPath = path ? path + '.' + key : key;
results = results.concat(diffObjects(sample[key], log ? log[key] : undefined, childPath));
}
for (var key2 in log) {
if (!log.hasOwnProperty(key2) || seen[key2]) continue;
var childPath2 = path ? path + '.' + key2 : key2;
results.push({path: childPath2, sampleVal: undefined, logVal: log[key2], status: 'log_only'});
}
return results;
}
function eqVal(a, b) {
if (a === b) return true;
if (a === undefined || b === undefined) return false;
if (typeof a === 'object' || typeof b === 'object') return JSON.stringify(a) === JSON.stringify(b);
return String(a) === String(b);
}
function fmtDiffVal(v) {
if (v === undefined || v === null) return 'null';
if (typeof v === 'object') return JSON.stringify(v);
return String(v);
}
/** 解析 DTO 表达式 ClassName(field=value, ...),返回 JS 对象 */
function parseDtoExpr(text) {
text = text.trim();
// 找出最外层 ClassName(...) 的括号范围
var m = text.match(/^([\w.]+)\s*\(([\s\S]*)\)$/);
if (!m) {
var found = findOuterParens(text);
if (!found) throw new Error('未识别到 DTO 表达式,期望格式: ClassName(field=value,...)');
text = found;
m = text.match(/^([\w.]+)\s*\(([\s\S]*)\)$/);
if (!m) throw new Error('DTO 格式异常');
}
var obj = {};
var content = m[2];
var pairs = splitTopLevel(content, ',');
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].trim();
if (!pair) continue;
// 在顶层找第一个 '='
var eqIdx = findTopLevelChar(pair, '=');
if (eqIdx < 0) continue;
var key = pair.substring(0, eqIdx).trim();
var valStr = pair.substring(eqIdx + 1).trim();
obj[key] = parseDtoValue(valStr);
}
return obj;
}
/** 在文本中定位最外层用括号包裹的 ClassName(...) 片段 */
function findOuterParens(text) {
var m = text.match(/([\w.]+)\s*\(/);
if (!m) return null;
var start = m.index;
var openIdx = start + m[0].length - 1;
var depth = 1;
for (var i = openIdx + 1; i < text.length; i++) {
if (text[i] === '(') depth++;
else if (text[i] === ')') depth--;
if (depth === 0) return text.substring(start, i + 1);
}
return null;
}
/** 按分隔符拆分字符串,仅拆分括号深度为 0 的位置 */
function splitTopLevel(str, sep) {
var parts = [];
var start = 0;
var depth = 0;
for (var i = 0; i < str.length; i++) {
var ch = str[i];
if (ch === '(' || ch === '[') depth++;
else if (ch === ')' || ch === ']') depth--;
else if (ch === sep && depth === 0) {
parts.push(str.substring(start, i));
start = i + 1;
}
}
parts.push(str.substring(start));
return parts;
}
/** 在字符串中找括号深度为 0 的位置 */
function findTopLevelChar(str, ch) {
var depth = 0;
for (var i = 0; i < str.length; i++) {
if (str[i] === '(' || str[i] === '[') depth++;
else if (str[i] === ')' || str[i] === ']') depth--;
else if (str[i] === ch && depth === 0) return i;
}
return -1;
}
/** 递归解析 DTO 值:嵌套 DTO / 列表 / 基本值 */
function parseDtoValue(val) {
val = val.trim();
if (!val) return null;
// 列表:[item, item, ...]
if (val.charAt(0) === '[' && val.charAt(val.length - 1) === ']') {
var inner = val.substring(1, val.length - 1).trim();
if (!inner) return [];
var items = splitTopLevel(inner, ',');
var arr = [];
for (var i = 0; i < items.length; i++) {
var item = items[i].trim();
if (!item) continue;
arr.push(parseDtoValue(item));
}
return arr;
}
// 嵌套 DTO:ClassName(...)
if (/^[\w.]+\s*\(/.test(val)) return parseDtoExpr(val);
// 匿名对象:super=(requestSystem="CF_MIX", ...)
if (val.charAt(0) === '(' && val.charAt(val.length - 1) === ')') {
return parseAnonymousDto(val.substring(1, val.length - 1));
}
// null
if (val === 'null' || val === 'NULL') return null;
// 日志中的 DTO 表达式不区分类型(string/number/boolean),统一按字符串返回,
// Dubbo 序列化时由方法签名决定最终类型,避免误判(如 loanNo=211254457192 被当成数字截断)
// Date 格式转换:Tue Jul 28 18:10:00 CST 2026 → 2026-07-28 18:10:00
if (/^\w{3}\s\w{3}\s\d{1,2}\s\d{2}:\d{2}:\d{2}\s\w+\s\d{4}$/.test(val)) {
var d = new Date(val);
if (!isNaN(d.getTime())) {
return d.getFullYear() + '-' +
pad2(d.getMonth() + 1) + '-' +
pad2(d.getDate()) + ' ' +
pad2(d.getHours()) + ':' +
pad2(d.getMinutes()) + ':' +
pad2(d.getSeconds());
}
}
return val;
}
/** 两位数补零 */
function pad2(n) { return n < 10 ? '0' + n : '' + n; }
/** 解析匿名对象 (field=value, ...),没有类名前缀 */
function parseAnonymousDto(content) {
var obj = {};
var pairs = splitTopLevel(content, ',');
for (var i = 0; i < pairs.length; i++) {
var pair = pairs[i].trim();
if (!pair) continue;
var eqIdx = findTopLevelChar(pair, '=');
if (eqIdx < 0) continue;
var key = pair.substring(0, eqIdx).trim();
var valStr = pair.substring(eqIdx + 1).trim();
obj[key] = parseDtoValue(valStr);
}
return obj;
}
/** 扁平化 super 继承链。
* 日志中 super=TradeRequestDTO(super=BaseRequestDTO(...), ...) 嵌套结构
* 需要把 super 中的父类字段提升到当前层级,使比对路径与默认值对齐 */
function flattenSuperChain(obj) {
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj;
var result = {};
for (var key in obj) {
if (!obj.hasOwnProperty(key)) continue;
if (key === 'super' && obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
var superFlat = flattenSuperChain(obj[key]);
for (var sKey in superFlat) {
if (superFlat.hasOwnProperty(sKey)) result[sKey] = superFlat[sKey];
}
} else if (key !== 'super') {
result[key] = obj[key];
}
}
return result;
}
/** 深度合并:src 覆盖 base,base 补齐 src 缺失字段。仅处理普通对象,数组直接覆盖 */
function deepMerge(base, src) {
if (!base || typeof base !== 'object') return src;
if (!src || typeof src !== 'object') return base;
if (Array.isArray(base) || Array.isArray(src)) return src;
var result = {};
for (var key in base) {
if (base.hasOwnProperty(key)) {
if (src.hasOwnProperty(key)) {
result[key] = deepMerge(base[key], src[key]);
} else {
result[key] = base[key];
}
}
}
for (var key2 in src) {
if (src.hasOwnProperty(key2) && !base.hasOwnProperty(key2)) {
result[key2] = src[key2];
}
}
return result;
}
/** HTML 转义 */
function escHtml(s) {
if (!s) return '';
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
}
/** 获取本地时区的 yyyy-MM-dd HH:mm:ss */
function formatLocalDateTime() {
var d = new Date();
var pad = function(n) { return (n < 10 ? '0' : '') + n; };
return d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' +
pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds());
}
/** 构建请求 body */
function buildDubboBody() {
var argsText = document.getElementById('dubboArgs') ? document.getElementById('dubboArgs').value : '{}';
var tntId = document.getElementById('dubboTntId') ? document.getElementById('dubboTntId').value : '0101';
var env = document.getElementById('dubboEnv') ? document.getElementById('dubboEnv').value : 'STB';
var evtEnv = document.getElementById('dubboEvtEnv') ? document.getElementById('dubboEvtEnv').value : 'DEV';
var appId = document.getElementById('dubboAppId') ? document.getElementById('dubboAppId').value : '980010000114002';
var card = getActiveDubboCard();
var iface = card ? card.getAttribute('data-dubbo-interface') : '';
var method = card ? card.getAttribute('data-dubbo-method') : '';
var paramTypes = card ? card.getAttribute('data-dubbo-param-types') || '[]' : '[]';
var args;
try { args = JSON.parse(stripJsonComments(argsText)); } catch(e) { alert('args JSON 格式错误: ' + e.message); return null; }
return {
data: {
interfacePath: iface,
method: method,
parameterTypes: JSON.parse(paramTypes),
args: args,
traceId: genTraceId(),
zoneType: 'G',
eventContext: {
tntId: tntId, ipTId:'', ipId:'null', ipRId:'', chlNo: genTraceId(),
originalArNo:'', originalEvtCd:'', arNo:'', chlEvtCd:'',
prePdCd:'null', preEvtCd:'null', pdCd:'', evtCd:'', pdPath:'',
env: evtEnv, tagExtensionInfo:'{}', shadow:'0'
}
},
tnt_id: tntId,
app_id: appId,
timestamp: formatLocalDateTime(),
env: env
};
}
/** 获取当前打开调试的那个 method-card */
function getActiveDubboCard() {
var mask = document.getElementById('dubboMask');
if (!mask) return null;
// 从触发按钮找:记录最后点击的 card
return window._dubboActiveCard || document.querySelector('.method-card[data-dubbo-interface]');
}
/** 生成 traceId */
function genTraceId() {
return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c==='x'? r : (r&0x3|0x8);
return v.toString(16);
});
}
/** 发送 Dubbo 请求 — 通过本地 Java 代理转发,解决浏览器无法直连内网网关的问题 */
function sendDubboRequest() {
var body = buildDubboBody();
if (!body) return;
// 发送前保存当前表单状态
saveDubboState();
var gwUrl = document.getElementById('dubboGwUrl') ? document.getElementById('dubboGwUrl').value : getGwUrl();
setGwUrl(gwUrl);
var btn = document.getElementById('dubboSendBtn');
btn.disabled = true;
btn.innerHTML = '<span class="dubbo-spinner"></span>请求中...';
// 确保右栏响应面板存在
var statusEl = document.getElementById('dubboStatus');
var contentEl = document.getElementById('dubboRespContent');
var emptyEl = document.querySelector('.dubbo-response-panel-empty');
if (emptyEl) emptyEl.style.display = 'none';
if (!contentEl) {
var rightCol = document.querySelector('.dubbo-modal-right');
if (rightCol) {
rightCol.innerHTML = '<div class="dubbo-response-panel" id="dubboResponsePanel">' +
'<div class="dubbo-response-panel-header"><span>响应结果</span><span class="dubbo-response-status" id="dubboStatus"></span></div>' +
'<pre class="dubbo-response-content" id="dubboRespContent"></pre></div>';
contentEl = document.getElementById('dubboRespContent');
statusEl = document.getElementById('dubboStatus');
}
}
statusEl.textContent = '请求中...';
statusEl.className = 'dubbo-response-status';
contentEl.textContent = '';
// 去除旧的 time 行
var oldTime = document.querySelector('.dubbo-response-content-time');
if (oldTime) oldTime.remove();
var headers = defaultHeaders();
var evtEnv = document.getElementById('dubboEvtEnv') ? document.getElementById('dubboEvtEnv').value : 'DEV';
headers['Event-Context'] = buildEventContext(body.tnt_id, body.env, evtEnv);
headers['X-Gateway-Url'] = gwUrl;
// 走本地 Java 代理,避免浏览器 CORS/网络限制
fetch(DUBBO_PROXY_URL, {
method: 'POST',
headers: headers,
body: JSON.stringify(body)
}).then(function(res) {
var statusOk = res.ok;
var statusText = res.status + ' ' + res.statusText;
statusEl.textContent = statusText;
statusEl.className = 'dubbo-response-status ' + (statusOk ? 'ok' : 'err');
return res.text().then(function(txt) {
var displayText;
try {
var j = JSON.parse(txt);
displayText = JSON.stringify(j, null, 2);
} catch(e) {
displayText = txt;
}
contentEl.textContent = displayText;
// 保存响应结果到 localStorage
saveDubboResponse(statusText, displayText, statusOk);
// 追加时间戳显示
var timeEl = document.querySelector('.dubbo-response-content-time');
if (timeEl) timeEl.remove();
var timeHtml = '<div class="dubbo-response-content-time">📅 ' + formatLocalDateTime() + '</div>';
contentEl.insertAdjacentHTML('beforebegin', timeHtml);
btn.disabled = false;
btn.textContent = '▶ 发送请求';
});
}).catch(function(err) {
var errMsg = err.message || String(err);
statusEl.textContent = '网络错误';
statusEl.className = 'dubbo-response-status err';
contentEl.textContent = errMsg;
saveDubboResponse('网络错误', errMsg, false);
btn.disabled = false;
btn.textContent = '▶ 发送请求';
});
}
/** 复制为 cURL */
function copyDubboCurl() {
var body = buildDubboBody();
if (!body) return;
var gwUrl = document.getElementById('dubboGwUrl') ? document.getElementById('dubboGwUrl').value : getGwUrl();
var headers = defaultHeaders();
var evtEnv = document.getElementById('dubboEvtEnv') ? document.getElementById('dubboEvtEnv').value : 'DEV';
headers['Event-Context'] = buildEventContext(body.tnt_id, body.env, evtEnv);
var curl = 'curl -X POST \\\n ' + JSON.stringify(gwUrl) + ' \\\n';
for (var k in headers) {
curl += ' -H ' + JSON.stringify(k + ': ' + headers[k]) + ' \\\n';
}
curl += ' -d ' + JSON.stringify(JSON.stringify(body, null, 2));
copyToClip(curl, null);
showToast('cURL 已复制');
}
/** 复制请求体 JSON */
function copyDubboBody() {
var body = buildDubboBody();
if (!body) return;
copyToClip(JSON.stringify(body, null, 2), null);
showToast('请求体已复制');
}
/** Toast 提示 */
function showToast(msg) {
var t = document.getElementById('dubboToast');
if (!t) { t = document.createElement('div'); t.id = 'dubboToast'; t.style.cssText = 'position:fixed;bottom:32px;left:50%;transform:translateX(-50%);background:#1d2129;color:#fff;padding:8px 24px;border-radius:6px;font-size:13px;z-index:99999;pointer-events:none;transition:opacity .3s'; document.body.appendChild(t); }
t.textContent = msg;
t.style.opacity = '1';
clearTimeout(t._tid);
t._tid = setTimeout(function(){ t.style.opacity = '0'; }, 1800);
}
// 修复:track 最后点击的 dubbo card
document.addEventListener('click', function(e) {
var card = e.target.closest('.method-card[data-dubbo-interface]');
if (card) window._dubboActiveCard = card;
});
File diff suppressed because one or more lines are too long