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
@@ -61,9 +61,14 @@ public class ApiDocGenerator {
String template = resourceLoader.loadTemplate();
// 懒初始化本地代理(仅第一次触发)
String proxyUrl = DubboDebugProxy.getInstance().getInvokeUrl();
return template
.replace("{{CSS}}", resourceLoader.loadCss())
.replace("{{JS}}", resourceLoader.loadJs())
.replace("{{HTML2CANVAS}}", resourceLoader.loadHtml2Canvas())
.replace("{{PROXY_URL}}", proxyUrl)
.replace("{{GENERATE_TIME}}", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")))
.replace("{{STATS_BAR}}", buildStatsBar(docs.size(), totalClasses, totalMethods))
.replace("{{SIDEBAR}}", buildSidebar(docs))
@@ -167,7 +172,7 @@ public class ApiDocGenerator {
sb.append(" <div class=\"methods-section\">\n");
sb.append(" <div class=\"section-label\">📋 接口方法</div>\n");
for (ApiDocModel.MethodInfo mi : ci.methods) {
sb.append(buildMethodCard(anchor, mi));
sb.append(buildMethodCard(anchor, mi, ci.fullClassName));
}
sb.append(" </div>\n");
}
@@ -206,15 +211,24 @@ public class ApiDocGenerator {
}
/** 构建单个方法卡片 */
private String buildMethodCard(String classAnchor, ApiDocModel.MethodInfo mi) {
private String buildMethodCard(String classAnchor, ApiDocModel.MethodInfo mi, String fullClassName) {
String methodId = classAnchor + "_" + safeId(mi.name);
StringBuilder sb = new StringBuilder(2000);
sb.append(" <div class=\"method-card\" id=\"").append(methodId).append("\">\n");
String httpMethod = extractHttpMethod(mi.annotations);
StringBuilder sb = new StringBuilder(3000);
// 所有方法默认嵌入调试元数据
String paramTypesJson = buildParamTypesJson(mi);
String sampleArgsJson = buildSampleArgsJson(mi.requestFields);
sb.append(" <div class=\"method-card\" id=\"").append(methodId).append("\"");
sb.append("\n data-dubbo-interface=\"").append(escapeAttr(fullClassName)).append("\"");
sb.append("\n data-dubbo-method=\"").append(escapeAttr(mi.name)).append("\"");
sb.append("\n data-dubbo-param-types='").append(paramTypesJson).append("'");
sb.append("\n data-dubbo-sample-args='").append(sampleArgsJson).append("'");
sb.append(">\n");
// 方法头
sb.append(" <div class=\"method-header\" onclick=\"toggleMethod(this)\">\n");
sb.append(" <span class=\"method-arrow\">▼</span>\n");
String httpMethod = extractHttpMethod(mi.annotations);
if (!httpMethod.isEmpty()) {
sb.append(" <span class=\"http-badge ").append(httpMethod.toLowerCase()).append("\">").append(httpMethod).append("</span>\n");
}
@@ -222,6 +236,7 @@ public class ApiDocGenerator {
if (mi.returnType != null) {
sb.append(" <span class=\"method-return\">→ <code>").append(escapeHtml(mi.returnType)).append("</code></span>\n");
}
sb.append(" <button class=\"method-copy-btn dubbo\" onclick=\"event.stopPropagation();openDubboDebug(this.closest('.method-card'))\" title=\"接口调试\">🔧 调试</button>\n");
sb.append(" <button class=\"method-copy-btn img\" onclick=\"event.stopPropagation();copyImage(this.closest('.method-card'),this)\" title=\"复制为图片(PNG)\">🖼 图片</button>\n");
sb.append(" <button class=\"method-copy-btn txt\" onclick=\"event.stopPropagation();copyRtfFromEl(this.closest('.method-card'),this)\" title=\"复制为 Word 富文本(RTF)\">📋 富文本</button>\n");
sb.append(" </div>\n");
@@ -417,6 +432,108 @@ public class ApiDocGenerator {
return "";
}
// ==================== Dubbo 调试元数据 ====================
/** 提取参数类型列表 JSON 数组,如 ["com.xxx.ReqDTO"] */
private String buildParamTypesJson(ApiDocModel.MethodInfo mi) {
StringBuilder sb = new StringBuilder("[");
if (mi.params != null) {
boolean first = true;
for (ApiDocModel.ParamInfo pi : mi.params) {
if (!first) sb.append(",");
String fqn = pi.fullQualifiedName != null && !pi.fullQualifiedName.isEmpty()
? pi.fullQualifiedName : pi.type;
sb.append("\"").append(escapeJson(fqn)).append("\"");
first = false;
}
}
sb.append("]");
return sb.toString();
}
/** 根据请求字段生成示例 args JSON,如 [{"loanNo":"","seasonList":[...]}] */
private String buildSampleArgsJson(List<ApiDocModel.FieldInfo> requestFields) {
StringBuilder sb = new StringBuilder("[");
if (requestFields != null && !requestFields.isEmpty()) {
sb.append(fieldToSampleJson(requestFields));
}
sb.append("]");
return sb.toString();
}
/** 递归将字段列表转为示例 JSON 字符串 */
private String fieldToSampleJson(List<ApiDocModel.FieldInfo> fields) {
StringBuilder sb = new StringBuilder("{");
for (int i = 0; i < fields.size(); i++) {
if (i > 0) sb.append(",");
ApiDocModel.FieldInfo f = fields.get(i);
sb.append("\"").append(escapeJson(f.name)).append("\":");
sb.append(sampleValueForField(f));
}
sb.append("}");
return sb.toString();
}
/** 根据字段类型返回示例值 */
private String sampleValueForField(ApiDocModel.FieldInfo f) {
if (f.children != null && !f.children.isEmpty()) {
// 如果类型是集合(如 List<DTO> / DTO[] / Set<DTO>),包裹一层数组
if (isCollectionType(f.type)) {
return "[" + fieldToSampleJson(f.children) + "]";
}
return fieldToSampleJson(f.children);
}
// 无 children 的纯集合字段 → 空数组
if (isCollectionType(f.type)) {
return "[]";
}
String type = f.type == null ? "" : f.type.trim();
// 数字类型
if ("int".equals(type) || "long".equals(type) || "short".equals(type) || "byte".equals(type)
|| "double".equals(type) || "float".equals(type)
|| "Integer".equals(type) || "Long".equals(type) || "Short".equals(type) || "Byte".equals(type)
|| "Double".equals(type) || "Float".equals(type)
|| "BigDecimal".equals(type) || "BigInteger".equals(type)) {
return "0";
}
// 布尔
if ("boolean".equals(type) || "Boolean".equals(type)) {
return "false";
}
// 其他默认字符串
return "\"\"";
}
/** 判断 type 是否为集合/数组类型 */
private boolean isCollectionType(String type) {
if (type == null) return false;
// 数组格式 DTO[]
if (type.endsWith("[]")) return true;
// 泛型集合 List<...>, Set<...>, Collection<...> 等
if (type.startsWith("List<") || type.startsWith("Set<") || type.startsWith("Collection<")
|| type.startsWith("ArrayList<") || type.startsWith("HashSet<")) return true;
// 纯集合名(无泛型参数)
String simple = type.trim();
return "List".equals(simple) || "Set".equals(simple) || "Collection".equals(simple)
|| "ArrayList".equals(simple) || "HashSet".equals(simple);
}
/** JSON 字符串转义(双引号、反斜杠、换行等) */
private String escapeJson(String s) {
if (s == null) return "";
return s.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t");
}
/** HTML 属性值转义 */
private String escapeAttr(String s) {
if (s == null) return "";
return s.replace("&", "&amp;").replace("\"", "&quot;").replace("<", "&lt;").replace(">", "&gt;");
}
private String safeId(String s) {
if (s == null) return "x";
return s.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5_-]", "_");
@@ -0,0 +1,156 @@
package com.example.workutils.biz.apidoc.generator;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
/**
* 本地 HTTP 代理,解决浏览器直连内网网关不通的问题。
* 监听 localhost 随机端口,JS 请求本代理,由 Java 侧转发到 gw.test.bestpay.net。
*
* 单例,应用启动时创建,进程退出时自动回收。
*/
@Slf4j
public class DubboDebugProxy {
private static volatile DubboDebugProxy INSTANCE;
private final int port;
private final HttpServer server;
private DubboDebugProxy() {
try {
server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
this.port = server.getAddress().getPort();
server.createContext("/dubbo/invoke", this::handleInvoke);
server.setExecutor(Executors.newFixedThreadPool(4));
server.start();
log.info("DubboDebugProxy 已启动: http://127.0.0.1:{}/dubbo/invoke", port);
} catch (IOException e) {
throw new RuntimeException("无法启动本地代理", e);
}
}
public static DubboDebugProxy getInstance() {
if (INSTANCE == null) {
synchronized (DubboDebugProxy.class) {
if (INSTANCE == null) {
INSTANCE = new DubboDebugProxy();
}
}
}
return INSTANCE;
}
public int getPort() {
return port;
}
public String getInvokeUrl() {
return "http://127.0.0.1:" + port + "/dubbo/invoke";
}
private void handleInvoke(HttpExchange exchange) throws IOException {
// CORS 头
exchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
exchange.getResponseHeaders().add("Access-Control-Allow-Methods", "POST, OPTIONS");
exchange.getResponseHeaders().add("Access-Control-Allow-Headers", "*");
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(204, -1);
return;
}
if (!"POST".equalsIgnoreCase(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(405, -1);
return;
}
// 读取请求体
byte[] body = readAllBytes(exchange.getRequestBody());
// 从 header 中取网关地址(JS 传自定义 header),默认走测试环境
String gwUrl = exchange.getRequestHeaders().getFirst("X-Gateway-Url");
if (gwUrl == null || gwUrl.isEmpty()) {
gwUrl = "https://gw.test.bestpay.net/open/1.0/GenericFacade/genericInvoke";
}
// 透传所有请求头到上游(排除 proxy 专用 header)
Set<String> skipHeaders = new HashSet<>(Arrays.asList(
"X-gateway-url", "Host", "Connection", "Transfer-encoding", "Content-length"));
Map<String, List<String>> reqHeaders = exchange.getRequestHeaders();
try {
// 转发到网关
URI uri = URI.create(gwUrl);
HttpURLConnection conn = (HttpURLConnection) uri.toURL().openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setConnectTimeout(15_000);
conn.setReadTimeout(30_000);
// 透传所有 header
for (Map.Entry<String, List<String>> entry : reqHeaders.entrySet()) {
String key = entry.getKey();
if (key != null && !skipHeaders.contains(key.toLowerCase())) {
for (String val : entry.getValue()) {
conn.addRequestProperty(key, val);
}
}
}
try (OutputStream os = conn.getOutputStream()) {
os.write(body);
os.flush();
}
int status = conn.getResponseCode();
InputStream is = status >= 400 ? conn.getErrorStream() : conn.getInputStream();
byte[] respBody = is != null ? readAllBytes(is) : new byte[0];
// 原样返回
exchange.getResponseHeaders().set("Content-Type", "application/json; charset=utf-8");
exchange.sendResponseHeaders(status, respBody.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(respBody);
}
} catch (Exception e) {
log.error("代理转发失败: {} {}", gwUrl, e.getMessage());
String err = "{\"error\":\"" + escapeJson(e.getMessage()) + "\"}";
byte[] errBytes = err.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(502, errBytes.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(errBytes);
}
}
}
private static byte[] readAllBytes(InputStream is) throws IOException {
byte[] buf = new byte[8192];
java.io.ByteArrayOutputStream bos = new java.io.ByteArrayOutputStream();
int n;
while ((n = is.read(buf)) != -1) {
bos.write(buf, 0, n);
}
return bos.toByteArray();
}
private static String escapeJson(String s) {
if (s == null) return "";
return s.replace("\\", "\\\\").replace("\"", "\\\"");
}
}
@@ -1,9 +1,9 @@
package com.example.workutils.biz.apidoc.generator;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* API 文档资源加载器。
@@ -45,9 +45,19 @@ public class HtmlResourceLoader {
return loadResource(RESOURCE_BASE + "api-doc.js");
}
/**
* 加载 html2canvas 库(本地打包,无外部 CDN 依赖)。
* 对应资源文件:resources/templates/apidoc/html2canvas.min.js
*/
public String loadHtml2Canvas() {
return loadResource(RESOURCE_BASE + "html2canvas.min.js");
}
/**
* 从 classpath 读取资源文件内容。
* 所有资源必须以 UTF-8 编码。
* 使用 ByteArrayOutputStream 替代 Scanner,避免 \\A 分隔符在不同平台上的兼容性问题。
* 同时自动剥离 UTF-8 BOM 头,防止嵌入到 &lt;script&gt; 标签中导致 JS 解析失败。
*/
private String loadResource(String path) {
ClassLoader cl = getClass().getClassLoader();
@@ -58,12 +68,25 @@ public class HtmlResourceLoader {
if (is == null) {
throw new IOException("资源文件未找到: " + path);
}
try (Scanner scanner = new Scanner(is, StandardCharsets.UTF_8.name())) {
scanner.useDelimiter("\\A");
return scanner.hasNext() ? scanner.next() : "";
byte[] bytes = readAllBytes(is);
String content = new String(bytes, StandardCharsets.UTF_8);
// 剥离 UTF-8 BOM(\uFEFF),避免在 <script> 标签中导致 JS 解析失败
if (!content.isEmpty() && content.charAt(0) == '\uFEFF') {
content = content.substring(1);
}
return content;
} catch (IOException e) {
throw new RuntimeException("加载资源失败: " + path, e);
}
}
private static byte[] readAllBytes(InputStream is) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = is.read(buf)) != -1) {
bos.write(buf, 0, n);
}
return bos.toByteArray();
}
}
@@ -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