mirror of
https://github.com/tencentmusic/supersonic.git
synced 2025-12-13 04:57:28 +00:00
(improvement)(headless) Add API interface to provide data services to other applications (#561)
Co-authored-by: jolunoluo
This commit is contained in:
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user