Skip to content

Commit

Permalink
feat(定时报告): 下载定时报告图片api
Browse files Browse the repository at this point in the history
  • Loading branch information
fit2cloud-chenyw committed Dec 1, 2022
1 parent b2a5cfa commit 904d2c9
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 10 deletions.
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
12 changes: 12 additions & 0 deletions backend/src/main/java/io/dataease/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
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})
Expand All @@ -36,4 +40,12 @@ public RedisMessageListenerContainer redisContainer() {
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;
}

}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -27,6 +29,9 @@
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;
Expand Down Expand Up @@ -150,7 +155,51 @@ public XpackEmailCreate queryForm(@PathVariable Long taskId) {
return xpackEmailCreate;
}

@PostMapping("/preview")
@DeRateLimiter
@GetMapping("/testApple")
public String testApple() {
return "调用api成功";
}

@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();
Expand All @@ -159,7 +208,6 @@ public String preview(@RequestBody XpackEmailViewRequest request) {
String url = ServletUtils.domain() + "/#/previewScreenShot/" + panelId + "/true";

String token = ServletUtils.getToken();
String fileId = null;
try {
Future<?> future = priorityExecutor.submit(() -> {
try {
Expand All @@ -172,19 +220,21 @@ public String preview(@RequestBody XpackEmailViewRequest request) {
}, 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;

}

Expand Down
27 changes: 27 additions & 0 deletions backend/src/main/resources/scripts/limit.lua
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 904d2c9

Please sign in to comment.