Files
notes/work/移动杭研/AI 项目/getHitRatio接口分析.md
Docker7530 6b50219f55 1776654103
2026-04-20 11:01:47 +08:00

359 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `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 = 3005分钟粒度)
```
### 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,防止线程污染