JWT 用户认证 + 拦截器 + ThreadLocal
JWT
JWT(JSON Web Token)是一种用于身份认证和信息安全传输的开放标准(RFC 7519)。它是一种基于 JSON 格式的令牌,用于在客户端与服务器之间安全地传递信息。
JWT 的结构
JWT 由三个部分组成,并用 . 连接:
- Header(头部):包含令牌的元信息,如加密算法(如 HS256、RS256)。
- Payload(负载):存储用户信息(如 userId、username),以及额外的声明(claims),如
exp(过期时间)。 - Signature(签名):使用 Header 中指定的算法,对 Header 和 Payload 进行签名,确保 JWT 的完整性。
JWT 的使用
1. 登录时生成 JWT
- 用户登录后,服务器验证用户名和密码。
- 服务器生成 JWT 并返回给客户端。
- 客户端存储 JWT(通常存放在
localStorage或HTTP Only Cookie)。
2. 客户端携带 JWT 访问资源
- 客户端在请求时将 JWT 放入
Authorization头部:Authorization: Bearer <JWT_TOKEN> - 服务器接收请求,验证 JWT 的有效性(是否篡改、是否过期)。
3. 服务器验证 JWT 并返回响应
- 服务器解码 JWT,验证签名和过期时间。
- 若 JWT 有效,则返回数据;否则返回
401 Unauthorized。
JWT 的优缺点
✅ 优点:
- 无状态认证:不需要在服务器存储会话信息,适用于分布式系统。
- 安全性:使用签名(HMAC、RSA)防止篡改。
- 可扩展性:可以自定义 payload 内容,如角色权限等。
❌ 缺点:
- 无法撤销:JWT 一旦签发,无法主动让其失效(除非使用黑名单机制)。
- 体积较大:相比传统的 Session ID,JWT 包含较多信息,占用带宽。
JWT 令牌为什么能解决集群部署,什么是集群部署?
在传统的基于会话和Cookie的身份验证方式中,会话信息通常存储在服务器的内存或数据库中。但在集群部署中,不同服务器之间没有共享的会话信息,这会导致用户在不同服务器之间切换时需要重新登录,或者需要引入额外的共享机制(如Redis),增加了复杂性和性能开销。
而JWT令牌通过在令牌中包含所有必要的身份验证和会话信息,使得服务器无需存储会话信息,从而解决了集群部署中的身份验证和会话管理问题。当用户进行登录认证后,服务器将生成一个JWT令牌并返回给客户端。客户端在后续的请求中携带该令牌,服务器可以通过对令牌进行验证和解析来获取用户身份和权限信息,而无需访问共享的会话存储。
由于JWT令牌是自包含的,服务器可以独立地对令牌进行验证,而不需要依赖其他服务器或共享存储。这使得集群中的每个服务器都可以独立处理请求,提高了系统的可伸缩性和容错性。
JWT 令牌如果泄露了,怎么解决,JWT是怎么做的?
- 及时失效令牌:当检测到JWT令牌泄露或存在风险时,可以立即将令牌标记为失效状态。服务器在接收到带有失效标记的令牌时,会拒绝对其进行任何操作,从而保护用户的身份和数据安全。
- 刷新令牌:JWT 令牌通常具有一定的有效期,过期后需要重新获取新的令牌。当检测到令牌泄露时,可以主动刷新令牌,即重新生成一个新的令牌,并将旧令牌标记为失效状态。这样,即使泄露的令牌被恶意使用,也会很快失效,减少了被攻击者滥用的风险。
- 使用黑名单:服务器可以维护一个令牌的黑名单,将泄露的令牌添加到黑名单中。在接收到令牌时,先检查令牌是否在黑名单中,如果在则拒绝操作。这种方法需要服务器维护黑名单的状态,对性能有一定的影响,但可以有效地保护泄露的令牌不被滥用。
TreadLocal
TreadLocal 有什么用
通常情况下,我们创建的变量可以被任何一个线程访问和修改。这在多线程环境中可能导致数据竞争和线程安全问题。
ThreadLocal 主要功能有两个,第一个是可以实现资源对象的线程隔离,让每个线程各用各的资源对象,避免争用引发的线程安全问题,第二个是实现了线程内的资源共享。
ThreadLocal 类允许每个线程绑定自己的值,当你创建一个 ThreadLocal 变量时,每个访问该变量的线程都会拥有一个独立的副本。线程可以通过 get() 方法获取自己线程的本地副本,或通过 set() 方法修改该副本的值,从而避免了线程安全问题。
ThreadLocal的底层原理实现
在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量,用来存储资源对象。
- 当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
- 当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
- 当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
ThreadLocal导致内存溢出问题
ThreadLocalMap 的 key 和 value 引用机制:
- key 是弱引用:ThreadLocalMap 中的 key 是 ThreadLocal 的弱引用 (
WeakReference<ThreadLocal<?>>)。 这意味着,如果 ThreadLocal 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 ThreadLocalMap 中对应的 key 变为 null。 - value 是强引用:ThreadLocalMap 中的 value 是强引用。 即使 key 被回收(变为 null),value 仍然存在于 ThreadLocalMap 中,被强引用,不会被回收。
当 ThreadLocal 实例失去强引用后,其对应的 value 仍然存在于 ThreadLocalMap 中,因为 Entry 对象强引用了它。如果线程持续存活(例如线程池中的线程),ThreadLocalMap 也会一直存在,导致 key 为 null 的 entry 无法被垃圾回收,就会造成内存泄漏。
也就是说,内存泄漏的发生需要同时满足两个条件:
- ThreadLocal 实例不再被强引用;
- 线程持续存活,导致 ThreadLocalMap 长期存在。
因此,在使用完 ThreadLocal 后,务必调用 remove() 方法。 这是最安全和最推荐的做法。
无状态的用户登录认证流程
在 Spring Boot 中,我们可以基于 JWT + 拦截器 + ThreadLocal 实现 无状态的用户登录认证,主要流程如下:
- 用户登录:提交用户名、密码,验证成功后,服务器生成 JWT 并返回给前端。
- 前端携带 JWT 访问 API:后续请求在
Authorization头中携带Bearer Token。 - 拦截器拦截请求:
- 解析 JWT,校验签名与过期时间。
- 从 JWT 中提取 用户信息,存入 ThreadLocal,供当前请求使用。
- 请求结束后清理 ThreadLocal,防止内存泄漏。
对比:
| 方案 | 优点 | 缺点 |
|---|---|---|
| JWT | 1️⃣ 无状态,可跨服务、集群扩展方便 2️⃣ 解析快,直接从 Token 获取用户信息 | 1️⃣ Token 无法撤销,必须等到过期 2️⃣ 需要客户端存储 Token |
| Session + Redis | 1️⃣ 可随时注销 Session,安全性更高 2️⃣ 可存储更多用户信息 | 1️⃣ 需要 Redis 依赖,有额外存储开销 2️⃣ 跨域复杂,Session 需要 Sticky Session 或 Redis 共享 |
试卷导出场景的工厂模式 + 策略模式实现
1. 需求分析与设计背景
首先,我分析了试卷导出的需求,我们需要支持多种文件格式(如 PDF、Word、Excel)进行试卷导出。每种格式的导出逻辑不同,但它们都遵循相同的基本流程,因此我决定结合使用 工厂模式 和 策略模式 来管理不同的导出策略。
- 策略模式:用于封装不同的导出格式(如 PDF、Word、Excel),使每个导出格式的逻辑独立,易于扩展。
- 工厂模式:用于根据用户的需求(如 PDF、Word、Excel)动态选择相应的导出策略,使得系统具有更高的灵活性。
2. 策略模式实现
我首先定义了一个 ExportStrategy 接口,所有导出格式都需要实现该接口,并提供具体的 export() 方法。每个实现类负责实现相应格式的导出逻辑。
public interface ExportStrategy {
void export(ExamPaper examPaper);
}
然后,我为每种导出格式(如 PDF、Word、Excel)创建了具体的策略类,实现了 ExportStrategy 接口。例如:
@Component
public class PdfExportStrategy implements ExportStrategy {
@Override
public void export(ExamPaper examPaper) {
System.out.println("导出试卷为 PDF 格式");
// 具体的 PDF 导出逻辑
}
}
@Component
public class WordExportStrategy implements ExportStrategy {
@Override
public void export(ExamPaper examPaper) {
System.out.println("导出试卷为 Word 格式");
// 具体的 Word 导出逻辑
}
}
@Component
public class ExcelExportStrategy implements ExportStrategy {
@Override
public void export(ExamPaper examPaper) {
System.out.println("导出试卷为 Excel 格式");
// 具体的 Excel 导出逻辑
}
}
3. 工厂模式实现
为了根据用户选择的导出格式动态选择相应的策略,我实现了一个 工厂类,该类从配置文件中加载支持的格式类型,并根据选择的格式返回对应的策略对象。
@Component
public class ExportFactory implements ApplicationContextAware {
private static Map<String, ExportStrategy> strategyPool = new ConcurrentHashMap<>();
@Autowired
private ExportTypeConfig exportTypeConfig;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
exportTypeConfig.getTypes().forEach((k, v) -> {
strategyPool.put(k, (ExportStrategy) applicationContext.getBean(v));
});
}
public ExportStrategy getExportStrategy(String exportType) {
return strategyPool.get(exportType);
}
}
在这个工厂类中,我使用了 Spring 的 ApplicationContext 来获取不同的导出策略(例如 pdfExportStrategy、wordExportStrategy、excelExportStrategy),并将它们存储在 strategyPool 中。通过传入的格式类型(如 pdf、word、excel),工厂类返回对应的策略对象。
4. 配置文件与动态配置
为了方便后期扩展,我将导出格式及对应的策略类名配置在 application.yml 文件中,使得导出格式可以灵活配置,而无需修改代码。
export:
types:
pdf: pdfExportStrategy
word: wordExportStrategy
excel: excelExportStrategy
ExportTypeConfig 类读取 application.yml 文件中的配置,并将不同的策略类映射到配置文件中的格式(如 PDF、Word、Excel)。
@Configuration
@ConfigurationProperties(prefix = "export")
public class ExportTypeConfig {
private Map<String, String> types;
public Map<String, String> getTypes() {
return types;
}
public void setTypes(Map<String, String> types) {
this.types = types;
}
}
@Configuration:标识该类是一个 Spring 配置类,用于定义 Bean。
@ConfigurationProperties(prefix = "export"):自动将配置文件中以 export 为前缀的属性注入到类的字段中,简化了配置管理。
5. 使用工厂类与策略类进行导出操作
在导出试卷的业务类中,我通过工厂类 ExportFactory 获取具体的导出策略,并调用 export() 方法来执行导出操作。
@Component
public class ExamExportService {
@Autowired
private ExportFactory exportFactory;
public void exportExamPaper(ExamPaper examPaper, String exportType) {
ExportStrategy exportStrategy = exportFactory.getExportStrategy(exportType);
if (exportStrategy == null) {
throw new IllegalArgumentException("Unsupported export type: " + exportType);
}
exportStrategy.export(examPaper);
}
}
优化计算场景的多线程计算实现
1. 完整方案
第一步:站流量分配优化,使用类似 fmincon 的优化方法。
第二步:使用 ThreadPoolExecutor 并行计算每个站的井流量分配。
第三步:汇总各站井流量分配方案并得到总能耗。
2. 采用线性规划实现站流量分配优化
import org.apache.commons.math3.optimization.GoalType;
import org.apache.commons.math3.optimization.linear.*;
import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
// 站流量分配优化类
public class FlowOptimization {
/**
* 优化站流量分配
* 通过线性规划求解最小能耗的流量分配方案
*/
public void optimizeFlow(double[] initialGuess) {
// 定义目标函数:能耗最小化,系数根据能耗计算公式设置
// 这里使用简化的线性目标函数,实际应用中需要根据具体能耗模型定义
LinearObjectiveFunction objectiveFunction = new LinearObjectiveFunction(initialGuess, 0); // 系数根据你的能耗公式定义
// 定义约束条件:例如流量限制,流量总和等
LinearConstraintSet constraints = new LinearConstraintSet();
constraints.add(new LinearConstraint(initialGuess, Relationship.LEQ, 100)); // 示例约束条件:流量 ≤ 100
// 创建线性优化求解器
SimplexSolver solver = new SimplexSolver();
// 求解优化问题,GoalType.MINIMIZE 表示最小化目标函数
PointValuePair solution = solver.optimize(objectiveFunction, constraints, GoalType.MINIMIZE, new NonNegativeConstraint(true));
// 输出优化结果
double[] optimizedFlow = solution.getPoint();
System.out.println("Optimized Flow: " + Arrays.toString(optimizedFlow));
}
}
3. 自定义线程池计算井的流量分配
public class StationEnergyOptimization {
private ThreadPoolExecutor executorService;
public StationEnergyOptimization(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit) {
// 创建一个自定义线程池
executorService = new ThreadPoolExecutor(
corePoolSize, maximumPoolSize, keepAliveTime, unit,
new LinkedBlockingQueue<>(10),
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用者线程执行
);
}
/**
* 执行多线程任务,对每个站点进行井流量分配计算
* @param stationFlow 每个站点的流量分配
*/
public void calculateEnergyDistribution(double[] stationFlow) {
List<Future<Double>> futures = new ArrayList<>();
// 提交每个井的能耗计算任务到线程池
for (double flow : stationFlow) {
futures.add(executorService.submit(() -> calculateEnergyForWell(flow)));
}
// 获取每个任务的计算结果
for (Future<Double> future : futures) {
try {
System.out.println("Energy for well: " + future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
// 关闭线程池,释放资源
executorService.shutdown();
}
/**
* 计算单个井的能耗
* @param flow 该井的流量
* @return 该井的能耗
*/
private double calculateEnergyForWell(double flow) {
// 假设能耗计算公式为 flow^2 * costFactor
double costFactor = 1.5; // 示例成本因子
return Math.pow(flow, 2) * costFactor; // 能耗计算公式
}
}
Future 是 Java 并发编程中的一个接口,用于 表示一个异步计算的结果。它提供了一些方法,可以用来查询任务的状态、等待任务执行完毕并获取执行结果等。通常,Future 是通过提交任务给线程池时获取的,线程池会返回一个 Future 对象,表示任务执行的状态和结果。
主要方法:
get():阻塞方法,用于获取异步计算的结果。如果任务还没完成,调用get()会一直等待直到任务执行完毕。get(long timeout, TimeUnit unit):带超时参数的get方法,如果任务在指定时间内未完成,将抛出TimeoutException。cancel(boolean mayInterruptIfRunning):尝试取消任务的执行,mayInterruptIfRunning参数指定是否中断正在执行的任务。isDone():判断任务是否已经完成。isCancelled():判断任务是否已被取消。
Spring AOP 记录日志
1. 介绍 OpenTracing 和 Spring AOP
-
OpenTracing:OpenTracing 是一个跨语言的标准,用于提供分布式追踪功能,能够帮助我们追踪一个请求在微服务架构中的流转过程。它主要通过生成 TraceId 来标识一个请求的生命周期,跟踪请求在各个服务间的执行情况。
-
Spring AOP:Spring AOP 是 Spring 框架提供的面向切面编程(Aspect-Oriented Programming)功能,它允许你在不修改核心业务代码的情况下,动态地在方法执行前后增加横切逻辑(如日志记录、安全检查等)。
2. 为什么将 OpenTracing 与 Spring AOP 结合使用
将 OpenTracing 与 Spring AOP 配合使用,主要是为了在方法执行的过程中 自动生成 TraceId,并在方法调用时记录相关的跟踪信息。通过 AOP,我们能够在服务方法调用的开始和结束时自动生成和传递 Trace 信息,避免手动在每个方法中加入跟踪逻辑。
3. 实现步骤
步骤 1: 添加依赖
首先,在 pom.xml 中添加所需的 OpenTracing 和 Spring AOP 相关的依赖。
步骤 2: 配置 OpenTracing
在 Spring 项目中配置 OpenTracing。这里以 Jaeger 为例,你可以在配置类中设置 Jaeger 的 Tracer:
import io.jaegertracing.Configuration;
import io.opentracing.Tracer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TracingConfig {
@Bean
public Tracer tracer() {
// 创建并返回 Jaeger Tracer
return new Configuration("co2-pipeline") // 服务名
.getTracer();
}
}
步骤 3: 创建 AOP 切面类
使用 Spring AOP 创建一个切面类,负责在方法执行时启动和关闭 OpenTracing Span,并将 TraceId 自动传播到日志中。
import io.opentracing.Span;
import io.opentracing.Tracer;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Aspect
@Component
public class TracingAspect {
private static final Logger logger = LoggerFactory.getLogger(TracingAspect.class);
@Autowired
private Tracer tracer;
// 前置通知:方法执行前
@Before("execution(* com.example.co2.service.*.*(..))")
public void startTrace(ProceedingJoinPoint joinPoint) {
// 创建 Span
Span span = tracer.buildSpan(joinPoint.getSignature().getName()).start();
// 将 TraceId 注入到日志上下文中
tracer.activateSpan(span);
logger.info("Started tracing method: {}", joinPoint.getSignature().getName());
}
// 后置通知:方法执行后
@After("execution(* com.example.co2.service.*.*(..))")
public void endTrace(ProceedingJoinPoint joinPoint) {
Span span = tracer.activeSpan();
if (span != null) {
span.finish(); // 结束 Span,记录 Trace 信息
logger.info("Ended tracing method: {}", joinPoint.getSignature().getName());
}
}
// 环绕通知:记录方法执行的时间
@Around("execution(* com.example.co2.service.*.*(..))")
public Object logMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
// 执行目标方法
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
logger.info("Method {} executed in {}ms", joinPoint.getSignature().getName(), (endTime - startTime));
return result;
}
}
步骤 4: 配置 TraceId 注入到日志中
通过 MDC(Mapped Diagnostic Context),你可以将 TraceId 注入到日志中,便于在分布式系统中查看请求的完整轨迹。
import org.slf4j.MDC;
@Before("execution(* com.example.co2.service.*.*(..))")
public void startTrace(ProceedingJoinPoint joinPoint) {
// 获取方法签名
Signature signature = joinPoint.getSignature();
Span span = tracer.buildSpan(signature.getName()).start();
// 将 TraceId 注入到 MDC,方便在日志中打印
MDC.put("traceId", span.context().toString());
tracer.activateSpan(span);
logger.info("Started tracing method: {}", signature.getName());
}
@After("execution(* com.example.co2.service.*.*(..))")
public void endTrace(ProceedingJoinPoint joinPoint) {
Span span = tracer.activeSpan();
if (span != null) {
span.finish();
MDC.remove("traceId"); // 结束后移除 TraceId
logger.info("Ended tracing method: {}", joinPoint.getSignature().getName());
}
}
4. 配置日志框架
在日志配置(例如 logback.xml)中,可以通过 MDC 输出 TraceId:
<pattern>
%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg %n
%X{traceId}
</pattern>
5. 使用 TraceId
当业务方法(例如 CO2Service 中的 calculateEnergyConsumption)被调用时,Spring AOP 会自动记录 TraceId 和请求的执行信息:
@Service
public class CO2Service {
public void calculateEnergyConsumption(String pipelineId) {
// 业务逻辑
System.out.println("Calculating energy consumption for pipeline " + pipelineId);
}
public void updateFlowStatus(String wellId, double flowRate) {
// 更新井流量状态
System.out.println("Updating flow status for well " + wellId + " with flow rate " + flowRate);
}
}
6. 总结
在 Spring AOP 与 OpenTracing 配合使用 的过程中,您可以通过以下步骤完成 TraceId 的自动生成与记录:
- 配置 OpenTracing:设置
Tracer,如使用 Jaeger 来生成 TraceId。 - 创建 AOP 切面类:通过切面(
@Aspect)定义日志记录方法,在方法执行前后生成 TraceId,记录日志。 - 将 TraceId 注入日志中:通过
MDC把 TraceId 注入到日志上下文,确保在日志中能够记录 TraceId 信息。 - 可视化追踪:结合 OpenTracing 的分布式追踪工具(如 Jaeger、Zipkin)来查看请求在各个服务中的完整执行路径,帮助排查性能瓶颈和错误。
通过这种方式,你可以轻松地将 分布式追踪 和 日志记录 融合到业务逻辑中,帮助你在微服务架构中有效追踪请求,提升系统可观察性和可维护性。
Echarts 渲染优化
1. 数据分片
数据分片的主要目的是将大规模的数据集拆分成多个小块,这样可以减少一次渲染的数据量,提高渲染速度。
实现思路:
- 将数据切分成多个小块:对于大规模的时间序列或散点数据,可以将其分成多个小段,在每次渲染时只加载当前显示区域的数据。
- 动态加载和卸载数据:随着用户缩放或滚动,动态加载或卸载数据块。只加载当前视图范围内的数据,避免一次性加载过多数据。
实现步骤:
- 计算视图区域范围:通过监听 ECharts 的
dataZoom或roam事件,获取当前视图的显示范围(如 X 轴和 Y 轴的显示区域)。 - 切割数据:根据视图的范围,选择性地加载或展示数据。例如,选择 X 轴范围内的数据进行渲染。
- 动态更新数据:当用户缩放或拖动图表时,动态更新当前视图范围内的数据,卸载不再显示的数据。
// ECharts 配置中使用数据分片
var chart = echarts.init(document.getElementById('main'));
var data = [...]; // 假设这是大量的数据
var chunkSize = 1000; // 每次渲染的最大数据量
var currentChunkIndex = 0;
function getCurrentData() {
var start = currentChunkIndex * chunkSize;
var end = Math.min((currentChunkIndex + 1) * chunkSize, data.length);
return data.slice(start, end);
}
chart.setOption({
xAxis: {
type: 'category',
data: getCurrentData().map(item => item.x) // 提取 X 轴数据
},
yAxis: {
type: 'value'
},
series: [{
data: getCurrentData().map(item => item.y), // 提取 Y 轴数据
type: 'line'
}]
});
// 监听 zoom 或 roam 事件,更新数据
chart.on('datazoom', function () {
var newRange = chart.getOption().dataZoom[0];
var start = Math.floor(newRange.start * data.length / 100);
var end = Math.floor(newRange.end * data.length / 100);
currentChunkIndex = Math.floor(start / chunkSize);
chart.setOption({
xAxis: {
data: data.slice(start, end).map(item => item.x)
},
series: [{
data: data.slice(start, end).map(item => item.y)
}]
});
});
2. 降采样算法
降采样算法用于减少渲染时需要处理的数据点数量,通过减少数据量来提高渲染速度。常见的降采样算法包括 随机采样、平均采样、最大/最小值采样、平滑采样等。
实现思路:
- 选择性抽取数据点: 通过选择一个固定的采样率,减少数据点的数量。
- 在保持数据趋势的情况下减少点数: 在图表展示时,通过一定的算法只保留重要的数据点,去除无关紧要的部分。
降采样算法示例(使用 平均采样):
- 计算每一段数据的平均值:将数据按一定的时间间隔(或数据点间隔)分段,并计算每段的平均值(或其他统计量)。
- 替换原始数据:将计算后的平均值用于绘图。
// 简单的降采样:按时间间隔计算平均值
function downSampleData(data, interval) {
var sampledData = [];
for (var i = 0; i < data.length; i += interval) {
var chunk = data.slice(i, i + interval);
var averageX = chunk.reduce(function (sum, item) { return sum + item.x; }, 0) / chunk.length;
var averageY = chunk.reduce(function (sum, item) { return sum + item.y; }, 0) / chunk.length;
sampledData.push({ x: averageX, y: averageY });
}
return sampledData;
}
var downsampledData = downSampleData(data, 5); // 每隔5个数据点取平均
chart.setOption({
xAxis: {
type: 'category',
data: downsampledData.map(item => item.x)
},
yAxis: {
type: 'value'
},
series: [{
data: downsampledData.map(item => item.y),
type: 'line'
}]
});

8770

被折叠的 条评论
为什么被折叠?



