Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 85 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,113 @@
# 数据看板
# Halo 数据统计插件

## 简介
## 📊 简介

为 Halo2 提供强大的数据可视化统计功能,支持 Umami 流量统计、网站内部数据图表(标签、分类、文章趋势、评论排行、热门文章等),可在编辑器中灵活插入
为 Halo2 提供强大的数据可视化统计功能,支持

## 致谢
- **Umami 流量统计**:实时访客、访问趋势、网站分析
- **网站内部数据图表**:标签分布、分类统计、文章发布趋势、评论排行、热门文章
- **服务状态监控**:集成 Uptime Kuma 状态页面监控
- **GitHub 统计展示**:访客可查看博主的 GitHub 活跃度
- **灵活插入**:可在编辑器中任意位置插入数据图表

**文章热力图设计借鉴微浸主题**:[了解主题](https://www.webjing.cn/archives/roLydXtD)
## ✨ 特性

- 🚀 **高性能**:内置连接池优化,解决 WebClient 连接复用问题
- 🛡️ **高可用**:自动重试机制,提升服务稳定性
- ⚡ **响应式**:支持暗色/亮色主题自动切换
- 📱 **移动端适配**:完美适配手机和平板设备
- 🔧 **易配置**:简洁的后台配置界面
- 🎨 **美观设计**:现代化 UI 设计,视觉效果出色

## 🔧 技术栈

- **后端**:Spring Boot WebFlux、Reactor Netty、Hutool
- **前端**:Vue 3、TypeScript、Rsbuild
- **UI 组件**:Halo Components
- **数据可视化**:Chart.js
- **构建工具**:Gradle、pnpm

## 🙏 致谢

**文章热力图设计灵感来源于微浸主题**:[了解主题](https://www.webjing.cn/archives/roLydXtD)

## 🌐 演示与交流

- **演示站点1**:[https://www.xhhao.com/](https://www.xhhao.com/chart)
- **演示站点2**:[https://blog.timxs.com/](https://blog.timxs.com/)
- **文档**:[https://docs.lik.cc/](https://docs.lik.cc/)
- **文档中心**:[https://docs.lik.cc/](https://docs.lik.cc/)
- **QQ 交流群**:[![QQ群](https://www.xhhao.com/upload/iShot_2025-03-03_16.03.00.png)](https://www.xhhao.com/upload/iShot_2025-03-03_16.03.00.png)
-
## 开发环境

## 📦 最近更新

### v1.0.5 (2026-02-15)

**🚀 性能优化**
- 优化 WebClient 连接池配置,解决 `PrematureCloseException` 连接复用问题
- 添加自动重试机制,提升服务调用稳定性
- 实现数据缓存机制,减少重复计算

**🔧 功能完善**
- 统一 API 响应格式,提升接口规范性
- 抽取通用常量类,提高代码可维护性
- 优化日志级别,减少生产环境日志噪音

**🎨 代码质量**
- 重构 Umami 服务实现,抽取公共方法
- 完善错误处理机制
- 添加详细的代码注释
## ⚙️ 开发环境

- Java 21+
- Node.js 18+
- pnpm
- pnpm 9+
- Gradle 8+

## 🛠️ 开发指南

## 开发
### 环境准备

```bash
# 构建插件
./gradlew build
# 克隆项目
git clone <repository-url>
cd plugin-data-statistics

# 开发前端
# 安装前端依赖
cd ui
pnpm install
pnpm dev
cd ..
```

## 构建
### 开发命令

```bash
# 后端开发(监听模式)
./gradlew build --continuous

# 前端开发(热重载)
cd ui
pnpm dev

# 构建插件
./gradlew build

# 只压缩 CSS
./gradlew minifyCss

# 清理构建
cd ..
./gradlew clean
```

构建完成后,可以在 `build/libs` 目录找到插件 jar 文件。
## 📄 许可证

[GPL-3.0](./LICENSE) © Handsome

## 💖 支持作者

## 许可证
如果你觉得这个插件对你有帮助,欢迎:

[GPL-3.0](./LICENSE) © Handsome
- Star 本项目 ⭐
- 分享给更多人 📢
- 提交 Issue 或 PR 🤝
- 捐赠支持开发者 ☕
37 changes: 37 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,37 @@ test {
useJUnitPlatform()
}

// CSS 压缩任务
tasks.register('minifyCss') {
description = '压缩 CSS 文件到 min 目录'
group = 'build'

def cssDir = file('src/main/resources/static/css')
def minDir = file('src/main/resources/static/min')

inputs.dir cssDir
outputs.dir minDir

doLast {
minDir.mkdirs()
cssDir.listFiles({ it.name.endsWith('.css') } as FileFilter)?.each { cssFile ->
def content = cssFile.text
// 简单的 CSS 压缩:移除注释、多余空白、换行
def minified = content
.replaceAll('/\\*[\\s\\S]*?\\*/', '') // 移除块注释
.replaceAll('//[^\n]*', '') // 移除行注释
.replaceAll('\\s+', ' ') // 多个空白合并为一个
.replaceAll('\\s*([{};:,>~+])\\s*', '$1') // 移除符号周围空白
.replaceAll(';\\}', '}') // 移除最后一个分号
.trim()

def minFile = new File(minDir, cssFile.name.replace('.css', '.min.css'))
minFile.text = minified
println "已压缩: ${cssFile.name} -> ${minFile.name}"
}
}
}

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
Expand All @@ -45,6 +76,12 @@ tasks.register('processUiResources', Copy) {

tasks.named('classes') {
dependsOn tasks.named('processUiResources')
dependsOn tasks.named('minifyCss') // 构建时自动压缩 CSS
}

// 确保 processResources 在 minifyCss 之后执行
tasks.named('processResources') {
dependsOn tasks.named('minifyCss')
}

halo {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
version=1.0.0
version=1.0.5
6 changes: 5 additions & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.14-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
org.gradle.daemon=true
org.gradle.jvmargs=-Xmx5120m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
org.gradle.parallel=true
org.gradle.configureondemand=true
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.xhhao.dataStatistics;

import org.springframework.stereotype.Component;

import run.halo.app.plugin.BasePlugin;
import run.halo.app.plugin.PluginContext;

Expand All @@ -10,7 +11,7 @@
* <p>Only one main class extending {@link BasePlugin} is allowed per plugin.</p>
*
* @author Handsome
* @since 1.0.0
* @since 1.0.5
*/
@Component
public class DataStatisticsPlugin extends BasePlugin {
Expand Down
53 changes: 53 additions & 0 deletions src/main/java/com/xhhao/dataStatistics/common/ApiResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.xhhao.dataStatistics.common;

import com.fasterxml.jackson.annotation.JsonInclude;

import lombok.Data;

/**
* 统一 API 响应格式
*
* @author Handsome
* @since 1.0.5
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse<T> {

private boolean success;
private String message;
private T data;
private String error;

private ApiResponse() {}

public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setSuccess(true);
response.setData(data);
return response;
}

public static <T> ApiResponse<T> success(String message, T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setSuccess(true);
response.setMessage(message);
response.setData(data);
return response;
}

public static <T> ApiResponse<T> error(String error) {
ApiResponse<T> response = new ApiResponse<>();
response.setSuccess(false);
response.setError(error);
return response;
}

public static <T> ApiResponse<T> error(String message, String error) {
ApiResponse<T> response = new ApiResponse<>();
response.setSuccess(false);
response.setMessage(message);
response.setError(error);
return response;
}
}
45 changes: 45 additions & 0 deletions src/main/java/com/xhhao/dataStatistics/common/Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.xhhao.dataStatistics.common;

import java.time.ZoneId;

/**
* 通用常量类
*
* @author Handsome
* @since 1.0.5
*/
public final class Constants {

private Constants() {
// 禁止实例化
}

/**
* 默认时区:亚洲/上海
*/
public static final String DEFAULT_TIMEZONE = "Asia/Shanghai";
public static final ZoneId DEFAULT_ZONE_ID = ZoneId.of(DEFAULT_TIMEZONE);

/**
* 缓存相关常量
*/
public static final class Cache {
private Cache() {}

/** 图表数据缓存时间(分钟) */
public static final int CHART_DATA_CACHE_MINUTES = 5;

/** Umami Token 缓存时间(小时) */
public static final int UMAMI_TOKEN_CACHE_HOURS = 24;
}

/**
* 默认 URL 常量
*/
public static final class DefaultUrls {
private DefaultUrls() {}

public static final String GITHUB_STATS_URL = "https://github-readme-stats.vercel.app/";
public static final String GITHUB_GRAPH_URL = "https://github-readme-activity-graph.vercel.app/";
}
}
51 changes: 47 additions & 4 deletions src/main/java/com/xhhao/dataStatistics/config/WebClientConfig.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,65 @@
package com.xhhao.dataStatistics.config;

import java.time.Duration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;

import io.netty.channel.ChannelOption;
import reactor.netty.http.client.HttpClient;
import reactor.netty.resources.ConnectionProvider;

/**
* WebClient 配置类
* 提供 WebClient.Builder Bean 供其他组件使用
* 提供 WebClient.Builder 和 WebClient Bean 供其他组件使用
*
* @author Handsome
* @since 1.0.0
* @since 1.0.5
*/
@Configuration
public class WebClientConfig {

/**
* 配置连接池,设置空闲超时时间避免复用已关闭的连接
*/
@Bean
public ConnectionProvider connectionProvider() {
return ConnectionProvider.builder("data-statistics")
.maxConnections(50)
.maxIdleTime(Duration.ofSeconds(20)) // 空闲连接最大存活时间
.maxLifeTime(Duration.ofSeconds(60)) // 连接最大生命周期
.pendingAcquireTimeout(Duration.ofSeconds(30))
.evictInBackground(Duration.ofSeconds(30)) // 后台定期清理过期连接
.build();
}

/**
* 配置 HttpClient
*/
@Bean
public HttpClient httpClient(ConnectionProvider connectionProvider) {
return HttpClient.create(connectionProvider)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) // 连接超时 10 秒
.responseTimeout(Duration.ofSeconds(30)); // 响应超时 30 秒
}

/**
* 提供 WebClient.Builder(用于需要自定义 baseUrl 的场景)
*/
@Bean
public WebClient.Builder webClientBuilder(HttpClient httpClient) {
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient));
}

/**
* 提供预配置的 WebClient 实例(用于不需要自定义 baseUrl 的场景)
*/
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
public WebClient webClient(WebClient.Builder webClientBuilder) {
return webClientBuilder.build();
}
}

Loading