13 KiB
13 KiB
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 状态码强制设值
param.setStatusCodes(new ArrayList<>());
param.getStatusCodes().add("all");
注意:这里忽略了传入的
statusCodes,直接强制设为["all"],意味着命中率接口不看状态码筛选。
4.5 运营商 ISP 默认值
if (CommonUtil.listIsNullOrSizeEqualZero(param.getIsps())) {
param.setIsps(Arrays.asList("all"));
}
4.6 CROP 角色特殊处理:域名转换
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) — 构建请求数统计参数
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) — 构建命中请求数统计参数
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 线程变量检查
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 数据转换逻辑
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 最终响应结构
{
"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. 关键细节总结
- 时间默认:前后端均未传时间时,默认当天 00:00~23:59(分钟粒度数据)
- 粒度自动推断:不传
granular时,checkSeconds根据跨度自动判断(300s/3600s/86400s) - statusCodes 被强制覆盖:接口内部强制将
statusCodes设为["all"],忽略前端传入值 - isps 默认 ["all"]:只有
isps为空时才默认全选,不为空时使用传入值 - CROP 角色域名转换:仅企业门户角色会走冲突域名转换逻辑,普通角色不转换
- SA_PRV 的 userType:仅当
cpId="all"时才设置userType(省份短码),用于限制只能查询本省数据 - needParamSum 参数校验:内部通过参数个数校验来确保必填参数完整
- ThreadLocal 清理:
DATACHECK用完必须 remove,防止线程污染