Files
UserbaseService/src/main/java/com/yunhai/userbase/loadtestmetrics/OapiMetricsController.java
T
chenxing 3421851b6f
build / build-push (push) Failing after 20s
feat: oapi /oapi/app/list 接入学校别名 + userbase 缓存事件失效基建
别名接入(本次):
- SchoolAppOverrideService 新增上下文无关合并口 applyAlias:按单一学校覆盖
  BoaAppType/BoaApp 名称/图标/排序,覆盖字段为空回落主表真名(无别名=真实名),
  分类间/分类内按生效排序重排,口径与管理端 mergeAppVo/mergeCategoryList 一致
- listBoaAppType 新增可选 schoolId:生效学校三级兜底=显式传校(assertSchoolId
  锚点+用户归属校验,违规 code=5)→ 当前激活身份学校 → 主归属学校,均无则不合并
- userAppList 缓存 key 扩 :s: 维度:auto 字面量随用户精确枚举驱逐(8→16 条),
  显式学校后缀不可枚举靠 60s TTL 自愈;别名写口 AppAuthzChangedEvent 整清链路复用
- 新增 SchoolAppOverrideServiceApplyAliasTest(10)/AppCategoryServiceSchoolAliasTest(8),
  更新 CacheInvalidationListenerTest 16-key 断言;全量 102 用例全绿

缓存事件失效基建(前次会话实现,随本 commit 入库):
- AppAuthzChangedEvent/UserGroupRelLoader/CacheInvalidationListener 四事件
  AFTER_COMMIT 失效链路 + CacheNames 注册表(TTL/抖动/存放路由/依赖表/失效责任)
- 应用授权域/组域/用户域各写口发布失效事件(AppModule/AppRole/GroupAuthz/
  SysGroup/SysUser/Identity 等服务),配套 AppAuthzEventPublishTest 等用例
2026-09-10 11:13:30 +08:00

162 lines
5.7 KiB
Java

package com.yunhai.userbase.loadtestmetrics;
/* =============================================================================
* [LOADTEST-METRICS] 临时埋点 —— 《压测与oapi埋点统计方案.md》阶段一(方案 §2.3)
* 压测结束后整体移除:删除 loadtestmetrics 包 + 回退 pom.xml/application.yml
* 中 [LOADTEST-METRICS] 标记段 + DROP TABLE oapi_metrics。
* 移除步骤详见仓库根《埋点移除清单.md》。
* ============================================================================= */
import com.yunhai.userbase.config.Config;
import io.swagger.v3.oas.annotations.Operation;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* [LOADTEST-METRICS] oapi 热度报告查询(方案 §2.3)。<br>
* 默认只统计线上流量(is_load_test=0),压测流量单独给合计数,两者严格隔离。<br>
* 例:GET /api/oapimetrics/TopN?dimension=both&amp;days=7&amp;top=10
*/
@RestController
@RequestMapping(value = Config.API_PATH + "/oapimetrics/")
public class OapiMetricsController {
@Autowired
private OapiMetricsRepository repository;
/**
* TopN 热度报告:Top 接口 + appKey 分层分布。
*/
@Operation(summary = "[LOADTEST-METRICS] oapi热度TopN报告", operationId = "oapiMetricsTopN")
@GetMapping("/TopN")
public ReportVo topN(
@RequestParam(value = "dimension", defaultValue = "both") String dimension,
@RequestParam(value = "days", defaultValue = "1") Integer days,
@RequestParam(value = "top", defaultValue = "10") Integer top) {
int d = clamp(days == null ? 1 : days, 1, 30);
int n = clamp(top == null ? 10 : top, 1, 100);
LocalDateTime since = LocalDateTime.now().minusDays(d);
ReportVo vo = new ReportVo();
vo.setGeneratedAt(LocalDateTime.now().toString());
vo.setDays(d);
vo.setTop(n);
Object[] total = firstRow(repository.aggregateTotal(since, 0));
vo.setTotalCnt(asLong(total, 0));
vo.setTotalAvgCostMs(avg(asLong(total, 0), asLong(total, 1)));
vo.setTotalErrCnt(asLong(total, 2));
if ("uri".equals(dimension) || "both".equals(dimension)) {
List<UriRow> rows = new ArrayList<>();
for (Object[] r : repository.aggregateByUri(since, 0)) {
rows.add(toUriRow(r));
if (rows.size() >= n) {
break;
}
}
vo.setTopUris(rows);
}
if ("appkey".equals(dimension) || "both".equals(dimension)) {
List<AppKeyRow> rows = new ArrayList<>();
for (Object[] r : repository.aggregateByAppKey(since, 0)) {
AppKeyRow row = new AppKeyRow();
row.setAppKey((String) r[0]);
row.setCnt(asLong(r, 1));
row.setAvgCostMs(avg(asLong(r, 1), asLong(r, 2)));
row.setErrCnt(asLong(r, 3));
rows.add(row);
if (rows.size() >= n) {
break;
}
}
vo.setAppKeys(rows);
}
// 压测流量合计(单列口径,不与线上热度混算)
Object[] lt = firstRow(repository.aggregateTotal(since, 1));
vo.setLoadTestCnt(asLong(lt, 0));
return vo;
}
private UriRow toUriRow(Object[] r) {
UriRow row = new UriRow();
row.setUri((String) r[0]);
row.setMethod((String) r[1]);
row.setCnt(asLong(r, 2));
row.setAvgCostMs(avg(asLong(r, 2), asLong(r, 3)));
row.setErrCnt(asLong(r, 4));
return row;
}
/**
* Spring Data 对「无 group by 的多列 select」返回 Object[]{行数组} 嵌套结构
* (每个元素是一行的 Object[],不是标量),直接下标取会 ClassCastException,统一先解一层。
*/
private Object[] firstRow(Object[] arr) {
if (arr != null && arr.length > 0 && arr[0] instanceof Object[]) {
return (Object[]) arr[0];
}
return arr;
}
private long asLong(Object[] arr, int i) {
if (arr == null || i >= arr.length || arr[i] == null) {
return 0L;
}
return ((Number) arr[i]).longValue();
}
private double avg(long cnt, long total) {
return cnt <= 0 ? 0.0 : Math.round(total * 100.0 / cnt) / 100.0;
}
private int clamp(int v, int min, int max) {
return v < min ? min : (v > max ? max : v);
}
/** 报告 VO(嵌套声明,压测结束随包整体删除) */
@Data
public static class ReportVo {
private String generatedAt;
private Integer days;
private Integer top;
/** 线上总请求数 */
private Long totalCnt;
private Double totalAvgCostMs;
private Long totalErrCnt;
/** Top 接口热度 */
private List<UriRow> topUris;
/** appKey 分层分布 */
private List<AppKeyRow> appKeys;
/** 压测流量请求数(is_load_test=1 单列,不混入上面各项) */
private Long loadTestCnt;
}
@Data
public static class UriRow {
private String uri;
private String method;
private Long cnt;
private Double avgCostMs;
private Long errCnt;
}
@Data
public static class AppKeyRow {
private String appKey;
private Long cnt;
private Double avgCostMs;
private Long errCnt;
}
}