- pom: 引 yh-platform-dependencies(import) / yh-api-std / springdoc-openapi-ui 1.7.0 + nexus-public 仓库
- RoutingFilter.isGatewayInternal: /v3/api-docs、/swagger-ui/**、/swagger-ui.html 纳入自身路径旁路,避免被 TargetRouter 当 target 裸 404
- InternalAdminController: page/routes/targets/logs 三接口分页化返回 BasePageResult;补 @Operation(operationId=GatewayAdmin.*);错误契约去 @ResponseStatus(400) 改 200+code=2;logs 兼容旧 limit 入参
- RouteRefreshController: 补 @Tag/@Operation(operationId=GatewayAdmin.refresh)
- static/admin.html: 适配 {code,msg,data} 信封与 content/total 分页
- InternalAdminControllerTest: 重写 12 用例,77 项测试全绿
This commit is contained in:
@@ -23,6 +23,19 @@
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<!-- 基础平台版本管理(yh-* 架包与 spring-boot-dependencies 对齐);形态同 UserBase/Notice -->
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-platform-dependencies</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- 白名单(开放接口中心 T0.8):本清单外依赖不得引入 -->
|
||||
<dependency>
|
||||
@@ -77,6 +90,19 @@
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<!-- 接口规范:管理面(/api/internal/**)成功响应统一包装为 {code,msg,data},与 notice/userbase
|
||||
前端解析形态一致;同时传递引入 yh-utils-core(ErrorCode / BasePageQuery / BasePageResult)。
|
||||
oapi 转发链不经 Controller(Filter 直写 response),不受本 starter 的 ResponseBodyAdvice 影响。 -->
|
||||
<dependency>
|
||||
<groupId>com.yunhai.platform</groupId>
|
||||
<artifactId>yh-api-std</artifactId>
|
||||
</dependency>
|
||||
<!-- Swagger / OpenAPI 文档(springdoc,兼容 Boot 2.7;版本对齐 UserBase/Notice) -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-ui</artifactId>
|
||||
<version>1.7.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
@@ -95,4 +121,14 @@
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>nexus-public</id>
|
||||
<name>bsch/nexus public</name>
|
||||
<url>http://nexus.yunhaixingtong.com/repository/maven-public/</url>
|
||||
<releases><enabled>true</enabled></releases>
|
||||
<snapshots><enabled>true</enabled></snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
</project>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.yunhai.platform.gateway.admin;
|
||||
|
||||
import com.yunhai.platform.apistd.ApiResponse;
|
||||
import com.yunhai.platform.gateway.entity.OpenApiLog;
|
||||
import com.yunhai.platform.gateway.entity.OpenApiRoute;
|
||||
import com.yunhai.platform.gateway.entity.OpenApiTarget;
|
||||
@@ -7,12 +8,18 @@ import com.yunhai.platform.gateway.repository.OpenApiLogRepository;
|
||||
import com.yunhai.platform.gateway.repository.OpenApiRouteRepository;
|
||||
import com.yunhai.platform.gateway.repository.OpenApiTargetRepository;
|
||||
import com.yunhai.platform.gateway.route.RouteRegistry;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.platform.utils.model.BasePageResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
@@ -24,24 +31,27 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 网关治理小页后端(/api/internal/admin/**,D5 最小治理面):路由/目标地址的查询与行内编辑、
|
||||
* 台账倒序查询;写操作落库后立即热重载注册表(routes→reload / targets→reloadTargets),
|
||||
* 台账分页查询;写操作落库后立即热重载注册表(routes→reload / targets→reloadTargets),
|
||||
* 无须重启网关。与 {@code RouteRefreshController} 同前缀,被 RoutingFilter 旁路(不进 oapi 链)。
|
||||
* <p>
|
||||
* <b>响应契约</b>:成功响应由 {@code yh-api-std} 的 {@code ApiResponseAdvice} 统一包成
|
||||
* {@code {code:0, msg:"", data:...}}(与 notice/userbase 一致);非法入参走 {@link #badRequest}
|
||||
* 返回 HTTP 200 + {@code {code:2, msg:...}}(平台规范:业务错误不改 HTTP 状态码)。
|
||||
* <p>
|
||||
* <b>收网前提示</b>:本前缀当前宿主端口可达(10013 未绑 127.0.0.1),页面用于 observe 期
|
||||
* 治理与排障;T0.2 端口收网随下次重新部署自然收紧(届时仅容器网内可达)。
|
||||
* 值域白名单与 {@code docs/sql/01_schema.sql} 注释对齐:visibility∈{INTERNAL,EXTERNAL}、
|
||||
* authMode∈{SIGNED,ANONYMOUS}、cacheScope∈{CALLER,GLOBAL};非法值 400 且不落库。
|
||||
* authMode∈{SIGNED,ANONYMOUS}、cacheScope∈{CALLER,GLOBAL};非法值返回 code=2 且不落库。
|
||||
*/
|
||||
@Tag(name = "GatewayAdmin", description = "网关治理:路由旋钮 / 目标地址 / 访问台账")
|
||||
@RestController
|
||||
@RequestMapping("/api/internal/admin")
|
||||
public class InternalAdminController {
|
||||
@@ -71,28 +81,29 @@ public class InternalAdminController {
|
||||
this.routeRegistry = routeRegistry;
|
||||
}
|
||||
|
||||
/** 治理小页本体(classpath:/static/admin.html,N03 产出) */
|
||||
/** 治理小页本体(classpath:/static/admin.html,N03 产出);返回 String 故不被信封包装 */
|
||||
@Operation(operationId = "GatewayAdmin.page", summary = "治理小页(内置 HTML)")
|
||||
@GetMapping(value = "/page", produces = MediaType.TEXT_HTML_VALUE)
|
||||
public String page() throws IOException {
|
||||
return StreamUtils.copyToString(
|
||||
new ClassPathResource("static/admin.html").getInputStream(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/** 路由表(治理对象全量,含禁用行;排序稳定便于表格展示) */
|
||||
/** 路由表分页(治理对象全量含禁用行;targetCode+pathPattern 稳定排序) */
|
||||
@Operation(operationId = "GatewayAdmin.routes", summary = "路由注册表分页")
|
||||
@GetMapping("/routes")
|
||||
public List<OpenApiRoute> routes() {
|
||||
List<OpenApiRoute> all = routeRepo.findAll();
|
||||
all.sort((a, b) -> {
|
||||
int c = a.getTargetCode().compareTo(b.getTargetCode());
|
||||
return c != 0 ? c : a.getPathPattern().compareTo(b.getPathPattern());
|
||||
});
|
||||
return all;
|
||||
public BasePageResult<OpenApiRoute> routes(
|
||||
@RequestParam(value = "pageIndex", required = false, defaultValue = "1") Integer pageIndex,
|
||||
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize) {
|
||||
Sort sort = Sort.by("targetCode").and(Sort.by("pathPattern"));
|
||||
return toResult(routeRepo.findAll(pageRequest(pageIndex, pageSize, sort)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 行内编辑路由治理旋钮。只接受治理字段(不可改 target/pattern/method——那是路由身份,
|
||||
* 改动应走 SQL 迁移评审);enabled 可整体停用一行。成功后热重载并返回加载数。
|
||||
*/
|
||||
@Operation(operationId = "GatewayAdmin.updateRoute", summary = "编辑路由治理旋钮")
|
||||
@PutMapping("/routes/{id}")
|
||||
public Map<String, Object> updateRoute(@PathVariable("id") Long id,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
@@ -148,15 +159,17 @@ public class InternalAdminController {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 目标地址表全量 */
|
||||
/** 目标地址表分页 */
|
||||
@Operation(operationId = "GatewayAdmin.targets", summary = "目标地址分页")
|
||||
@GetMapping("/targets")
|
||||
public List<OpenApiTarget> targets() {
|
||||
List<OpenApiTarget> all = targetRepo.findAll();
|
||||
all.sort((a, b) -> a.getTargetCode().compareTo(b.getTargetCode()));
|
||||
return all;
|
||||
public BasePageResult<OpenApiTarget> targets(
|
||||
@RequestParam(value = "pageIndex", required = false, defaultValue = "1") Integer pageIndex,
|
||||
@RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize) {
|
||||
return toResult(targetRepo.findAll(pageRequest(pageIndex, pageSize, Sort.by("targetCode"))));
|
||||
}
|
||||
|
||||
/** 行内编辑目标地址/启停;成功后热重载 target 快照(禁用/空白行按无行回落 yml 由 TargetRouter 承担) */
|
||||
@Operation(operationId = "GatewayAdmin.updateTarget", summary = "编辑目标地址/启停")
|
||||
@PutMapping("/targets/{id}")
|
||||
public Map<String, Object> updateTarget(@PathVariable("id") Long id,
|
||||
@RequestBody Map<String, Object> body) {
|
||||
@@ -181,32 +194,61 @@ public class InternalAdminController {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 台账倒序查询(limit 默认 50 上限 500;targetCode/callerAppKey 可空) */
|
||||
/**
|
||||
* 台账倒序分页查询。{@code pageSize} 优先、上限 500;兼容旧 {@code limit} 参数
|
||||
* (未传 pageSize 时以 limit 作 pageSize)。targetCode/callerAppKey 可空=不过滤。
|
||||
*/
|
||||
@Operation(operationId = "GatewayAdmin.logs", summary = "访问台账分页")
|
||||
@GetMapping("/logs")
|
||||
public Map<String, Object> logs(@RequestParam(value = "limit", required = false) Integer limit,
|
||||
@RequestParam(value = "targetCode", required = false) String targetCode,
|
||||
@RequestParam(value = "callerAppKey", required = false) String callerAppKey) {
|
||||
int size = limit == null || limit < 1 ? 50 : Math.min(limit, 500);
|
||||
Page<OpenApiLog> page = logRepo.pageLatest(emptyToNull(targetCode), emptyToNull(callerAppKey),
|
||||
PageRequest.of(0, size));
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("total", page.getTotalElements());
|
||||
out.put("rows", new ArrayList<>(page.getContent()));
|
||||
return out;
|
||||
public BasePageResult<OpenApiLog> logs(
|
||||
@RequestParam(value = "pageIndex", required = false, defaultValue = "1") Integer pageIndex,
|
||||
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||
@RequestParam(value = "limit", required = false) Integer limit,
|
||||
@RequestParam(value = "targetCode", required = false) String targetCode,
|
||||
@RequestParam(value = "callerAppKey", required = false) String callerAppKey) {
|
||||
int size = resolvePageSize(pageSize, limit);
|
||||
int page = normalizePageIndex(pageIndex);
|
||||
Page<OpenApiLog> result = logRepo.pageLatest(emptyToNull(targetCode), emptyToNull(callerAppKey),
|
||||
PageRequest.of(page - 1, size));
|
||||
return toResult(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 值域/参数错误 → 400 + JSON 错误体(页面 alert 展示)。本类抛出的 IllegalArgumentException
|
||||
* 值域/参数错误 → HTTP 200 + {@code {code:2, msg}}(平台规范:业务错误不改 HTTP 状态码,
|
||||
* 与 notice/userbase 的 GlobalExceptionHandler 行为一致)。本类抛出的 IllegalArgumentException
|
||||
* 全是"调用方传值非法",缺省会被 Spring 映射成 500,误导排障方向——故在类内显式收敛。
|
||||
*/
|
||||
@org.springframework.web.bind.annotation.ExceptionHandler(IllegalArgumentException.class)
|
||||
@org.springframework.web.bind.annotation.ResponseStatus(
|
||||
org.springframework.http.HttpStatus.BAD_REQUEST)
|
||||
public Map<String, Object> badRequest(IllegalArgumentException e) {
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
out.put("code", 1);
|
||||
out.put("msg", e.getMessage());
|
||||
return out;
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ApiResponse<Object> badRequest(IllegalArgumentException e) {
|
||||
return ApiResponse.fail(ErrorCode.PARAM_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
private static <T> BasePageResult<T> toResult(Page<T> page) {
|
||||
return new BasePageResult<T>(
|
||||
page.getContent(),
|
||||
(int) page.getTotalElements(),
|
||||
page.getNumber() + 1,
|
||||
page.getSize(),
|
||||
page.getTotalPages(),
|
||||
page.hasNext());
|
||||
}
|
||||
|
||||
private static PageRequest pageRequest(Integer pageIndex, Integer pageSize, Sort sort) {
|
||||
return PageRequest.of(normalizePageIndex(pageIndex) - 1, normalizePageSize(pageSize), sort);
|
||||
}
|
||||
|
||||
private static int normalizePageIndex(Integer pageIndex) {
|
||||
return pageIndex == null || pageIndex < 1 ? 1 : pageIndex;
|
||||
}
|
||||
|
||||
private static int normalizePageSize(Integer pageSize) {
|
||||
return pageSize == null || pageSize < 1 ? 10 : pageSize;
|
||||
}
|
||||
|
||||
/** pageSize 优先;兼容旧 limit 参数,上限 500 */
|
||||
private static int resolvePageSize(Integer pageSize, Integer limit) {
|
||||
Integer v = pageSize != null ? pageSize : limit;
|
||||
return v == null || v < 1 ? 50 : Math.min(v, 500);
|
||||
}
|
||||
|
||||
private static String text(Object v) {
|
||||
|
||||
@@ -37,6 +37,16 @@ public class RoutingFilter implements Filter {
|
||||
*/
|
||||
public static final String ACTUATOR_PREFIX = "/actuator";
|
||||
|
||||
/**
|
||||
* OpenAPI 文档端点(B2):springdoc 的 {@code /v3/api-docs}、{@code /swagger-ui/**} 属网关自身路径,
|
||||
* 须与 {@link #INTERNAL_PREFIX} 同级旁路——否则首段({@code v3} / {@code swagger-ui.html})会被
|
||||
* {@link TargetRouter} 当作 target 解析 → 未知 target 裸 404,文档端点不可达。
|
||||
*/
|
||||
public static final String API_DOCS_PREFIX = "/v3/api-docs";
|
||||
public static final String SWAGGER_UI_PREFIX = "/swagger-ui";
|
||||
/** springdoc UI 传统入口;{@link #isSelfPath} 的 equals+前缀双条件均不覆盖此路径,须单独 equals 判定 */
|
||||
public static final String SWAGGER_UI_HTML = "/swagger-ui.html";
|
||||
|
||||
/** 路由产物 attribute({@link TargetRouter.Routing}) */
|
||||
public static final String ATTR_ROUTING = "yh.gateway.routing";
|
||||
/** 注册表策略 attribute({@link RouteSpec}) */
|
||||
@@ -89,7 +99,11 @@ public class RoutingFilter implements Filter {
|
||||
}
|
||||
|
||||
static boolean isGatewayInternal(String uri) {
|
||||
return isSelfPath(uri, INTERNAL_PREFIX) || isSelfPath(uri, ACTUATOR_PREFIX);
|
||||
return isSelfPath(uri, INTERNAL_PREFIX)
|
||||
|| isSelfPath(uri, ACTUATOR_PREFIX)
|
||||
|| isSelfPath(uri, API_DOCS_PREFIX)
|
||||
|| isSelfPath(uri, SWAGGER_UI_PREFIX)
|
||||
|| (uri != null && uri.equals(SWAGGER_UI_HTML));
|
||||
}
|
||||
|
||||
private static boolean isSelfPath(String uri, String prefix) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.yunhai.platform.gateway.route;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -12,6 +14,7 @@ import java.util.Map;
|
||||
* 网关内部管理端点(T0.10):/api/internal/** 属网关自身路径,收网后仅容器网内可达,
|
||||
* 不注册进 open_api_route(注册表只治理 /{target}/oapi/**)。
|
||||
*/
|
||||
@Tag(name = "GatewayAdmin", description = "网关治理:路由旋钮 / 目标地址 / 访问台账")
|
||||
@RestController
|
||||
@RequestMapping("/api/internal")
|
||||
public class RouteRefreshController {
|
||||
@@ -20,6 +23,7 @@ public class RouteRefreshController {
|
||||
private RouteRegistry routeRegistry;
|
||||
|
||||
/** 清缓存重 load,返回加载数(routes = 路由条目,targets = DB 地址行,E11 同端点重载) */
|
||||
@Operation(operationId = "GatewayAdmin.refresh", summary = "热重载路由注册表与目标地址")
|
||||
@PostMapping("/routes/refresh")
|
||||
public Map<String, Object> refresh() {
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
|
||||
@@ -106,6 +106,7 @@ function toast(msg, isErr) {
|
||||
setTimeout(function(){ t.className = ''; }, isErr ? 4200 : 2200);
|
||||
}
|
||||
|
||||
/* 管理面响应 = 平台信封 {code,msg,data}(yh-api-std 统一包装);成功时把 data 交给调用方 */
|
||||
function api(method, url, body) {
|
||||
var opt = { method: method, headers: {} };
|
||||
if (body !== undefined) {
|
||||
@@ -113,9 +114,10 @@ function api(method, url, body) {
|
||||
opt.body = JSON.stringify(body);
|
||||
}
|
||||
return fetch(url, opt).then(function(r) {
|
||||
return r.json().catch(function(){ return {}; }).then(function(j) {
|
||||
if (!r.ok) { throw new Error(j.msg || ('HTTP ' + r.status)); }
|
||||
return j;
|
||||
if (!r.ok) { throw new Error('HTTP ' + r.status); }
|
||||
return r.json().then(function(j) {
|
||||
if (j.code !== 0) { throw new Error(j.msg || ('业务错误 code=' + j.code)); }
|
||||
return j.data;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -128,7 +130,8 @@ function esc(s) {
|
||||
|
||||
/* ---------- 路由表 ---------- */
|
||||
function loadRoutes() {
|
||||
return api('GET', 'routes').then(function(rows) {
|
||||
return api('GET', 'routes?pageIndex=1&pageSize=200').then(function(pg) {
|
||||
var rows = (pg && pg.content) || [];
|
||||
var tb = $('routeT').tBodies[0];
|
||||
tb.innerHTML = rows.map(function(r) { return routeRow(r); }).join('');
|
||||
});
|
||||
@@ -167,13 +170,14 @@ function saveRoute(btn) {
|
||||
else { body[k] = el.value; }
|
||||
});
|
||||
api('PUT', 'routes/' + tr.dataset.id, body)
|
||||
.then(function(j) { toast('已保存,注册表加载 ' + j.loaded + ' 条路由'); })
|
||||
.then(function(d) { toast('已保存,注册表加载 ' + d.loaded + ' 条路由'); })
|
||||
.catch(function(e) { toast('保存失败:' + e.message, true); });
|
||||
}
|
||||
|
||||
/* ---------- 目标表 ---------- */
|
||||
function loadTargets() {
|
||||
return api('GET', 'targets').then(function(rows) {
|
||||
return api('GET', 'targets?pageIndex=1&pageSize=200').then(function(pg) {
|
||||
var rows = (pg && pg.content) || [];
|
||||
var tb = $('tgtT').tBodies[0];
|
||||
tb.innerHTML = rows.map(function(t) {
|
||||
return '<tr data-id="' + t.id + '">'
|
||||
@@ -195,23 +199,25 @@ function saveTarget(btn) {
|
||||
body[k] = el.type === 'checkbox' ? el.checked : el.value;
|
||||
});
|
||||
api('PUT', 'targets/' + tr.dataset.id, body)
|
||||
.then(function(j) { toast('已保存,目标表加载 ' + j.targets + ' 条'); })
|
||||
.then(function(d) { toast('已保存,目标表加载 ' + d.targets + ' 条'); })
|
||||
.catch(function(e) { toast('保存失败:' + e.message, true); });
|
||||
}
|
||||
|
||||
/* ---------- 台账 ---------- */
|
||||
function loadLogs() {
|
||||
var q = 'logs?limit=' + (parseInt($('qLimit').value || '50', 10) || 50);
|
||||
var size = parseInt($('qLimit').value || '50', 10) || 50;
|
||||
var q = 'logs?pageIndex=1&pageSize=' + size;
|
||||
if ($('qTarget').value.trim()) q += '&targetCode=' + encodeURIComponent($('qTarget').value.trim());
|
||||
if ($('qCaller').value.trim()) q += '&callerAppKey=' + encodeURIComponent($('qCaller').value.trim());
|
||||
return api('GET', q).then(function(j) {
|
||||
$('logTotal').textContent = '共 ' + j.total + ' 条';
|
||||
return api('GET', q).then(function(pg) {
|
||||
$('logTotal').textContent = '共 ' + pg.total + ' 条';
|
||||
var rows = (pg && pg.content) || [];
|
||||
var tb = $('logT').tBodies[0];
|
||||
if (!j.rows || !j.rows.length) {
|
||||
if (!rows.length) {
|
||||
tb.innerHTML = '<tr><td colspan="9" class="idle">无匹配记录</td></tr>';
|
||||
return;
|
||||
}
|
||||
tb.innerHTML = j.rows.map(function(l) {
|
||||
tb.innerHTML = rows.map(function(l) {
|
||||
var st = l.status;
|
||||
var cls = st >= 500 ? 'st5xx' : (st >= 400 ? 'st4xx' : 'st2');
|
||||
return '<tr>'
|
||||
@@ -230,7 +236,7 @@ function loadLogs() {
|
||||
/* ---------- 顶部 ---------- */
|
||||
function reloadRegistry() {
|
||||
api('POST', '../routes/refresh')
|
||||
.then(function(j) { toast('注册表已重载:路由 ' + j.loaded + ' 条 / 目标 ' + j.targets + ' 条'); })
|
||||
.then(function(d) { toast('注册表已重载:路由 ' + d.loaded + ' 条 / 目标 ' + d.targets + ' 条'); })
|
||||
.catch(function(e) { toast('重载失败:' + e.message, true); });
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -30,7 +29,12 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
||||
|
||||
/**
|
||||
* 治理小页后端(N02 TDD):page/routes/targets/logs 查询形态 + 路由/目标行内编辑的
|
||||
* 值域闸(非法值 400 且不落库)+ 写后热重载调用。MockMvc standalone + mock 仓储/注册表。
|
||||
* 值域闸(非法值不落库)+ 写后热重载调用。MockMvc standalone + mock 仓储/注册表。
|
||||
* <p>
|
||||
* <b>契约说明(B3/B4 后)</b>:查询接口返回 {@code BasePageResult}({@code {total, content, ...}}),
|
||||
* 非法入参返回 <b>HTTP 200</b> + {@code {code:2, msg}}(平台规范)。standalone 模式不装配
|
||||
* {@code yh-api-std} 的 {@code ApiResponseAdvice},故成功响应在本测试中保持裸结构;
|
||||
* 线上由 advice 统一再包一层 {@code {code,msg,data}}(分页体落在 {@code data})。
|
||||
*/
|
||||
class InternalAdminControllerTest {
|
||||
|
||||
@@ -51,8 +55,10 @@ class InternalAdminControllerTest {
|
||||
controller.setRepos(routeRepo, targetRepo, logRepo, registry);
|
||||
mvc = MockMvcBuilders.standaloneSetup(controller).build();
|
||||
|
||||
when(routeRepo.findAll()).thenReturn(new ArrayList<>(Arrays.asList(route(1L))));
|
||||
when(targetRepo.findAll()).thenReturn(new ArrayList<>(Arrays.asList(target(5L))));
|
||||
when(routeRepo.findAll(any(Pageable.class)))
|
||||
.thenReturn(new PageImpl<>(Arrays.asList(route(1L))));
|
||||
when(targetRepo.findAll(any(Pageable.class)))
|
||||
.thenReturn(new PageImpl<>(Arrays.asList(target(5L))));
|
||||
when(registry.reload()).thenReturn(22);
|
||||
when(registry.reloadTargets()).thenReturn(13);
|
||||
when(logRepo.pageLatest(any(), any(), any(Pageable.class)))
|
||||
@@ -73,8 +79,19 @@ class InternalAdminControllerTest {
|
||||
void routes_listSorted() throws Exception {
|
||||
mvc.perform(get("/api/internal/admin/routes"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].targetCode").value("userbase"))
|
||||
.andExpect(jsonPath("$[0].pathPattern").value("/oapi/user/**"));
|
||||
.andExpect(jsonPath("$.total").value(1))
|
||||
.andExpect(jsonPath("$.pageIndex").value(1))
|
||||
.andExpect(jsonPath("$.content[0].targetCode").value("userbase"))
|
||||
.andExpect(jsonPath("$.content[0].pathPattern").value("/oapi/user/**"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void targets_paged() throws Exception {
|
||||
mvc.perform(get("/api/internal/admin/targets?pageIndex=1&pageSize=20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.total").value(1))
|
||||
.andExpect(jsonPath("$.content[0].targetCode").value("notice"));
|
||||
verify(targetRepo).findAll(Mockito.<Pageable>argThat(p -> p.getPageSize() == 20 && p.getPageNumber() == 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -90,34 +107,37 @@ class InternalAdminControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void routeUpdate_illegalAuthMode_400AndNoSave() throws Exception {
|
||||
void routeUpdate_illegalAuthMode_bizErrorAndNoSave() throws Exception {
|
||||
when(routeRepo.findById(1L)).thenReturn(java.util.Optional.of(route(1L)));
|
||||
mvc.perform(put("/api/internal/admin/routes/1")
|
||||
.contentType("application/json")
|
||||
.content("{\"authMode\":\"OPEN\"}"))
|
||||
.andExpect(status().isBadRequest())
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(2))
|
||||
.andExpect(jsonPath("$.msg").value(org.hamcrest.Matchers.containsString("authMode")));
|
||||
verify(routeRepo, never()).save(any());
|
||||
verify(registry, never()).reload();
|
||||
}
|
||||
|
||||
@Test
|
||||
void routeUpdate_negativeTtl_400() throws Exception {
|
||||
void routeUpdate_negativeTtl_bizError() throws Exception {
|
||||
when(routeRepo.findById(1L)).thenReturn(java.util.Optional.of(route(1L)));
|
||||
mvc.perform(put("/api/internal/admin/routes/1")
|
||||
.contentType("application/json")
|
||||
.content("{\"cacheTtlSeconds\":-1}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(2));
|
||||
verify(routeRepo, never()).save(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void routeUpdate_missingRow_400() throws Exception {
|
||||
void routeUpdate_missingRow_bizError() throws Exception {
|
||||
when(routeRepo.findById(99L)).thenReturn(java.util.Optional.empty());
|
||||
mvc.perform(put("/api/internal/admin/routes/99")
|
||||
.contentType("application/json")
|
||||
.content("{\"enabled\":false}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -133,12 +153,13 @@ class InternalAdminControllerTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void targetUpdate_blankBaseUrl_400NoSave() throws Exception {
|
||||
void targetUpdate_blankBaseUrl_bizErrorNoSave() throws Exception {
|
||||
when(targetRepo.findById(5L)).thenReturn(java.util.Optional.of(target(5L)));
|
||||
mvc.perform(put("/api/internal/admin/targets/5")
|
||||
.contentType("application/json")
|
||||
.content("{\"baseUrl\":\" \"}"))
|
||||
.andExpect(status().isBadRequest());
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.code").value(2));
|
||||
verify(targetRepo, never()).save(any());
|
||||
}
|
||||
|
||||
@@ -147,10 +168,18 @@ class InternalAdminControllerTest {
|
||||
mvc.perform(get("/api/internal/admin/logs?targetCode=notice&callerAppKey=100001&limit=10"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.total").value(1))
|
||||
.andExpect(jsonPath("$.rows[0].targetCode").value("notice"));
|
||||
.andExpect(jsonPath("$.content[0].targetCode").value("notice"));
|
||||
verify(logRepo).pageLatest(eq("notice"), eq("100001"), any(Pageable.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void logs_pageIndexAndSize() throws Exception {
|
||||
mvc.perform(get("/api/internal/admin/logs?pageIndex=2&pageSize=20"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.total").value(1));
|
||||
verify(logRepo).pageLatest(any(), any(), Mockito.argThat(p -> p.getPageSize() == 20 && p.getPageNumber() == 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
void logs_overLimitCappedTo500() throws Exception {
|
||||
mvc.perform(get("/api/internal/admin/logs?limit=9999"))
|
||||
|
||||
Reference in New Issue
Block a user