Merge pull request #3994 from dataease/pr@dev@feat_report_img_down_api

feat(定时报告): 下载定时报告图片api
This commit is contained in:
fit2cloud-chenyw 2022-12-02 09:04:18 +08:00 committed by GitHub
commit 0aabb23165
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 257 additions and 10 deletions

View File

@ -0,0 +1,24 @@
package io.dataease.auth.annotation;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DeRateLimiter {
long DEFAULT_REQUEST = 2;
@AliasFor("max") long value() default DEFAULT_REQUEST;
@AliasFor("value") long max() default DEFAULT_REQUEST;
String key() default "";
long timeout() default 500;
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}

View File

@ -0,0 +1,55 @@
package io.dataease.auth.aop;
import cn.hutool.core.util.StrUtil;
import io.dataease.auth.annotation.DeRateLimiter;
import io.dataease.auth.service.DeLimitService;
import io.dataease.commons.utils.IPUtils;
import io.dataease.commons.utils.ServletUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class DeRateLimiterHandler {
private final static String SEPARATOR = ":";
@Resource
private DeLimitService deLimitService;
@Around(value = "@annotation(io.dataease.auth.annotation.DeRateLimiter)")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
DeRateLimiter rateLimiter = AnnotationUtils.findAnnotation(method, DeRateLimiter.class);
if (rateLimiter != null) {
String key = rateLimiter.key();
if (StrUtil.isBlank(key)) {
key = method.getDeclaringClass().getName() + StrUtil.DOT + method.getName();
}
key = key + SEPARATOR + IPUtils.get();
long max = rateLimiter.max();
long timeout = rateLimiter.timeout();
TimeUnit timeUnit = rateLimiter.timeUnit();
Boolean limited = deLimitService.checkRestricted(key, max, timeout, timeUnit);
if (limited) {
String msg = "The current API [%s] is limited, please try again later!";
String requestURI = ServletUtils.request().getRequestURI();
throw new RuntimeException(String.format(msg, requestURI));
}
}
return point.proceed();
}
}

View File

@ -0,0 +1,8 @@
package io.dataease.auth.service;
import java.util.concurrent.TimeUnit;
public interface DeLimitService {
Boolean checkRestricted(String key, long max, long timeout, TimeUnit timeUnit);
}

View File

@ -0,0 +1,51 @@
package io.dataease.auth.service.impl;
import io.dataease.auth.service.DeLimitService;
import io.dataease.commons.condition.RedisStatusCondition;
import io.dataease.commons.utils.LogUtil;
import org.slf4j.Logger;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.Instant;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Conditional({RedisStatusCondition.class})
@Component
@Primary
public class RedisLimitServiceImpl implements DeLimitService {
Logger log = LogUtil.getLogger();
private final static String REDIS_LIMIT_KEY_PREFIX = "limit:";
@Resource
private RedisScript<Long> limitRedisScript;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public Boolean checkRestricted(String key, long max, long timeout, TimeUnit timeUnit) {
key = REDIS_LIMIT_KEY_PREFIX + key;
long ttl = timeUnit.toMillis(timeout);
long now = Instant.now().toEpochMilli();
long expired = now - ttl;
Long executeTimes = stringRedisTemplate.execute(limitRedisScript, Collections.singletonList(key), now + "", ttl + "", expired + "", max + "");
if (executeTimes != null) {
if (executeTimes == 0) {
log.error("【{}】在单位时间 {} 毫秒内已达到访问上限,当前接口上限 {}", key, ttl, max);
return true;
} else {
log.info("【{}】在单位时间 {} 毫秒内访问 {} 次", key, ttl, executeTimes);
return false;
}
}
return false;
}
}

View File

@ -0,0 +1,26 @@
package io.dataease.auth.service.impl;
import com.google.common.util.concurrent.RateLimiter;
import io.dataease.auth.service.DeLimitService;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Service
public class StandaloneLimitServiceImpl implements DeLimitService {
private static ConcurrentHashMap<String, RateLimiter> RATE_LIMITER = new ConcurrentHashMap<>();
@Override
public Boolean checkRestricted(String key, long max, long timeout, TimeUnit timeUnit) {
RateLimiter rateLimiter = null;
if (!RATE_LIMITER.containsKey(key)) {
RATE_LIMITER.put(key, RateLimiter.create(max));
}
rateLimiter = RATE_LIMITER.get(key);
return !rateLimiter.tryAcquire(timeout, timeUnit);
}
}

View File

@ -6,10 +6,14 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.scripting.support.ResourceScriptSource;
@Conditional({RedisStatusCondition.class})
@ -36,4 +40,12 @@ public class RedisConfig {
return container;
}
@Bean
public RedisScript<Long> limitRedisScript() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/limit.lua")));
redisScript.setResultType(Long.class);
return redisScript;
}
}

