# `getHitRatio` 接口分析 ## 1. 接口概述 - **路由**: `GET /statistics/getHitRatio` - **所在类**: `StatisticsController` - **方法**: `getHitRatio(QueryParam param)` - **注解**: `@StaticQueryCpCheckByDomain`(静态查询CP域名校验) - **返回**: `JSONObject`,包含 `errCode`、`data`(命中率列表)、`reqUnit`("%") - **功能**: 查询命中率数据,以时间维度返回各时间点的命中率百分比 --- ## 2. 输入参数(`QueryParam`) | 参数名 | 类型 | 说明 | |--------|------|------| | `cpId` | String | 企业ID | | `domainNames` | List\ | 域名列表 | | `product` | String | 产品类型(如 "all"、具体产品ID) | | `granular` | String | 时间粒度(minute/hour/day) | | `providers` | List\ | 加速厂商/平台列表 | | `affectAreas` | List\ | 加速区域/省份 | | `isps` | List\ | 运营商列表 | | `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\ | 企业ID列表(内部使用) | | `historyCpIdsByDomains` | List\ | 按域名的历史企业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 domainMap = getDomainAndCpDomainMap(param.getDomainNames(), param.getCpId()); convertDomain(domainMap, param, param.getCpId()); ``` - **作用**:处理"冲突域名"场景,即 `SelfServiceDomainConfigPO` 中 `domain` 和 `cpDomain` 字段不一致的情况 - `getDomainAndCpDomainMap`:查询 `selfServiceDomainConfigDao.findByTenantIdAndCpDomainIn`,找出请求域名中哪些是冲突域名(domain ≠ cpDomain),返回 Map\ - `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`,三方接口内部可能将其设置为非 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,防止线程污染