359 lines
13 KiB
Markdown
359 lines
13 KiB
Markdown
# `getHitRatio` 接口分析
|
||
|
||
## 1. 接口概述
|
||
|
||
- **路由**: `GET /statistics/getHitRatio`
|
||
- **所在类**: `StatisticsController`
|
||
- **方法**: `getHitRatio(QueryParam param)`
|
||
- **注解**: `@StaticQueryCpCheckByDomain`(静态查询CP域名校验)
|
||
- **返回**: `JSONObject`,包含 `errCode`、`data`(命中率列表)、`reqUnit`("%")
|
||
- **功能**: 查询命中率数据,以时间维度返回各时间点的命中率百分比
|
||
|
||
---
|
||
|
||
## 2. 输入参数(`QueryParam`)
|
||
|
||
| 参数名 | 类型 | 说明 |
|
||
|--------|------|------|
|
||
| `cpId` | String | 企业ID |
|
||
| `domainNames` | List\<String\> | 域名列表 |
|
||
| `product` | String | 产品类型(如 "all"、具体产品ID) |
|
||
| `granular` | String | 时间粒度(minute/hour/day) |
|
||
| `providers` | List\<String\> | 加速厂商/平台列表 |
|
||
| `affectAreas` | List\<String\> | 加速区域/省份 |
|
||
| `isps` | List\<String\> | 运营商列表 |
|
||
| `operator` | String | 运营商(具体值) |
|
||
| `productId` | String | 订购ID |
|
||
| `startTime` | String | 开始时间(yyyy-MM-dd HH:mm) |
|
||
| `endTime` | String | 结束时间(yyyy-MM-dd HH:mm) |
|
||
| `userType` | String | 用户类型(省代码,SA_PRV角色使用) |
|
||
| `cpIds` | List\<String\> | 企业ID列表(内部使用) |
|
||
| `historyCpIdsByDomains` | List\<String\> | 按域名的历史企业ID |
|
||
|
||
---
|
||
|
||
## 3. 校验流程
|
||
|
||
### 3.1 注解级校验
|
||
|
||
`@StaticQueryCpCheckByDomain` — 在调用方法前进行静态查询CP域名的权限校验(切面/拦截器实现),确保当前用户有权限查询所提供域名的数据。
|
||
|
||
### 3.2 `paramVerify(param, roleId)` — 核心参数校验
|
||
|
||
#### 3.2.1 用户登录校验
|
||
|
||
- 通过 `getUser()` 获取当前用户,为 null 则返回错误:"无用户登录!"
|
||
|
||
#### 3.2.2 加速厂商(平台)校验
|
||
|
||
- 调用 `validServicePlatform(param)`:
|
||
- `provider` 为 `"*"` 或 `"all"` → 重置为 `"all"`,返回 true
|
||
- 否则检查 `ServicePlatformEnum.getByCode(provider)` 是否存在,不存在返回错误
|
||
|
||
#### 3.2.3 带宽单位进制校验
|
||
|
||
- `unitScale < 1` → 返回错误:"所选带宽单位进制不规范!"
|
||
|
||
#### 3.2.4 日期时间校验
|
||
|
||
**默认行为**:如果 `startTime` 或 `endTime` 为空,则默认当天 00:00 ~ 23:59。
|
||
|
||
**日期参数明确时**:
|
||
|
||
| 条件 | 错误信息 |
|
||
|------|---------|
|
||
| 结束时间在未来1小时之后 | "只能查询到1小时前的数据" |
|
||
| 开始时间距今超过5年 | "只能查询五年内的数据" |
|
||
| 结束时间距今超过2分钟 且 跨度超过90天 | "跨度不能超过90天" |
|
||
| 超过1年 且 粒度为"小时" | "小时粒度数据仅支持1年内数据查询" |
|
||
|
||
**粒度与时间跨度校验**:
|
||
|
||
| 时间跨度条件 | 支持的粒度 |
|
||
|------------|-----------|
|
||
| ≤2分钟 且 距今≤60分钟,或 跨度=1分钟 | minute / hour |
|
||
| ≤2分钟 且 距今>60分钟 | hour / day |
|
||
| >2分钟 且 ≤31天 | hour / day |
|
||
| >31天 | day(仅支持按天) |
|
||
|
||
#### 3.2.5 角色相关校验
|
||
|
||
根据 `roleId`(`SecurityUserUtil.getRoleId()`)进行不同处理:
|
||
|
||
**角色 = ROLE_CROP(企业门户)**:
|
||
|
||
- 必须有且仅有一个匹配的企业
|
||
- 只能查询自己所属企业的数据(越权检查)
|
||
- `cpIds` 只含自己的 `enterpriseId`
|
||
|
||
**角色 = ROLE_MANAGER / ROLE_MANAGER_ZQ(经理类)**:
|
||
|
||
- `cpId` 不能为空且不能为 `"*"` → 错误:"请选定一个企业!"
|
||
|
||
**角色 = ROLE_OPT(运维类)**:
|
||
|
||
- `cpId` 为空或 `"*"` → `cpIds` 设为 `["all"]`(查所有)
|
||
|
||
#### 3.2.6 域名越权校验
|
||
|
||
- 获取用户有权查看的所有域名列表
|
||
- 遍历请求的 `domainNames`,必须在有权域名列表中,或在删除域名记录表中
|
||
- 若不在,且为 SA_PRV 角色,还会额外检查是否在 `STATISTICS_KEY_ENTERPRISE` 配置的企业列表中
|
||
- 不在任何一个列表中 → 错误:"has no domain in cps!"
|
||
|
||
#### 3.2.7 加速区域(省份)处理
|
||
|
||
- 区域为空 → 默认为 `["all"]`(全国)
|
||
- 区域非空 → 将省份名称转换为省份短码(`provinceRepository.findByName`)
|
||
- 特殊值 `Constants.OTHER_PROVINCE`(其他省)→ 替换为 `Constants.OTHER_PROVINCE_SHORT_CODE`
|
||
|
||
#### 3.2.8 产品类型默认值
|
||
|
||
- `"*"` → 重置为 `"all"`
|
||
|
||
#### 3.2.9 历史企业ID合并
|
||
|
||
- 若 `historyCpIdsByDomains` 非空,且 `cpIds` 首个值不是 `"*"` 或 `"all"`,则合并历史企业ID
|
||
|
||
#### 3.2.10 SA_PRV 角色的特殊处理
|
||
|
||
- 若角色为 SA_PRV(非企业门户),且用户的省份简称非空 → 设置 `param.userType = 用户省份短码`
|
||
- 否则 `userType` 设为 null
|
||
|
||
#### 3.2.11 `paramVerify` 输出
|
||
|
||
- 成功时返回 `code=Constants.OK`,附带 `param`(处理后的 `StatisticQueryParam`)和 `cpIds`
|
||
|
||
---
|
||
|
||
## 4. `getHitRatio` 方法体内的后续处理
|
||
|
||
### 4.1 `hitReqCheck(param)` — 时间粒度调整
|
||
|
||
根据开始时间和结束时间差,自动修正 `seconds` 参数(查询粒度):
|
||
|
||
```
|
||
if (开始+29天 之前于结束) → seconds = 86400(天粒度)
|
||
else if (开始+1天 之前于结束) → seconds = 3600(小时粒度)
|
||
else → seconds = 300(5分钟粒度)
|
||
```
|
||
|
||
### 4.2 `cpDomainProductAreaCheck(param)` — 域名/产品/区域默认值补全
|
||
|
||
| 字段 | 空值时的默认值 |
|
||
|------|--------------|
|
||
| `cpId` | `"all"` |
|
||
| `domainNames` | `["all"]` |
|
||
| `product` | `"all"` |
|
||
| `affectAreas` | `["all"]` |
|
||
| 多域名(size>1)| 设置 `isGenericDomain=true`,`genericDomain="domainCollect"` |
|
||
|
||
### 4.3 cpIds 非空二次检查
|
||
|
||
- 若 `cpIds` 为空或 null → 返回错误:"无法获得相关企业信息!"
|
||
|
||
### 4.4 状态码强制设值
|
||
|
||
```java
|
||
param.setStatusCodes(new ArrayList<>());
|
||
param.getStatusCodes().add("all");
|
||
```
|
||
|
||
> 注意:这里忽略了传入的 `statusCodes`,直接强制设为 `["all"]`,意味着命中率接口不看状态码筛选。
|
||
|
||
### 4.5 运营商 ISP 默认值
|
||
|
||
```java
|
||
if (CommonUtil.listIsNullOrSizeEqualZero(param.getIsps())) {
|
||
param.setIsps(Arrays.asList("all"));
|
||
}
|
||
```
|
||
|
||
### 4.6 CROP 角色特殊处理:域名转换
|
||
|
||
```java
|
||
Map<String, String> domainMap = getDomainAndCpDomainMap(param.getDomainNames(), param.getCpId());
|
||
convertDomain(domainMap, param, param.getCpId());
|
||
```
|
||
|
||
- **作用**:处理"冲突域名"场景,即 `SelfServiceDomainConfigPO` 中 `domain` 和 `cpDomain` 字段不一致的情况
|
||
- `getDomainAndCpDomainMap`:查询 `selfServiceDomainConfigDao.findByTenantIdAndCpDomainIn`,找出请求域名中哪些是冲突域名(domain ≠ cpDomain),返回 Map\<domain, cpDomain\>
|
||
- `convertDomain`:如果请求的所有域名都在冲突域名 Map 中,则用转换后的域名列表替换
|
||
|
||
---
|
||
|
||
## 5. 参数转换工厂 `StatisticParamFactory`
|
||
|
||
### 5.1 `getWebRequest2(param)` — 构建请求数统计参数
|
||
|
||
```java
|
||
getByWebStatisticParam(param)
|
||
.metric(StatisticEnum.PARAMMETRIC.REQ.getValue()) // "req"
|
||
.isps(param.getIsps())
|
||
.dimensions([DOMAIN, TIME, AREA])
|
||
.needParamSum(15)
|
||
```
|
||
|
||
**`getByWebStatisticParam(param)` 内部逻辑**:
|
||
|
||
| 字段 | 转换逻辑 |
|
||
|------|--------|
|
||
| `cpIds` | `cpId` 含逗号则 split,否则单值;历史域名则用 `historyCpIdsByDomains` |
|
||
| `domainNames` | 多个域名设 `genericDomain=true` |
|
||
| `productIds` | `"*"/空`→`all`;`"11"`→`[0,1,2]`(点播);`"12"`→`[5,6]`(直播);`"13"`→`[8,9]`(全站);`"14"`→`[7]`(超低时延);其他→原值 |
|
||
| `areas` | 空→`["all"]`;否则传入 |
|
||
| `providers` | 空或不匹配→`ServicePlatformEnum.getCRSPlatformCodesNew()`;否则传入 |
|
||
| `startTime/endTime` | 格式化为 ISO_OFFSET_DATE_TIME |
|
||
| `seconds` | 根据时间跨度自动判断(见下方 `checkSeconds`) |
|
||
| `userType` | 仅当 `cpId="all"` 时设置 |
|
||
| `granular` → `seconds` | `hour`→3600;`minute`→300;`day`→86400 |
|
||
| `operator` | 非空则传入 |
|
||
| `orderId` | `productId` 非空非"*"非"all" → 设置;否则 `"all"` |
|
||
|
||
**`checkSeconds(start, end)` 自动判断粒度**:
|
||
|
||
| 条件 | seconds | 含义 |
|
||
|------|---------|------|
|
||
| 跨度 > 29天 | 86400 | 天粒度 |
|
||
| 跨度 > 1天 且 ≤29天 | 3600 | 小时粒度 |
|
||
| 跨度 ≤ 1天 | 300 | 5分钟粒度 |
|
||
|
||
### 5.2 `getWebHitreq2(param)` — 构建命中请求数统计参数
|
||
|
||
```java
|
||
getByWebStatisticParam(param)
|
||
.isps(param.getIsps())
|
||
.needParamSum(13)
|
||
```
|
||
|
||
> 与 `getWebRequest2` 的区别:**不设置 `metric`**,且 `needParamSum=13`(请求数参数 15,命中请求 13)
|
||
|
||
---
|
||
|
||
## 6. 三方接口调用
|
||
|
||
### 6.1 `HttpStateService.getHitRatio(request, hitReq)`
|
||
|
||
调用链路(`data-service/HttpStateServiceImpl`):
|
||
|
||
```
|
||
1. getHitRatio(request, hitReq)
|
||
└→ getRequest(request) // 请求数(metric=req)
|
||
└→ handleToDataProcess(url: MULTI_METRIC_URL, data: request)
|
||
└→ getHitReq(hitReq) // 命中请求数(无metric)
|
||
└→ handleToDataProcess(url: MULTI_METRIC_URL, data: hitReq)
|
||
└→ req.getData().mergeHitReqIntoThis(hit.getData()) // 合并,计算命中率
|
||
```
|
||
|
||
- 调用 `getDataIP() + MULTI_METRIC_URL`(大数据平台接口)
|
||
- 将请求数结果 `DataProcess` 和命中请求数结果 `DataProcess` 通过 `mergeHitReqIntoThis` 合并
|
||
- 合并内部逻辑:命中率 = 命中请求数 / 总请求数
|
||
|
||
### 6.2 `DATACHECK` 线程变量检查
|
||
|
||
```java
|
||
if (httpStateService.DATACHECK.get() != null) {
|
||
resultMap.put("errCode", Constants.ERROR);
|
||
resultMap.put("error", "查询失败");
|
||
httpStateService.DATACHECK.remove();
|
||
return new JSONObject(resultMap);
|
||
}
|
||
```
|
||
|
||
> `DATACHECK` 是一个 `ThreadLocal<Boolean>`,三方接口内部可能将其设置为非 null 以标记查询失败。接口返回前必须清理。
|
||
|
||
---
|
||
|
||
## 7. 响应数据处理
|
||
|
||
### 7.1 数据转换逻辑
|
||
|
||
```java
|
||
hitRatioList = vo.getData().getDomains().get(0).getTimes().stream()
|
||
.sorted((t1, t2) -> t1.getTime().compareTo(t2.getTime())) // 时间升序
|
||
.map(t -> {
|
||
StatisticResult temp = new StatisticResult();
|
||
// 根据粒度决定时间格式
|
||
formatter = DAY粒度 ? "yyyy-MM-dd" : "yyyy-MM-dd HH:mm"
|
||
temp.setName(formatter.format(解析(t.getTime())))
|
||
// 原始值 * 100,转百分比,小数保留2位
|
||
temp.setValue(UnitConverUtil.getDecimal(t.getProvinces().get(0).getValue().doubleValue() * 100))
|
||
return temp;
|
||
}).collect(Collectors.toList());
|
||
```
|
||
|
||
### 7.2 最终响应结构
|
||
|
||
```json
|
||
{
|
||
"errCode": 0,
|
||
"data": [
|
||
{ "name": "2026-03-27 10:00", "value": 85.23 },
|
||
{ "name": "2026-03-27 10:05", "value": 86.45 }
|
||
],
|
||
"reqUnit": "%"
|
||
}
|
||
```
|
||
|
||
| 字段 | 说明 |
|
||
|------|------|
|
||
| `errCode` | 0=成功,其他=失败 |
|
||
| `data[].name` | 时间字符串,格式由粒度决定(day="yyyy-MM-dd",其他="yyyy-MM-dd HH:mm") |
|
||
| `data[].value` | 命中率百分比,2位小数(如 85.23 表示 85.23%) |
|
||
| `reqUnit` | 固定为 "%" |
|
||
|
||
---
|
||
|
||
## 8. 完整调用时序
|
||
|
||
```
|
||
前端请求
|
||
↓
|
||
@StaticQueryCpCheckByDomain 注解校验
|
||
↓
|
||
paramVerify(param, roleId)
|
||
├─ 用户登录校验
|
||
├─ 加速厂商有效性校验
|
||
├─ 带宽单位进制校验
|
||
├─ 日期时间范围校验 + 粒度校验
|
||
├─ 角色相关cpId/域名越权校验
|
||
├─ 省份区域转换
|
||
└─ SA_PRV角色 userType 注入
|
||
↓
|
||
hitReqCheck(param) // 自动修正 seconds 粒度
|
||
↓
|
||
cpDomainProductAreaCheck(param) // cpId/domain/product/area 默认值
|
||
↓
|
||
cpIds 非空检查
|
||
↓
|
||
[仅CROP角色] getDomainAndCpDomainMap + convertDomain // 冲突域名转换
|
||
↓
|
||
isps 默认值 ["all"]
|
||
↓
|
||
statisticParamFactory.getWebRequest2(param) // 构建请求数参数
|
||
↓
|
||
statisticParamFactory.getWebHitreq2(param) // 构建命中请求数参数
|
||
↓
|
||
httpStateService.getHitRatio(request, hitReq)
|
||
├─ getRequest(request) → 大数据平台(请求数)
|
||
├─ getHitReq(hitReq) → 大数据平台(命中请求数)
|
||
└─ mergeHitReqIntoThis() → 计算命中率 = 命中/请求
|
||
↓
|
||
数据排序 + 时间格式化 + *100 转百分比
|
||
↓
|
||
返回 JSONObject { errCode, data[], reqUnit:"%" }
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 关键细节总结
|
||
|
||
1. **时间默认**:前后端均未传时间时,默认当天 00:00~23:59(分钟粒度数据)
|
||
2. **粒度自动推断**:不传 `granular` 时,`checkSeconds` 根据跨度自动判断(300s/3600s/86400s)
|
||
3. **statusCodes 被强制覆盖**:接口内部强制将 `statusCodes` 设为 `["all"]`,忽略前端传入值
|
||
4. **isps 默认 ["all"]**:只有 `isps` 为空时才默认全选,不为空时使用传入值
|
||
5. **CROP 角色域名转换**:仅企业门户角色会走冲突域名转换逻辑,普通角色不转换
|
||
6. **SA_PRV 的 userType**:仅当 `cpId="all"` 时才设置 `userType`(省份短码),用于限制只能查询本省数据
|
||
7. **needParamSum 参数校验**:内部通过参数个数校验来确保必填参数完整
|
||
8. **ThreadLocal 清理**:`DATACHECK` 用完必须 remove,防止线程污染
|