From 97be68db61a45633e1fc9dbb761cb455f1416b4a Mon Sep 17 00:00:00 2001 From: lianlonggang Date: Fri, 11 Sep 2026 17:50:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20/api/admin/httpPro?= =?UTF-8?q?xy=20=E7=8E=AF=E5=9B=9E=E4=BB=A3=E7=90=86=E7=AB=AF=E7=82=B9?= =?UTF-8?q?=EF=BC=8C=E4=BE=9B=20B=20=E7=AB=AF=E6=8E=A5=E5=8F=A3=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E7=9B=B4=E8=BF=9E=E8=B0=83=E7=94=A8=EF=BC=88=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E9=99=84=E5=8A=A0=20Basic/X-Admin-Token=EF=BC=8C?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E7=99=BD=E5=90=8D=E5=8D=95=E9=99=90=20/order?= =?UTF-8?q?=20=E4=B8=8E=20/api/admin=EF=BC=8C=E8=AF=BB=E8=B6=85=E6=97=B6?= =?UTF-8?q?=20120s=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- .../hospital/front/api/admin/AdminApi.java | 120 +++++++++++++++++- 1 file changed, 119 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/hospital/front/api/admin/AdminApi.java b/src/main/java/com/hospital/front/api/admin/AdminApi.java index 4378e98..4d5949a 100644 --- a/src/main/java/com/hospital/front/api/admin/AdminApi.java +++ b/src/main/java/com/hospital/front/api/admin/AdminApi.java @@ -2,6 +2,8 @@ package com.hospital.front.api.admin; import java.io.File; import java.lang.management.ManagementFactory; +import java.net.URI; +import java.util.Base64; import java.nio.ByteBuffer; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -280,7 +282,123 @@ public class AdminApi { return Result.success(); } - // ================= 5. 接口清单(供 B 端接口测试工具加载) ================= + // ================= 5. 接口代理(供 B 端接口测试工具直连调用) ================= + + /** httpProxy 请求体(Solon 自动绑定 JSON 请求体) */ + public static class HttpProxyReq { + /** HTTP 方法:GET/POST/PUT/DELETE */ + public String method = "GET"; + /** 接口路径,如 /order/getItemList */ + public String path; + /** query 参数(拼到 URL 上) */ + public Map query; + /** 请求体字符串(POST/PUT 时使用) */ + public String body; + /** 额外请求头 */ + public Map headers; + } + + /** 代理专用 HttpClient:环回调用本机接口,读超时 120s 适配大接口 */ + private final java.net.http.HttpClient proxyHttp = java.net.http.HttpClient.newBuilder() + .connectTimeout(java.time.Duration.ofSeconds(5)) + .build(); + + /** + * HTTP 代理:在 main 进程内环回调用本机接口,原样透传响应。 + * + * B 端接口测试工具经此代理访问业务接口,免去 Agent WS 通道倒手 + * (WS 通道有 2MB 消息上限与 60s 等待超时,大接口会超时/掉线)。 + * 自动按 path 附加鉴权头:/order/** → Basic(order.basic 配置), + * /api/admin/** → X-Admin-Token。 + */ + @Mapping(value = "/httpProxy", method = {MethodType.POST}) + public Result> httpProxy(Context ctx, HttpProxyReq req) { + checkToken(ctx); + // 参数校验与路径白名单:只允许代理本机 /order/** 与 /api/admin/** + if (req == null || req.path == null || req.path.isEmpty()) { + throw BusinessException.paramMissing("path 不能为空"); + } + String path = req.path.trim(); + if (!path.startsWith("/order/") && !path.startsWith("/api/admin/")) { + throw BusinessException.businessFailed("仅允许代理 /order/** 与 /api/admin/** 路径:" + path); + } + // 拼接环回 URL:server.port + path + query + String port = Solon.cfg().get("server.port", "8080"); + StringBuilder url = new StringBuilder("http://127.0.0.1:").append(port).append(path); + if (req.query != null && !req.query.isEmpty()) { + StringBuilder qs = new StringBuilder(); + for (Map.Entry e : req.query.entrySet()) { + if (e.getKey() == null || e.getValue() == null) { + continue; + } + if (qs.length() > 0) { + qs.append('&'); + } + qs.append(java.net.URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8)) + .append('=') + .append(java.net.URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)); + } + if (qs.length() > 0) { + url.append('?').append(qs); + } + } + // 构建请求:按 path 附加鉴权头 + java.net.http.HttpRequest.Builder rb = java.net.http.HttpRequest.newBuilder(URI_COMPAT.apply(url.toString())); + String m = req.method == null ? "GET" : req.method.toUpperCase(); + java.net.http.HttpRequest.BodyPublisher pub = req.body == null || req.body.isEmpty() + ? java.net.http.HttpRequest.BodyPublishers.noBody() + : java.net.http.HttpRequest.BodyPublishers.ofString(req.body, StandardCharsets.UTF_8); + switch (m) { + case "POST" -> rb.POST(pub); + case "PUT" -> rb.PUT(pub); + case "DELETE" -> rb.DELETE(); + default -> rb.GET(); + } + if (path.startsWith("/order/")) { + String basic = Solon.cfg().get("order.basic", ""); + boolean authFlag = !"false".equals(Solon.cfg().get("order.authFlag", "true")); + if (authFlag && basic != null && !basic.isEmpty()) { + // 对齐 OrderApi checkBasic 的校验格式:Basic + Base64(user:password) + rb.header("Authorization", "Basic " + + Base64.getEncoder().encodeToString(basic.getBytes(StandardCharsets.UTF_8))); + } + } else { + rb.header("X-Admin-Token", Solon.cfg().get("admin.token", "")); + } + if (req.headers != null) { + for (Map.Entry e : req.headers.entrySet()) { + if (e.getKey() != null && e.getValue() != null) { + rb.header(e.getKey(), e.getValue()); + } + } + } + rb.timeout(java.time.Duration.ofSeconds(120)); + // 执行并透传 + try { + java.net.http.HttpResponse resp = proxyHttp.send(rb.build(), + java.net.http.HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + Map out = new LinkedHashMap<>(); + out.put("status", resp.statusCode()); + out.put("body", resp.body()); + return Result.success(out); + } catch (java.io.IOException e) { + throw BusinessException.businessFailed("环回调用失败(本机 " + path + "):" + e.getMessage()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw BusinessException.businessFailed("环回调用被中断:" + path); + } + } + + /** URI 构建容错(URL 已校验,异常时给出可读错误) */ + private static final java.util.function.Function URI_COMPAT = s -> { + try { + return URI.create(s); + } catch (Exception e) { + throw BusinessException.paramMissing("非法 URL:" + s); + } + }; + + // ================= 6. 接口清单(供 B 端接口测试工具加载) ================= /** * 返回 /order 业务接口元数据清单。