(improvement)(headless) Add API interface to provide data services to other applications (#561)

Co-authored-by: jolunoluo
This commit is contained in:
LXW
2023-12-21 22:10:29 +08:00
committed by GitHub
parent 7b580b7c94
commit fa38e37be3
40 changed files with 1106 additions and 55 deletions

View File

@@ -1,4 +1,4 @@
package com.tencent.supersonic.headless.query.service;
package com.tencent.supersonic.headless.query.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -7,6 +7,6 @@ import java.lang.annotation.Target;
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DataPermission {
public @interface ApiDataPermission {
}

View File

@@ -1,4 +1,4 @@
package com.tencent.supersonic.headless.query.utils;
package com.tencent.supersonic.headless.query.annotation;
import java.lang.annotation.Target;
import java.lang.annotation.RetentionPolicy;
@@ -9,6 +9,6 @@ import java.lang.annotation.Documented;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface S2SQLPermissionAnnotation {
public @interface S2SQLDataPermission {
}

View File

@@ -0,0 +1,12 @@
package com.tencent.supersonic.headless.query.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface StructDataPermission {
}

View File

@@ -0,0 +1,73 @@
package com.tencent.supersonic.headless.query.aspect;
import com.tencent.supersonic.common.pojo.Pair;
import com.tencent.supersonic.common.pojo.exception.InvalidArgumentException;
import com.tencent.supersonic.common.util.SignatureUtils;
import com.tencent.supersonic.headless.api.model.enums.AppStatusEnum;
import com.tencent.supersonic.headless.api.model.response.AppDetailResp;
import com.tencent.supersonic.headless.model.domain.AppService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
@Aspect
@Order(1)
@Slf4j
public class ApiDataAspect {
public static final String APPID = "appId";
private static final String TIMESTAMP = "timestamp";
private static final String SIGNATURE = "signature";
@Autowired
private AppService appService;
@Pointcut("@annotation(com.tencent.supersonic.headless.query.annotation.ApiDataPermission)")
private void apiPermissionCheck() {
}
@Around("apiPermissionCheck()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] objects = joinPoint.getArgs();
HttpServletRequest request = (HttpServletRequest) objects[1];
checkHeader(request);
return joinPoint.proceed();
}
private void checkHeader(HttpServletRequest request) {
String timestampStr = request.getHeader(TIMESTAMP);
String signature = request.getHeader(SIGNATURE);
String appId = request.getHeader(APPID);
if (StringUtils.isBlank(timestampStr)) {
throw new InvalidArgumentException("header中timestamp不可为空");
}
if (StringUtils.isBlank(signature)) {
throw new InvalidArgumentException("header中signature不可为空");
}
if (StringUtils.isBlank(appId)) {
throw new InvalidArgumentException("header中appId不可为空");
}
AppDetailResp appDetailResp = appService.getApp(Integer.parseInt(appId));
if (appDetailResp == null) {
throw new InvalidArgumentException("该appId对应的应用不存在");
}
if (!AppStatusEnum.ONLINE.equals(appDetailResp.getAppStatus())) {
throw new InvalidArgumentException("该应用暂时为非在线状态");
}
Pair<Boolean, String> checkResult = SignatureUtils.isValidSignature(appId, appDetailResp.getAppSecret(),
Long.parseLong(timestampStr), signature);
if (!checkResult.first) {
throw new InvalidArgumentException(checkResult.second);
}
}
}

View File

@@ -1,4 +1,4 @@
package com.tencent.supersonic.headless.query.utils;
package com.tencent.supersonic.headless.query.aspect;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
@@ -16,6 +16,7 @@ import com.tencent.supersonic.headless.model.domain.ModelService;
import com.tencent.supersonic.headless.model.domain.pojo.MetaFilter;
import com.tencent.supersonic.headless.model.domain.pojo.ModelFilter;
import com.tencent.supersonic.headless.query.service.AuthCommonService;
import com.tencent.supersonic.headless.query.utils.QueryStructUtils;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
@@ -58,7 +59,7 @@ public class S2SQLDataAspect {
@Value("${permission.data.enable:true}")
private Boolean permissionDataEnable;
@Pointcut("@annotation(com.tencent.supersonic.headless.query.utils.S2SQLPermissionAnnotation)")
@Pointcut("@annotation(com.tencent.supersonic.headless.query.annotation.S2SQLDataPermission)")
private void s2SQLPermissionCheck() {
}

View File

@@ -1,6 +1,4 @@
package com.tencent.supersonic.headless.query.utils;
import static com.tencent.supersonic.common.pojo.Constants.MINUS;
package com.tencent.supersonic.headless.query.aspect;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
@@ -17,14 +15,7 @@ import com.tencent.supersonic.headless.model.domain.DimensionService;
import com.tencent.supersonic.headless.model.domain.ModelService;
import com.tencent.supersonic.headless.model.domain.pojo.MetaFilter;
import com.tencent.supersonic.headless.query.service.AuthCommonService;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import com.tencent.supersonic.headless.query.utils.QueryStructUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
@@ -36,10 +27,21 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import static com.tencent.supersonic.common.pojo.Constants.MINUS;
@Component
@Aspect
@Slf4j
public class DataPermissionAOP {
public class StructDataAspect {
@Autowired
private QueryStructUtils queryStructUtils;
@Autowired
@@ -51,7 +53,7 @@ public class DataPermissionAOP {
@Value("${permission.data.enable:true}")
private Boolean permissionDataEnable;
@Pointcut("@annotation(com.tencent.supersonic.headless.query.service.DataPermission)")
@Pointcut("@annotation(com.tencent.supersonic.headless.query.annotation.StructDataPermission)")
public void dataPermissionAOP() {
}

View File

@@ -0,0 +1,34 @@
package com.tencent.supersonic.headless.query.rest;
import com.tencent.supersonic.headless.api.query.request.QueryApiPreviewReq;
import com.tencent.supersonic.headless.api.query.request.QueryApiReq;
import com.tencent.supersonic.headless.api.query.response.ApiQueryResultResp;
import com.tencent.supersonic.headless.query.service.ApiQueryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
@RestController
@RequestMapping("/api/semantic/apiQuery")
@Slf4j
public class ApiQueryController {
@Autowired
private ApiQueryService apiQueryService;
@PostMapping("/preview")
public ApiQueryResultResp preview(@RequestBody QueryApiPreviewReq queryApiReq) throws Exception {
return apiQueryService.preview(queryApiReq);
}
@PostMapping("/query")
public ApiQueryResultResp query(@RequestBody QueryApiReq queryApiReq, HttpServletRequest request) throws Exception {
return apiQueryService.query(queryApiReq, request);
}
}

View File

@@ -0,0 +1,16 @@
package com.tencent.supersonic.headless.query.service;
import com.tencent.supersonic.headless.api.query.request.QueryApiPreviewReq;
import com.tencent.supersonic.headless.api.query.request.QueryApiReq;
import com.tencent.supersonic.headless.api.query.response.ApiQueryResultResp;
import com.tencent.supersonic.headless.query.annotation.ApiDataPermission;
import javax.servlet.http.HttpServletRequest;
public interface ApiQueryService {
ApiQueryResultResp preview(QueryApiPreviewReq queryApiReq) throws Exception;
@ApiDataPermission
ApiQueryResultResp query(QueryApiReq queryApiReq, HttpServletRequest request) throws Exception;
}

View File

@@ -0,0 +1,135 @@
package com.tencent.supersonic.headless.query.service;
import com.google.common.collect.Lists;
import com.tencent.supersonic.auth.api.authentication.pojo.User;
import com.tencent.supersonic.common.pojo.Aggregator;
import com.tencent.supersonic.common.pojo.Constants;
import com.tencent.supersonic.common.pojo.DateConf;
import com.tencent.supersonic.common.pojo.enums.TimeDimensionEnum;
import com.tencent.supersonic.common.pojo.exception.InvalidArgumentException;
import com.tencent.supersonic.headless.api.model.pojo.Item;
import com.tencent.supersonic.headless.api.model.response.AppDetailResp;
import com.tencent.supersonic.headless.api.model.response.DimensionResp;
import com.tencent.supersonic.headless.api.model.response.MetricResp;
import com.tencent.supersonic.headless.api.model.response.QueryResultWithSchemaResp;
import com.tencent.supersonic.headless.api.query.pojo.ApiQuerySingleResult;
import com.tencent.supersonic.headless.api.query.request.QueryApiPreviewReq;
import com.tencent.supersonic.headless.api.query.request.QueryApiReq;
import com.tencent.supersonic.headless.api.query.request.QueryStructReq;
import com.tencent.supersonic.headless.api.query.response.ApiQueryResultResp;
import com.tencent.supersonic.headless.model.domain.AppService;
import com.tencent.supersonic.headless.model.domain.DimensionService;
import com.tencent.supersonic.headless.model.domain.MetricService;
import com.tencent.supersonic.headless.model.domain.pojo.DimensionFilter;
import com.tencent.supersonic.headless.query.annotation.ApiDataPermission;
import com.tencent.supersonic.headless.query.aspect.ApiDataAspect;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Api service for other apps
* The current version defaults to query metrics data.
*/
@Service
public class ApiQueryServiceImpl implements ApiQueryService {
private static final long result_size = 10000;
private AppService appService;
private MetricService metricService;
private DimensionService dimensionService;
private QueryService queryService;
public ApiQueryServiceImpl(AppService appService,
MetricService metricService,
DimensionService dimensionService,
QueryService queryService) {
this.appService = appService;
this.metricService = metricService;
this.dimensionService = dimensionService;
this.queryService = queryService;
}
@Override
public ApiQueryResultResp preview(QueryApiPreviewReq queryApiReq) throws Exception {
Item item = queryApiReq.getItem();
ApiQuerySingleResult apiQuerySingleResult = query(item, queryApiReq.getDateConf());
return ApiQueryResultResp.builder().results(Lists.newArrayList(apiQuerySingleResult)).build();
}
public ApiQuerySingleResult query(Item item, DateConf dateConf) throws Exception {
MetricResp metricResp = metricService.getMetric(item.getId());
List<Item> items = item.getRelateItems();
List<DimensionResp> dimensionResps = Lists.newArrayList();
if (!CollectionUtils.isEmpty(items)) {
List<Long> ids = items.stream().map(Item::getId).collect(Collectors.toList());
DimensionFilter dimensionFilter = new DimensionFilter();
dimensionFilter.setIds(ids);
dimensionResps = dimensionService.getDimensions(dimensionFilter);
}
QueryStructReq queryStructReq = buildQueryStructReq(dimensionResps, metricResp, dateConf);
QueryResultWithSchemaResp queryResultWithSchemaResp =
queryService.queryByStruct(queryStructReq, User.getAppUser(0));
ApiQuerySingleResult apiQuerySingleResult = new ApiQuerySingleResult();
apiQuerySingleResult.setItem(item);
apiQuerySingleResult.setResult(queryResultWithSchemaResp);
return apiQuerySingleResult;
}
@Override
@ApiDataPermission
public ApiQueryResultResp query(QueryApiReq queryApiReq, HttpServletRequest request) throws Exception {
int appId = Integer.parseInt(request.getHeader(ApiDataAspect.APPID));
AppDetailResp appDetailResp = appService.getApp(appId);
Set<Long> idsInApp = appDetailResp.getConfig().getItems().stream()
.map(Item::getId).collect(Collectors.toSet());
if (!idsInApp.containsAll(queryApiReq.getIds())) {
throw new InvalidArgumentException("查询范围超过应用申请范围, 请检查");
}
List<ApiQuerySingleResult> results = Lists.newArrayList();
Map<Long, Item> map = appDetailResp.getConfig().getItems().stream()
.collect(Collectors.toMap(Item::getId, i -> i));
for (Long id : queryApiReq.getIds()) {
Item item = map.get(id);
ApiQuerySingleResult apiQuerySingleResult = query(item, queryApiReq.getDateConf());
results.add(apiQuerySingleResult);
}
return ApiQueryResultResp.builder().results(results).build();
}
private QueryStructReq buildQueryStructReq(List<DimensionResp> dimensionResps,
MetricResp metricResp, DateConf dateConf) {
Set<Long> modelIds = dimensionResps.stream().map(DimensionResp::getModelId).collect(Collectors.toSet());
modelIds.add(metricResp.getModelId());
QueryStructReq queryStructReq = new QueryStructReq();
queryStructReq.setGroups(dimensionResps.stream()
.map(DimensionResp::getBizName).collect(Collectors.toList()));
queryStructReq.getGroups().add(0, getTimeDimension(dateConf));
Aggregator aggregator = new Aggregator();
aggregator.setColumn(metricResp.getBizName());
queryStructReq.setAggregators(Lists.newArrayList(aggregator));
queryStructReq.setDateInfo(dateConf);
queryStructReq.setModelIds(modelIds);
queryStructReq.setLimit(result_size);
return queryStructReq;
}
private String getTimeDimension(DateConf dateConf) {
if (Constants.MONTH.equals(dateConf.getPeriod())) {
return TimeDimensionEnum.MONTH.getName();
} else if (Constants.WEEK.equals(dateConf.getPeriod())) {
return TimeDimensionEnum.WEEK.getName();
} else {
return TimeDimensionEnum.DAY.getName();
}
}
}

View File

@@ -30,8 +30,9 @@ import com.tencent.supersonic.headless.model.domain.Catalog;
import com.tencent.supersonic.headless.query.persistence.pojo.QueryStatement;
import com.tencent.supersonic.headless.query.executor.QueryExecutor;
import com.tencent.supersonic.headless.query.parser.convert.QueryReqConverter;
import com.tencent.supersonic.headless.query.annotation.StructDataPermission;
import com.tencent.supersonic.headless.query.utils.QueryUtils;
import com.tencent.supersonic.headless.query.utils.S2SQLPermissionAnnotation;
import com.tencent.supersonic.headless.query.annotation.S2SQLDataPermission;
import com.tencent.supersonic.headless.query.utils.StatUtils;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
@@ -80,7 +81,7 @@ public class QueryServiceImpl implements QueryService {
}
@Override
@S2SQLPermissionAnnotation
@S2SQLDataPermission
@SneakyThrows
public Object queryBySql(QueryS2SQLReq queryS2SQLReq, User user) {
statUtils.initStatInfo(queryS2SQLReq, user);
@@ -150,7 +151,7 @@ public class QueryServiceImpl implements QueryService {
}
@Override
@DataPermission
@StructDataPermission
@SneakyThrows
public QueryResultWithSchemaResp queryByStructWithAuth(QueryStructReq queryStructCmd, User user) {
return queryByStruct(queryStructCmd, user);