View File

@ -1,8 +1,10 @@
package io.dataease.plugins.server;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ArrayUtil;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.dataease.auth.annotation.DeRateLimiter;
import io.dataease.auth.api.dto.CurrentUserDto;
import io.dataease.commons.exception.DEException;
import io.dataease.commons.model.excel.ExcelSheetModel;
@ -27,6 +29,9 @@ import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.*;
import org.springframework.util.Base64Utils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.util.HtmlUtils;
import springfox.documentation.annotations.ApiIgnore;
@ -150,7 +155,45 @@ public class XEmailTaskServer {
return xpackEmailCreate;
}
@PostMapping("/preview")
@DeRateLimiter
@PostMapping(value = "/screenshot", produces = {MediaType.IMAGE_JPEG_VALUE, MediaType.IMAGE_PNG_VALUE})
public ResponseEntity<ByteArrayResource> screenshot(@RequestBody XpackEmailViewRequest request) {
EmailXpackService emailXpackService = SpringContextUtil.getBean(EmailXpackService.class);
String url = ServletUtils.domain() + "/#/previewScreenShot/" + request.getPanelId() + "/true";
byte[] bytes = null;
try {
String currentToken = ServletUtils.getToken();
Future<?> future = priorityExecutor.submit(() -> {
try {
return emailXpackService.print(url, currentToken, buildPixel(request.getPixel()));
} catch (Exception e) {
LogUtil.error(e.getMessage(), e);
DEException.throwException("预览失败,请联系管理员");
}
return null;
}, 0);
Object object = future.get();
if (ObjectUtils.isNotEmpty(object)) {
bytes = (byte[]) object;
if (ArrayUtil.isNotEmpty(bytes)) {
String fileName = request.getPanelId() + ".jpeg";
ByteArrayResource bar = new ByteArrayResource(bytes);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
ContentDisposition contentDisposition = ContentDisposition.parse("attachment; filename=" + URLEncoder.encode(fileName, "UTF-8"));
headers.setContentDisposition(contentDisposition);
return new ResponseEntity(bar, headers, HttpStatus.OK);
}
}
} catch (Exception e) {
LogUtil.error(e.getMessage(), e);
DEException.throwException("预览失败,请联系管理员");
}
return null;
}
@PostMapping(value = "/preview")
public String preview(@RequestBody XpackEmailViewRequest request) {
EmailXpackService emailXpackService = SpringContextUtil.getBean(EmailXpackService.class);
String panelId = request.getPanelId();
@ -159,7 +202,6 @@ public class XEmailTaskServer {
String url = ServletUtils.domain() + "/#/previewScreenShot/" + panelId + "/true";
String token = ServletUtils.getToken();
String fileId = null;
try {
Future<?> future = priorityExecutor.submit(() -> {
try {
@ -172,19 +214,21 @@ public class XEmailTaskServer {
}, 0);
Object object = future.get();
if (ObjectUtils.isNotEmpty(object)) {
fileId = object.toString();
byte[] bytes = (byte[]) object;
String baseCode = Base64Utils.encodeToString(bytes);
String imageUrl = "data:image/jpeg;base64," + baseCode;
String html = "<div>" +
content +
"<img style='width: 100%;' id='" + panelId + "' src='" + imageUrl + "' />" +
"</div>";
return html;
}
} catch (Exception e) {
LogUtil.error(e.getMessage(), e);
DEException.throwException("预览失败,请联系管理员");
}
String imageUrl = "/system/ui/image/" + fileId;
String html = "<div>" +
content +
"<img style='width: 100%;' id='" + panelId + "' src='" + imageUrl + "' />" +
"</div>";
return html;
return null;
}

View File

@ -0,0 +1,27 @@
-- 下标从 1 开始 获取key
local key = KEYS[1]
-- 下标从 1 开始 获取参数
local now = tonumber(ARGV[1]) -- 当前时间错
local ttl = tonumber(ARGV[2]) -- 有效
local expired = tonumber(ARGV[3]) --
local max = tonumber(ARGV[4])
-- 清除过期的数据
-- 移除指定分数区间内的所有元素expired 即已经过期的 score
-- 根据当前时间毫秒数 - 超时毫秒数,得到过期时间 expired
redis.call('zremrangebyscore', key, 0, expired)
-- 获取 zset 中的当前元素个数
local current = tonumber(redis.call('zcard', key))
local next = current + 1
if next > max then
-- 达到限流大小 返回 0
return 0;
else
-- 往 zset 中添加一个值、得分均为当前时间戳的元素,[value,score]
redis.call("zadd", key, now, now)
-- 每次访问均重新设置 zset 的过期时间,单位毫秒
redis.call("pexpire", key, ttl)
return next
end