Compare commits

14 Commits

Author SHA1 Message Date
Timi
0625c17716 refactor Setting 2026-04-28 10:22:19 +08:00
Timi
4e7f340cc6 clear jackson method 2026-04-14 15:38:12 +08:00
Timi
3cc371c53e v1.0.2
All checks were successful
CI / build-deploy (pull_request) Successful in 27s
CI / notify-on-failure (pull_request) Has been skipped
2026-04-13 18:27:29 +08:00
Timi
b46e9079d5 v1.0.1
All checks were successful
CI / build-deploy (pull_request) Successful in 26s
CI / notify-on-failure (pull_request) Has been skipped
2026-04-13 00:09:55 +08:00
Timi
dc20070bf8 update status api 2026-04-13 00:09:48 +08:00
Timi
78163441dd v1.0.0
All checks were successful
CI / build-deploy (pull_request) Successful in 16s
CI / notify-on-failure (pull_request) Has been skipped
2026-04-09 13:14:20 +08:00
Timi
9762be1244 v1.0.0
Some checks failed
CI / build-deploy (pull_request) Failing after 51s
CI / notify-on-failure (pull_request) Successful in 0s
2026-04-09 12:08:24 +08:00
Timi
971cad7365 v1.0.0
Some checks failed
CI / build-deploy (pull_request) Failing after 5s
CI / notify-on-failure (pull_request) Successful in 0s
2026-04-09 12:03:15 +08:00
Timi
b5e9da0e9b v1.0.0
Some checks failed
CI / build-deploy (pull_request) Failing after 3s
CI / notify-on-failure (pull_request) Successful in 0s
2026-04-08 16:30:10 +08:00
Timi
34e1ac6264 remove gson 2026-04-08 12:00:52 +08:00
Timi
ef192daa93 add export config 2026-04-08 11:56:07 +08:00
Timi
8947269351 remove gson 2026-04-08 11:25:45 +08:00
Timi
b6a58b7376 update system status api, add UPS/Docker status api 2026-04-07 20:12:52 +08:00
Timi
cd7bc31e6b add upload permission 2026-01-29 16:27:52 +08:00
121 changed files with 5294 additions and 1751 deletions

333
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,333 @@
name: CI
on:
pull_request:
branches:
- master
types:
- closed
jobs:
build-deploy:
runs-on: act_runner_java
if: ${{ github.event.pull_request.merged == true }}
outputs:
deployment_status: ${{ steps.set_status.outputs.status }}
env:
JAVA_HOME: /usr/lib/jvm/java-21-openjdk
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up environment
run: |
echo "PR #${{ github.event.number }} merged into master"
echo "Source branch: ${{ github.event.pull_request.head.ref }}"
echo "Target branch: ${{ github.event.pull_request.base.ref }}"
- name: Run tests
run: |
echo "Running test suite..."
- name: Setup Maven settings
run: |
if [ -z "${{ vars.TIMI_NEXUS_USERNAME }}" ] || [ -z "${{ vars.TIMI_NEXUS_PASSWORD }}" ]; then
echo "Missing vars.TIMI_NEXUS_USERNAME or vars.TIMI_NEXUS_PASSWORD"
exit 1
fi
mkdir -p ~/.m2
cat > ~/.m2/settings.xml <<EOF
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>timi_nexus</id>
<username>${{ vars.TIMI_NEXUS_USERNAME }}</username>
<password>${{ vars.TIMI_NEXUS_PASSWORD }}</password>
</server>
</servers>
</settings>
EOF
- name: Build project
run: |
mvn -B -DskipTests clean package -P prod-linux
- name: Deploy service
if: success()
env:
CONTAINER_NAME: ${{ vars.CONTAINER_NAME }}
CONTAINER_TARGET_PATH: ${{ vars.CONTAINER_TARGET_PATH }}
MAX_RETRIES: 3
RETRY_DELAY: 10
run: |
if [ -z "$CONTAINER_NAME" ] || [ -z "$CONTAINER_TARGET_PATH" ]; then
echo "Missing production environment variables"
echo "Required: CONTAINER_NAME, CONTAINER_TARGET_PATH"
exit 1
fi
retry_command() {
local cmd="$1"
local desc="$2"
local attempt=1
while [ $attempt -le $MAX_RETRIES ]; do
echo "[$desc] Attempt $attempt/$MAX_RETRIES..."
if eval "$cmd"; then
echo "OK: $desc succeeded"
return 0
fi
echo "FAIL: $desc failed (attempt $attempt/$MAX_RETRIES)"
if [ $attempt -lt $MAX_RETRIES ]; then
echo "Retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
fi
attempt=$((attempt + 1))
done
echo "FAIL: $desc failed after $MAX_RETRIES attempts"
return 1
}
version=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)
artifact_id=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.artifactId)
jar_file="target/${artifact_id}-${version}.jar"
if [ ! -f "$jar_file" ]; then
echo "Build artifact not found: $jar_file"
exit 1
fi
if ! command -v docker >/dev/null 2>&1; then
echo "docker command not found in runner environment"
exit 1
fi
if ! docker inspect "$CONTAINER_NAME" >/dev/null 2>&1; then
echo "Docker container not found: $CONTAINER_NAME"
exit 1
fi
target_jar="${artifact_id}.jar"
container_target="${CONTAINER_TARGET_PATH%/}/$target_jar"
echo "Deploying $jar_file to container $CONTAINER_NAME:$container_target"
if ! retry_command "docker cp \"$jar_file\" \"$CONTAINER_NAME:$container_target\"" "Docker copy"; then
exit 1
fi
echo "Restarting Docker container: $CONTAINER_NAME"
if ! retry_command "docker restart \"$CONTAINER_NAME\"" "Docker restart"; then
exit 1
fi
echo "Deployment completed successfully"
- name: Create release
if: ${{ success() && startsWith(github.event.pull_request.title, 'v') }}
env:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
GITEA_SERVER_URL: ${{ github.server_url }}
GITEA_INTERNAL_URL: ${{ vars.TIMI_GITEA_INTERNAL_URL }}
GITEA_REPOSITORY: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.pull_request.title }}
RELEASE_TARGET: ${{ github.sha }}
MAX_RETRIES: 3
RETRY_DELAY: 10
run: |
if [ -z "$GITEA_TOKEN" ]; then
echo "Missing secrets.RUNNER_TOKEN"
exit 1
fi
if [ -n "$GITEA_INTERNAL_URL" ]; then
api_base_url="$GITEA_INTERNAL_URL"
echo "Using internal Gitea URL: $api_base_url"
else
api_base_url="$GITEA_SERVER_URL"
echo "Using public Gitea URL: $api_base_url"
fi
version=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.version)
artifact_id=$(mvn -q -DforceStdout help:evaluate -Dexpression=project.artifactId)
jar_file="target/${artifact_id}-${version}.jar"
if [ ! -f "$jar_file" ]; then
echo "Build artifact not found: $jar_file"
exit 1
fi
file_size=$(stat -c%s "$jar_file" 2>/dev/null || stat -f%z "$jar_file" 2>/dev/null || echo "unknown")
echo "Found fat jar: $jar_file (size: $file_size bytes)"
api_url="$api_base_url/api/v1/repos/$GITEA_REPOSITORY/releases"
payload=$(cat <<EOF
{
"tag_name": "$RELEASE_TAG",
"name": "$RELEASE_TAG",
"target_commitish": "$RELEASE_TARGET",
"draft": false,
"prerelease": false
}
EOF
)
echo "Creating release with tag: $RELEASE_TAG"
echo "API URL: $api_url"
echo "Target commit: $RELEASE_TARGET"
release_response_file=$(mktemp /tmp/release_response_XXXXXX.json)
trap "rm -f $release_response_file" EXIT
release_id=""
attempt=1
while [ $attempt -le $MAX_RETRIES ] && [ -z "$release_id" ]; do
echo "[Create release] Attempt $attempt/$MAX_RETRIES..."
> "$release_response_file"
http_code=$(curl -sS -w "%{http_code}" -o "$release_response_file" -X POST "$api_url" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
--connect-timeout 30 \
--max-time 60 \
-d "$payload" 2>/dev/null) || http_code="000"
response=$(cat "$release_response_file" 2>/dev/null || echo "{}")
echo "HTTP Status: $http_code"
if [ "$http_code" = "201" ]; then
if command -v jq >/dev/null 2>&1; then
release_id=$(echo "$response" | jq -r '.id' 2>/dev/null)
else
release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 | tr -d '\n\r')
fi
echo "OK: Release created: id=$release_id"
elif [ "$http_code" = "409" ]; then
echo "Release already exists (HTTP 409), fetching existing release..."
existing=$(curl -sS "$api_url" -H "Authorization: token $GITEA_TOKEN" --connect-timeout 30 2>/dev/null || echo "[]")
if command -v jq >/dev/null 2>&1; then
release_id=$(echo "$existing" | jq -r ".[] | select(.tag_name==\"$RELEASE_TAG\") | .id" 2>/dev/null | head -1)
else
release_id=$(echo "$existing" | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2 | tr -d '\n\r')
fi
if [ -n "$release_id" ]; then
echo "OK: Found existing release: id=$release_id"
else
echo "FAIL: Could not find existing release id"
fi
else
echo "FAIL: Create release failed (HTTP $http_code)"
if [ $attempt -lt $MAX_RETRIES ]; then
echo "Retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
fi
fi
attempt=$((attempt + 1))
done
if [ -z "$release_id" ]; then
echo "FAIL: Failed to create or find release after $MAX_RETRIES attempts"
exit 1
fi
asset_name=$(basename "$jar_file")
echo "Uploading asset: $asset_name (size: $file_size bytes)"
upload_url="$api_url/$release_id/assets?name=$asset_name"
echo "Upload URL: $upload_url"
asset_response_file=$(mktemp /tmp/asset_response_XXXXXX.json)
trap "rm -f $release_response_file $asset_response_file" EXIT
upload_success=false
attempt=1
while [ $attempt -le $MAX_RETRIES ] && [ "$upload_success" = "false" ]; do
echo "[Upload asset] Attempt $attempt/$MAX_RETRIES..."
> "$asset_response_file"
http_code=$(curl -sS -w "%{http_code}" -o "$asset_response_file" -X POST "$upload_url" \
-H "Authorization: token $GITEA_TOKEN" \
--connect-timeout 30 \
--max-time 300 \
-F "attachment=@$jar_file" 2>/dev/null) || http_code="000"
if [ "$http_code" = "201" ]; then
upload_success=true
echo "OK: Successfully uploaded: $asset_name"
else
echo "FAIL: Upload failed (HTTP $http_code)"
cat "$asset_response_file" 2>/dev/null || true
fi
if [ "$upload_success" = "false" ] && [ $attempt -lt $MAX_RETRIES ]; then
echo "Retrying in ${RETRY_DELAY}s..."
sleep $RETRY_DELAY
fi
attempt=$((attempt + 1))
done
if [ "$upload_success" = "false" ]; then
echo "FAIL: Failed to upload asset after $MAX_RETRIES attempts"
exit 1
fi
- name: Mark deployment success
id: set_status
if: always()
run: |
echo "status=success" >> $GITHUB_OUTPUT
notify-on-failure:
runs-on: act_runner_java
needs: build-deploy
if: ${{ always() && github.event.pull_request.merged == true && needs.build-deploy.result == 'failure' }}
steps:
- name: Notify CI failure
env:
PR_NUMBER: ${{ github.event.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_URL: ${{ github.event.pull_request.html_url }}
SOURCE_BRANCH: ${{ github.event.pull_request.head.ref }}
AUTHOR: ${{ github.event.pull_request.user.login }}
COMMIT_SHA: ${{ github.sha }}
REPO: ${{ github.repository }}
SERVER_URL: ${{ github.server_url }}
WEBHOOK_URL: ${{ vars.NOTIFY_WEBHOOK_URL }}
run: |
echo "========================================="
echo "CI Pipeline Failed - Manual Review Required"
echo "========================================="
echo ""
echo "PR: #$PR_NUMBER - $PR_TITLE"
echo "Branch: $SOURCE_BRANCH"
echo "Author: $AUTHOR"
echo "Commit: $COMMIT_SHA"
echo ""
echo "Actions:"
echo " 1. Re-run CI: $SERVER_URL/$REPO/actions"
echo " 2. Revert PR: $PR_URL (click 'Revert' button)"
echo ""
echo "========================================="
if [ -n "$WEBHOOK_URL" ]; then
message="CI 部署失败\n\nPR: #$PR_NUMBER - $PR_TITLE\n分支: $SOURCE_BRANCH\n提交者: $AUTHOR\n\n请检查并决定:\n- 重试 CI\n- 回滚合并"
payload=$(cat <<EOF
{
"msgtype": "text",
"text": {
"content": "$message"
}
}
EOF
)
curl -sS -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "$payload" || echo "Warning: Failed to send notification"
echo "OK: Notification sent"
else
echo "Note: Set vars.NOTIFY_WEBHOOK_URL to enable webhook notifications"
fi

46
pom.xml
View File

@@ -5,31 +5,24 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<version>3.5.11</version>
<relativePath/>
</parent>
<groupId>com.imyeyu.timiserverapi</groupId>
<artifactId>TimiServerAPI</artifactId>
<version>1.0.0</version>
<version>1.0.1</version>
<packaging>jar</packaging>
<name>TimiServerAPI</name>
<description>imyeyu.com API</description>
<properties>
<springboot.version>3.4.0</springboot.version>
<springboot.version>3.5.11</springboot.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.multilingualField>21</maven.compiler.multilingualField>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<repositories>
<repository>
<id>apache-maven</id>
<url>https://repo.maven.apache.org/maven2/</url>
</repository>
</repositories>
<profiles>
<profile>
<id>dev-windows</id>
@@ -112,22 +105,43 @@
<configuration>
<excludeDevtools>true</excludeDevtools>
<mainClass>com.imyeyu.api.TimiServerAPI</mainClass>
<finalName>${project.artifactId}</finalName>
</configuration>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>apache-maven</id>
<url>https://repo.maven.apache.org/maven2/</url>
</repository>
<repository>
<id>timi_nexus</id>
<url>https://nexus.imyeyu.com/repository/maven-public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>com.imyeyu.java</groupId>
<artifactId>timi-java</artifactId>
<version>0.0.3</version>
</dependency>
<dependency>
<groupId>com.imyeyu.spring</groupId>
<artifactId>timi-spring</artifactId>
<version>0.0.2</version>
<version>0.0.12</version>
</dependency>
<dependency>
<groupId>com.imyeyu.network</groupId>
<artifactId>timi-network</artifactId>
<version>0.0.2</version>
<version>0.0.8</version>
</dependency>
<dependency>
<groupId>com.imyeyu.lang</groupId>
@@ -184,12 +198,12 @@
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
<version>6.7.0.202309050840-r</version>
<version>7.2.1.202505142326-r</version>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit.archive</artifactId>
<version>6.7.0.202309050840-r</version>
<version>7.2.1.202505142326-r</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
@@ -220,7 +234,7 @@
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.9.2</version>
<version>3.2.2</version>
</dependency>
<dependency>
<groupId>org.jcodec</groupId>

View File

@@ -1,10 +1,6 @@
package com.imyeyu.api;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.Language;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.utils.OS;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
@@ -13,7 +9,6 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.Environment;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.io.File;
@@ -31,8 +26,6 @@ import java.io.File;
@EnableTransactionManagement
public class TimiServerAPI implements OS.FileSystem, ApplicationContextAware {
private static final String DEV_LANG_CONFIG = "dev.lang";
public static ApplicationContext applicationContext;
@Override
@@ -40,32 +33,18 @@ public class TimiServerAPI implements OS.FileSystem, ApplicationContextAware {
TimiServerAPI.applicationContext = applicationContext;
}
public static Language.Enum getUserLanguage() {
Language.Enum userLanguage = TimiSpring.getLanguage();
Environment env = applicationContext.getBean(Environment.class);
if (env.containsProperty(DEV_LANG_CONFIG)) {
String property = env.getProperty(DEV_LANG_CONFIG);
if (TimiJava.isNotEmpty(property)) {
userLanguage = Ref.toType(Language.Enum.class, property);
}
}
return userLanguage;
}
public static void main(String[] args) {
try {
{
// 导出配置
String[] files = {"application.yml", "logback.xml"};
for (int i = 0; i < files.length; i++) {
File file = new File("config" + SEP + files[i]);
if (!file.exists() || !file.isFile()) {
log.info("exporting default config at {}", file.getAbsolutePath());
IO.resourceToDisk(TimiServerAPI.class, files[i], file.getAbsolutePath());
File application = new File("config" + SEP + "application.yml");
if (!application.exists()) {
IO.resourceToDisk(TimiServerAPI.class, "application_export.yml", application.getAbsolutePath());
}
File logback = new File("config" + SEP + "logback.xml");
if (!logback.exists()) {
IO.resourceToDisk(TimiServerAPI.class, "logback.xml", logback.getAbsolutePath());
}
}
}
// 启动 SpringBoot
SpringApplication.run(TimiServerAPI.class, args);
} catch (Exception e) {

View File

@@ -1,6 +1,6 @@
package com.imyeyu.api.annotation;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Setting;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -17,8 +17,34 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableSetting {
/** @return 配置键 */
SettingKey value();
/**
*
*
* @author 夜雨
* @since 2026-04-27 14:45
*/
enum Logic {
AND,
OR
}
Setting.Module.User[] user() default {};
Setting.Module.Comment[] comment() default {};
Setting.Module.ForeverMC[] foreverMC() default {};
Setting.Module.Music[] music() default {};
Setting.Module.Journal[] journal() default {};
Setting.Module.TempFile[] tempFile() default {};
Setting.Module.System[] system() default {};
Logic logic() default Logic.AND;
/** @return 未启用配置时响应消息语言映射键 */
String message() default "service.offline";

View File

@@ -1,17 +1,20 @@
package com.imyeyu.api.annotation;
import com.imyeyu.api.modules.common.service.SettingService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.common.service.SettingService;
import org.springframework.context.annotation.Lazy;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
/**
* 启用配置注解处理器
*
@@ -24,16 +27,28 @@ public class EnableSettingInterceptor implements HandlerInterceptor {
private final SettingService service;
public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) {
public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) throws InvocationTargetException, IllegalAccessException {
if (handler instanceof HandlerMethod handlerMethod) {
EnableSetting annotation = handlerMethod.getMethodAnnotation(EnableSetting.class);
if (annotation == null) {
return true;
}
if (service.is(annotation.value())) {
return true;
EnableSetting.Logic logic = annotation.logic();
List<Boolean> valueList = new ArrayList<>();
for (Method method : annotation.getClass().getMethods()) {
if (method.getName().equals("message")) {
continue;
}
Enum<?>[] enums = (Enum<?>[]) method.invoke(annotation);
for (Enum<?> anEnum : enums) {
valueList.add(service.is(anEnum));
}
}
if (logic == EnableSetting.Logic.AND) {
return valueList.stream().allMatch(Boolean::booleanValue);
} else {
return valueList.stream().anyMatch(Boolean::booleanValue);
}
throw new TimiException(TimiCode.ERROR_SERVICE_OFF, annotation.message());
}
return true;
}

View File

@@ -1,18 +0,0 @@
package com.imyeyu.api.bean;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.yaml.snakeyaml.Yaml;
/**
* @author 夜雨
* @since 2025-01-13 11:42
*/
@Component
public class IOCBeans {
@Bean
public Yaml yaml() {
return new Yaml();
}
}

View File

@@ -1,7 +1,12 @@
package com.imyeyu.api.config;
import com.google.gson.Gson;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.utils.Time;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.yaml.snakeyaml.Yaml;
/**
* @author 夜雨
@@ -10,7 +15,17 @@ import org.springframework.context.annotation.Configuration;
@Configuration
public class BeanConfig {
public Gson gson() {
return new Gson();
@Bean
public ObjectMapper jackson() {
ObjectMapper mapper = new ObjectMapper();
mapper.setDateFormat(Time.dateTime);
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return mapper;
}
@Bean
public Yaml yaml() {
return new Yaml();
}
}

View File

@@ -29,21 +29,12 @@ public class CORSConfig {
/** 允许跨域的地址 */
private String[] allowOrigin;
/** 是否允许请求带有验证信息 */
private boolean allowCredentials;
/** 允许请求的方法 */
private String allowMethods;
/** 允许服务端访问的客户端请求头 */
private String allowHeaders;
@Bean
public FilterRegistrationBean<Filter> corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader(allowHeaders);
config.addAllowedMethod(allowMethods);
config.setAllowCredentials(allowCredentials);
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
config.setAllowedOriginPatterns(Arrays.asList(allowOrigin));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

View File

@@ -1,13 +1,17 @@
package com.imyeyu.api.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.modules.blog.entity.ArticleRanking;
import com.imyeyu.api.modules.common.entity.Multilingual;
import com.imyeyu.spring.bean.RedisConfigParams;
import com.imyeyu.spring.config.AbstractRedisConfig;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.spring.util.RedisSerializers;
import com.imyeyu.utils.Time;
import io.lettuce.core.api.StatefulConnection;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.RequiredArgsConstructor;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
@@ -32,9 +36,12 @@ import java.time.Duration;
@Configuration
@EqualsAndHashCode(callSuper = true)
@EnableAutoConfiguration
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "spring.redis")
public class RedisConfig extends AbstractRedisConfig {
private final ObjectMapper jackson;
// ---------- 连接配置 ----------
/** 地址 */
@@ -46,50 +53,9 @@ public class RedisConfig extends AbstractRedisConfig {
/** 密码 */
private String password;
/** 超时(毫秒) */
private int timeout;
/** 连接池 */
private Lettuce lettuce;
/** 数据库 */
private Database database;
/**
* 连接池
*
* @author 夜雨
* @since 2023-08-21 16:23
*/
@Data
public static class Lettuce {
/** 配置 */
private Pool pool;
/**
* 配置
*
* @author 夜雨
* @since 2023-08-21 16:23
*/
@Data
public static class Pool {
/** 最大活跃连接 */
private int maxActive;
/** 最小空闲连接 */
private int minIdle;
/** 最大空闲连接 */
private int maxIdle;
/** 最大等待时间(秒) */
private int maxWait;
}
}
/**
* 数据库
*
@@ -145,22 +111,23 @@ public class RedisConfig extends AbstractRedisConfig {
setHost(host);
setPort(port);
setPassword(password);
setTimeout(timeout);
setMaxActive(lettuce.pool.maxActive);
setMinIdle(lettuce.pool.minIdle);
setMaxIdle(lettuce.pool.maxIdle);
setTimeout(Time.SI * 8);
setMaxActive(8);
setMinIdle(1);
setMaxIdle(8);
}};
}
/** @return 连接池配置 */
@Bean
@Override
public GenericObjectPoolConfig<?> getPoolConfig() {
GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(lettuce.pool.maxActive);
config.setMinIdle(lettuce.pool.minIdle);
config.setMaxIdle(lettuce.pool.maxIdle);
config.setMaxWait(Duration.ofMillis(lettuce.pool.maxWait));
public GenericObjectPoolConfig<StatefulConnection<?, ?>> getPoolConfig() {
RedisConfigParams configArgs = configParams();
GenericObjectPoolConfig<StatefulConnection<?, ?>> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(8);
config.setMinIdle(configArgs.getMinIdle());
config.setMaxIdle(configArgs.getMaxIdle());
config.setMaxWait(Duration.ofMillis(-1));
return config;
}
@@ -224,7 +191,7 @@ public class RedisConfig extends AbstractRedisConfig {
/** @return 文章访问统计,文章 ID: {@link ArticleRanking}(JSON) */
@Bean("redisArticleRanking")
public Redis<Long, ArticleRanking> getArticleRankingRedisTemplate() {
return getRedis(database.articleRanking, RedisSerializers.LONG, RedisSerializers.gsonSerializer(ArticleRanking.class));
return getRedis(database.articleRanking, RedisSerializers.LONG, RedisSerializers.jacksonSerializer(jackson, ArticleRanking.class));
}
/** @return 文章访问记录IP: [文章 ID] */

View File

@@ -2,8 +2,6 @@ package com.imyeyu.api.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@@ -19,37 +17,17 @@ import java.util.concurrent.ThreadPoolExecutor;
@Data
@Slf4j
@Configuration
@EnableAutoConfiguration
@ConfigurationProperties(prefix = "spring.async.thread-pool")
public class ThreadPoolConfig {
/** 核心数量 */
private int corePoolSize;
/** 最大数量 */
private int maxPoolSize;
/** 等待区容量 */
private int queueCapacity;
/** 最大保持活跃时间(秒) */
private int keepAliveSeconds;
/** 最大等待时间(秒) */
private int awaitTerminationSeconds;
/** 线程名称前缀 */
private String threadNamePrefix;
@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.setAwaitTerminationSeconds(awaitTerminationSeconds);
executor.setThreadNamePrefix(threadNamePrefix);
executor.setCorePoolSize(16);
executor.setMaxPoolSize(32);
executor.setQueueCapacity(16);
executor.setKeepAliveSeconds(60);
executor.setAwaitTerminationSeconds(60);
executor.setThreadNamePrefix("thread-pool-task-executor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;

View File

@@ -1,32 +1,28 @@
package com.imyeyu.api.config;
import com.google.gson.GsonBuilder;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.imyeyu.api.annotation.EnableSettingInterceptor;
import com.imyeyu.api.annotation.RequestRateLimitInterceptor;
import com.imyeyu.api.annotation.RequiredTokenInterceptor;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.vo.user.UserProfileView;
import com.imyeyu.api.modules.common.vo.user.UserView;
import com.imyeyu.api.modules.journal.util.JournalAPIInterceptor;
import com.imyeyu.api.modules.minecraft.annotation.RequiredFMCServerTokenInterceptor;
import com.imyeyu.api.modules.minecraft.entity.MinecraftPlayer;
import com.imyeyu.api.modules.mirror.vo.MirrorView;
import com.imyeyu.api.modules.system.util.SystemAPIInterceptor;
import com.imyeyu.api.util.GsonSerializerAdapter;
import com.imyeyu.spring.annotation.RequestSingleParamResolver;
import jakarta.validation.constraints.NotNull;
import lombok.NonNull;
import com.imyeyu.spring.annotation.RequestBodyValueArgumentResolver;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.io.Writer;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
@@ -40,12 +36,11 @@ import java.util.List;
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final ObjectMapper jackson;
private final SystemAPIInterceptor systemAPIInterceptor;
private final GsonSerializerAdapter gsonSerializerAdapter;
private final JournalAPIInterceptor journalAPIInterceptor;
private final RequiredTokenInterceptor requiredTokenInterceptor;
private final EnableSettingInterceptor enableSettingInterceptor;
private final RequestSingleParamResolver requestSingleParamResolver;
private final RequestRateLimitInterceptor requestRateLimitInterceptor;
private final RequiredFMCServerTokenInterceptor requiredFMCServerTokenInterceptor;
@@ -66,7 +61,7 @@ public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(requestSingleParamResolver);
argumentResolvers.add(new RequestBodyValueArgumentResolver(jackson));
}
/**
@@ -76,22 +71,17 @@ public class WebConfig implements WebMvcConfigurer {
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
GsonHttpMessageConverter converter = new GsonHttpMessageConverter() {
@Override
protected void writeInternal(@NotNull Object object, Type type, @NonNull Writer writer) {
// 忽略参数类型,因为接口返回对象会被全局返回处理器包装为 TimiResponse否则会序列化转型错误
getGson().toJson(object, writer);
}
};
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Attachment.class, gsonSerializerAdapter);
builder.registerTypeAdapter(UserView.class, gsonSerializerAdapter);
builder.registerTypeAdapter(MirrorView.class, gsonSerializerAdapter);
builder.registerTypeAdapter(UserProfileView.class, gsonSerializerAdapter);
builder.registerTypeAdapter(MinecraftPlayer.class, gsonSerializerAdapter);
converter.setGson(builder.create());
JsonMapper jsonMapper = JsonMapper.builder()
// 设置默认属性包含规则:忽略 null 值
.serializationInclusion(JsonInclude.Include.NON_NULL)
// 日期格式化
.defaultDateFormat(Time.dateTime)
// 忽略不存在字段
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
// 启用默认视图包含
.enable(MapperFeature.DEFAULT_VIEW_INCLUSION).build();
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(jsonMapper);
converter.setDefaultCharset(StandardCharsets.UTF_8);
converters.add(converter);
}
}

View File

@@ -70,8 +70,8 @@ public class GiteaDBConfig {
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List<String> mapperLocations = new ArrayList<>();
mapperLocations.add("classpath:mapper/gitea/**/*.xml");
for (int i = 0; i < mapperLocations.size(); i++) {
resources.addAll(List.of(resourceResolver.getResources(mapperLocations.get(i))));
for (String mapperLocation : mapperLocations) {
resources.addAll(List.of(resourceResolver.getResources(mapperLocation)));
}
}
String[] typeAliases = {

View File

@@ -83,8 +83,8 @@ public class TimiServerDBConfig {
mapperLocations.add("classpath:mapper/system/**/*.xml");
mapperLocations.add("classpath:mapper/journal/**/*.xml");
mapperLocations.add("classpath:mapper/minecraft/**/*.xml");
for (int i = 0; i < mapperLocations.size(); i++) {
resources.addAll(List.of(resourceResolver.getResources(mapperLocations.get(i))));
for (String mapperLocation : mapperLocations) {
resources.addAll(List.of(resourceResolver.getResources(mapperLocation)));
}
}
String[] typeAliases = {

View File

@@ -1,49 +0,0 @@
package com.imyeyu.api.modules.bill.controller;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.bill.entity.Bill;
import com.imyeyu.api.modules.bill.service.BillService;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
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;
/**
* 收支帐单接口
*
* @author 夜雨
* @since 2023-02-04 01:02
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/bill")
public class BillController {
private final BillService service;
private final SettingService settingService;
/**
* 创建收支帐单
*
* @param bill 账单
*/
@AOPLog
@RequestRateLimit
@PostMapping("/create")
public void createREBill(@Valid @RequestBody Bill bill) {
if (!settingService.getAsString(SettingKey.BILL_API_TOKEN).equals(TimiSpring.getToken())) {
throw new TimiException(TimiCode.REQUEST_BAD).msgKey("token.illegal");
}
service.create(bill);
}
}

View File

@@ -1,120 +0,0 @@
package com.imyeyu.api.modules.bill.entity;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import com.imyeyu.spring.entity.Entity;
/**
* 收支账单
*
* @author 夜雨
* @since 2022-03-29 11:28
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Bill extends Entity {
/**
* 类型
*
* @author 夜雨
* @since 2022-03-29 11:28
*/
@Getter
public enum Type {
/** 收入 */
REVENUE,
/** 支出 */
EXPENDITURE
}
/**
* 收入类型
*
* @author 夜雨
* @since 2022-03-29 11:28
*/
@Getter
public enum RevenueType {
/** 工作 */
WORK,
/** 退款 */
REFUND,
/** 其他 */
OTHER
}
/**
* 支出类型
*
* @author 夜雨
* @since 2022-03-29 11:28
*/
@Getter
public enum ExpenditureType {
/** 饮食 */
FOOD,
/** 生活 */
LIFE,
/** 通信 */
COMMUNICATION,
/** 交通 */
TRAFFIC,
/** 娱乐 */
GAME,
/** 工作 */
WORK,
/** 服饰 */
CLOTHES,
/** 医疗 */
HEALTH,
/** 其他 */
OTHER
}
/** 收入类型 */
private RevenueType revenueType;
/** 支出类型 */
private ExpenditureType expenditureType;
/** 描述 */
@NotBlank(message = "bill.description.empty")
private String description;
/** 金额(未确保计算精度,放大了 100 倍) */
@NotNull(message = "bill.decimal.empty")
@DecimalMin(value = "0", message = "bill.decimal.limit")
private Long decimal;
/** 备注 */
private String remarks;
/** @return true 为收入账单 */
public boolean isRevenue() {
return revenueType != null;
}
/** @return true 为支出账单 */
public boolean isExpenditure() {
return expenditureType != null;
}
}

View File

@@ -1,13 +0,0 @@
package com.imyeyu.api.modules.bill.mapper;
import com.imyeyu.api.modules.bill.entity.Bill;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 收支帐单表
*
* @author 夜雨
* @since 2022-04-01 16:26
*/
public interface BillMapper extends BaseMapper<Bill, Long> {
}

View File

@@ -1,13 +0,0 @@
package com.imyeyu.api.modules.bill.service;
import com.imyeyu.api.modules.bill.entity.Bill;
import com.imyeyu.spring.service.CreatableService;
/**
* 收支帐单服务
*
* @author 夜雨
* @since 2022-04-01 16:24
*/
public interface BillService extends CreatableService<Bill> {
}

View File

@@ -1,27 +0,0 @@
package com.imyeyu.api.modules.bill.service.implement;
import com.imyeyu.api.modules.bill.entity.Bill;
import com.imyeyu.api.modules.bill.mapper.BillMapper;
import com.imyeyu.api.modules.bill.service.BillService;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 收支账单服务
*
* @author 夜雨
* @since 2022-04-01 16:25
*/
@Service
@RequiredArgsConstructor
public class BillServiceImplement extends AbstractEntityService<Bill, Long> implements BillService {
private final BillMapper mapper;
@Override
protected BaseMapper<Bill, Long> mapper() {
return mapper;
}
}

View File

@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.blog.entity;
import com.google.gson.JsonElement;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.modules.common.bean.CommentSupport;
import com.imyeyu.spring.entity.Destroyable;
import com.imyeyu.spring.entity.Entity;
@@ -48,7 +48,7 @@ public class Article extends Entity implements CommentSupport, Destroyable {
protected String data;
/** 扩展数据 */
protected JsonElement extendData;
protected JsonNode extendData;
/** 阅读数量 */
protected int reads;

View File

@@ -1,8 +1,9 @@
package com.imyeyu.api.modules.blog.entity;
import com.imyeyu.spring.entity.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
import lombok.NoArgsConstructor;
/**
* 访问排行(每周)
@@ -12,6 +13,7 @@ import com.imyeyu.spring.entity.Entity;
* @since 2021-03-01 17:10
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ArticleRanking extends Entity {

View File

@@ -1,12 +1,12 @@
package com.imyeyu.api.modules.blog.util;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.config.RedisConfig;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.entity.User;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.spring.util.RedisSerializers;
@@ -24,7 +24,7 @@ import java.util.Objects;
/**
* Redis 令牌缓存
*
* <p>一级缓存 Session二级缓存 Redis有效期为 {@link SettingKey#TTL_USER_TOKEN} 天,每次触发
* <p>一级缓存 Session二级缓存 Redis有效期为 {@link com.imyeyu.api.modules.common.entity.Setting.Module.User#TOKEN_TTL} 天,每次触发
* 二级缓存获取时会刷新这个时间,即指定天数内不再访问则视为登出
*
* @author 夜雨
@@ -47,12 +47,12 @@ public class UserToken {
}
public Long set(String token, Long userId) {
long ttl = Time.D * settingService.getAsInt(SettingKey.TTL_USER_TOKEN);
long ttl = settingService.getAsTime(Setting.Module.User.TOKEN_TTL);
// 会话
TimiSpring.setSessionAttr(token, userId);
// 跨站 Cookie
Cookie cookie = Objects.requireNonNullElse(TimiSpring.getCookie("Token"), new Cookie("Token", token));
cookie.setDomain(settingService.getAsString(SettingKey.DOMAIN_ROOT));
cookie.setDomain(settingService.getAsString(Setting.Module.Domain.ROOT));
cookie.setPath("/");
cookie.setSecure(true);
cookie.setMaxAge((int) (ttl / 1000));
@@ -136,7 +136,7 @@ public class UserToken {
TimiSpring.removeSessionAttr(token);
// 清除跨站 Cookie
Cookie cookie = new Cookie("Token", "DIED");
cookie.setDomain(settingService.getAsString(SettingKey.DOMAIN_ROOT));
cookie.setDomain(settingService.getAsString(Setting.Module.Domain.ROOT));
cookie.setPath("/");
cookie.setSecure(true);
cookie.setMaxAge(0);

View File

@@ -1,187 +0,0 @@
package com.imyeyu.api.modules.common.bean;
/**
* 系统设置
*
* @author 夜雨
* @since 2021-07-20 21:46
*/
public enum SettingKey {
// ---------- 通用 ----------
RUN_ENV,
PUBLIC_RESOURCES,
/** 启用注册 */
ENABLE_REGISTER,
/** 启用登录 */
ENABLE_LOGIN,
/** 启用评论 */
ENABLE_COMMENT,
/** 启用测试 */
ENABLE_DEBUG,
/** 启用账号数据更新User 和 UserProfile */
ENABLE_USER_UPDATE,
/** 启用灰色滤镜 */
ENABLE_GRAY_FILTER,
TEMP_FILE_PATH,
// ---------- ICP 备案号 ----------
ICP_IMYEYU_COM,
// ---------- 域名 ----------
DOMAIN_ROOT,
DOMAIN_API,
DOMAIN_GIT,
DOMAIN_BLOG,
DOMAIN_SPACE,
DOMAIN_RESOURCE,
DOMAIN_DOWNLOAD,
DOMAIN_FOREVER_MC,
// ---------- ForeverMC ----------
/** 启用登录服务 */
FMC_PLAYER_LOGIN_ENABLE,
/** 最多绑定玩家数量 */
FMC_MAX_BIND,
/** 闪烁标语 */
FMC_SPLASHES,
/** 启动器背景 */
FMC_BG,
FMC_BGM,
FMC_BG_SWIPER,
/** JRE 列表 */
FMC_JRE,
/** 辅助登录模组 */
FMC_LOGIN_FABRIC,
/** 启用图片地图上传 */
FMC_ENABLE_IMAGE_MAP_UPLOAD,
/** 玩家登录令牌有效期(天) */
FMC_PLAYER_LOGIN_TOKEN_TTL,
/** 服务器与数据中心的通信令牌 */
FMC_SERVER_TOKEN,
// ---------- 生存时间 ----------
TTL_USER_TOKEN,
TTL_SETTING,
TTL_MULTILINGUAL,
// ---------- 多语言翻译 ----------
MULTILINGUAL_TRANSLATE_API,
MULTILINGUAL_TRANSLATE_APP_ID,
MULTILINGUAL_TRANSLATE_KEY,
// ---------- 账单 ----------
BILL_API_TOKEN,
// ---------- Git ----------
GIT_API,
GIT_ABOUT_ARTICLE,
GIT_REPO_PATH,
// ---------- 远程音乐 ----------
MUSIC_MAX_FRAME_LENGTH,
MUSIC_PLAYER_PORT,
MUSIC_PLAYER_IP,
MUSIC_CONTROLLER_PORT,
MUSIC_CONTROLLER_IP,
MUSIC_CONTROLLER_URI,
// ---------- 日记 ----------
JOURNAL_KEY,
JOURNAL_APP_ID,
JOURNAL_APP_SECRET,
// ---------- 临时文件 ----------
/** 临时文件最小缓存时间 */
TEMP_FILE_TTL_MIN,
/** 临时文件最长缓存时间 */
TEMP_FILE_TTL_MAX,
/** 临时文件默认缓存时间 */
TEMP_FILE_TTL_DEFAULT,
/** 已过期的临时文件保留时间 */
TEMP_FILE_RESIDUE_TIME,
/** 每个 IP 限制有效临时文件容量 */
TEMP_FILE_LIMIT,
// ---------- 系统 ----------
SYSTEM_FILE_BASE,
SYSTEM_FILE_TYPE,
SYSTEM_FILE_SYNC,
/** 文件过滤(通过密钥类型) */
SYSTEM_FILE_FILTER,
SYSTEM_STATUS_RATE,
SYSTEM_STATUS_LIMIT,
SYSTEM_STATUS_NETWORK_MAC,
SYSTEM_TERMINAL_TTL,
SYSTEM_TERMINAL_FILTERS,
/** 一般密钥 */
SYSTEM_API_KEY,
/** 超级密钥 */
SYSTEM_API_SUPER_KEY,
SYSTEM_REBOOT_COMMAND,
}

View File

@@ -0,0 +1,35 @@
package com.imyeyu.api.modules.common.bean;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
*
*
* @author 夜雨
* @since 2026-04-24 00:59
*/
@Getter
@AllArgsConstructor
public enum SettingModule {
COMMON("通用"),
DOMAIN("域名"),
FOREVER_MC("Forever MC"),
MULTILINGUAL("多语言环境"),
GIT("Git"),
MUSIC("远程音乐"),
JOURNAL("糕雨日记"),
TEMP_FILE("临时文件"),
SYSTEM("系统");
final String title;
}

View File

@@ -3,9 +3,9 @@ package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.annotation.CaptchaValid;
import com.imyeyu.api.annotation.EnableSetting;
import com.imyeyu.api.bean.CaptchaFrom;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.api.modules.common.entity.CommentReply;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.CommentReplyService;
import com.imyeyu.api.modules.common.service.CommentService;
import com.imyeyu.spring.annotation.AOPLog;
@@ -39,7 +39,7 @@ public class CommentController {
@AOPLog
@CaptchaValid(CaptchaFrom.COMMENT)
@EnableSetting(value = SettingKey.ENABLE_COMMENT, message = "comment.off_service")
@EnableSetting(comment = Setting.Module.Comment.ENABLE, message = "comment.off_service")
@RequestRateLimit
@PostMapping("/create")
public void create(@Valid @RequestBody CaptchaData<Comment> captchaData) {
@@ -59,7 +59,7 @@ public class CommentController {
*/
@AOPLog
@CaptchaValid(CaptchaFrom.COMMENT_REPLY)
@EnableSetting(value = SettingKey.ENABLE_COMMENT, message = "comment.off_service")
@EnableSetting(comment = Setting.Module.Comment.ENABLE, message = "comment.off_service")
@RequestRateLimit
@PostMapping("/reply/create")
public void createReply(@Valid @RequestBody CaptchaData<CommentReply> request) {

View File

@@ -1,10 +1,8 @@
package com.imyeyu.api.modules.common.controller;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.bean.CaptchaFrom;
import com.imyeyu.api.modules.common.bean.ImageType;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.entity.Task;
@@ -47,8 +45,8 @@ import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.tika.Tika;
import org.springframework.http.MediaType;
import org.springframework.data.mongodb.gridfs.GridFsResource;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -69,9 +67,9 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 系统接口
@@ -95,7 +93,7 @@ public class CommonController {
private final AttachmentService attachmentService;
private final ClipboardService clipboardService;
private final Gson gson;
private final ObjectMapper jackson;
private final Yaml yaml;
private final GridFSBucket gridFSBucket;
private final CaptchaManager captchaManager;
@@ -239,64 +237,40 @@ public class CommonController {
}
@RequestRateLimit
@GetMapping("/setting/{key}")
public String settingByKey(@PathVariable("key") String key, @RequestParam(value = "as", required = false) Setting.Type asType) {
Setting setting = settingService.getByKey(SettingKey.valueOf(key.toUpperCase()));
@GetMapping("/setting")
public String settingByKey(@RequestParam String module, @RequestParam String key) throws ClassNotFoundException {
Setting setting = settingService.getByKey(SettingService.fromModuleKey(module, key));
if (setting.isPrivate()) {
throw new TimiException(TimiCode.PERMISSION_ERROR);
}
String result = setting.getValue();
if (asType == null) {
return result;
}
switch (asType) {
case JSON -> {
if (setting.getType() == Setting.Type.YAML) {
Map<String, Object> obj = yaml.load(setting.getValue());
result = gson.toJson(obj);
}
}
case YAML -> {
if (setting.getType() == Setting.Type.JSON) {
Map<String, Object> obj = gson.fromJson(setting.getValue(), new TypeToken<Map<String, Object>>() {}.getType());
result = yaml.dump(obj);
}
}
}
return result;
return setting.getValue();
}
@RequestRateLimit
@PostMapping("/setting/map")
public Map<SettingKey, String> mapSettingByKeys(@RequestBody Map<SettingKey, Map<String, Object>> settingMap) {
List<Setting> result = settingService.listByKeys(new ArrayList<>(settingMap.keySet()));
for (int i = 0; i < result.size(); i++) {
Setting setting = result.get(i);
public Map<String, Map<String, String>> mapSettingByKeys(@RequestBody Map<String, List<String>> moduleKeyMap) {
List<Enum<?>> keyList = new ArrayList<>();
moduleKeyMap.forEach((k, v) -> {
try {
for (String key : v) {
keyList.add(SettingService.fromModuleKey(k, key));
}
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
});
Map<String, Map<String, String>> result = new HashMap<>();
List<Setting> settingList = settingService.listByKeys(keyList);
for (Setting setting : settingList) {
if (setting.isPrivate()) {
throw new TimiException(TimiCode.PERMISSION_ERROR);
}
Map<String, Object> args = settingMap.get(setting.getKey());
if (args == null) {
continue;
if (!result.containsKey(setting.getModule())) {
result.put(setting.getKey(), new HashMap<>());
}
if (args.containsKey("as")) {
switch (Ref.toType(Setting.Type.class, args.get("as").toString())) {
case JSON -> {
if (setting.getType() == Setting.Type.YAML) {
Map<String, Object> obj = new Yaml().load(setting.getValue());
setting.setValue(gson.toJson(obj));
result.get(setting.getModule()).put(setting.getKey(), setting.getValue());
}
}
case YAML -> {
if (setting.getType() == Setting.Type.JSON) {
Map<String, Object> obj = gson.fromJson(setting.getValue(), new TypeToken<Map<String, Object>>() {}.getType());
setting.setValue(new Yaml().dump(obj));
}
}
}
}
}
return result.stream().collect(Collectors.toMap(Setting::getKey, Setting::getValue));
return result;
}
@AOPLog
@@ -483,7 +457,7 @@ public class CommonController {
GridFSFile file = attachmentService.readByMongoId(mongoId);
@Cleanup
GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId());
RequestRange range = TimiSpring.requestRange(attach.getSize());
RequestRange range = TimiSpring.getRequestRange(attach.getSize());
if (range == null) {
// 完整文件
resp.setContentLengthLong(attach.getSize());

View File

@@ -3,9 +3,9 @@ package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.annotation.CaptchaValid;
import com.imyeyu.api.annotation.EnableSetting;
import com.imyeyu.api.bean.CaptchaFrom;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.api.modules.common.entity.CommentReply;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.entity.UserConfig;
import com.imyeyu.api.modules.common.entity.UserPrivacy;
import com.imyeyu.api.modules.common.service.CommentReplyService;
@@ -26,7 +26,6 @@ import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequestSingleParam;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.Page;
@@ -72,7 +71,7 @@ public class UserController implements TimiJava {
*/
@AOPLog
@CaptchaValid(CaptchaFrom.REGISTER)
@EnableSetting(value = SettingKey.ENABLE_REGISTER, message = "user.register.off_service")
@EnableSetting(user = Setting.Module.User.ENABLE_REGISTER, message = "user.register.off_service")
@RequestRateLimit(value = 1, lifeCycle = 60)
@PostMapping("/register")
public LoginResponse register(@Valid @RequestBody CaptchaData<RegisterRequest> request) {
@@ -87,7 +86,7 @@ public class UserController implements TimiJava {
*/
@AOPLog
@CaptchaValid(CaptchaFrom.LOGIN)
@EnableSetting(value = SettingKey.ENABLE_LOGIN, message = "user.login.off_service")
@EnableSetting(user = Setting.Module.User.ENABLE_LOGIN, message = "user.login.off_service")
@RequestRateLimit
@PostMapping("/login")
public LoginResponse login(@Valid @RequestBody CaptchaData<LoginRequest> request) {
@@ -100,7 +99,7 @@ public class UserController implements TimiJava {
* @return 登录数据
*/
@AOPLog
@EnableSetting(value = SettingKey.ENABLE_LOGIN, message = "user.login.off_service")
@EnableSetting(user = Setting.Module.User.ENABLE_LOGIN, message = "user.login.off_service")
@RequestRateLimit
@PostMapping("/login/token")
public LoginResponse login4Token() {
@@ -184,7 +183,7 @@ public class UserController implements TimiJava {
@RequiredToken
@RequestRateLimit
@PostMapping("/cancel")
public void cancel(@RequestSingleParam String password) {
public void cancel(@RequestBody String password) {
service.cancel(password);
}
@@ -208,7 +207,7 @@ public class UserController implements TimiJava {
*/
@AOPLog
@RequiredToken
@EnableSetting(value = SettingKey.ENABLE_USER_UPDATE, message = "user.data.off_service")
@EnableSetting(user = Setting.Module.User.ENABLE_PROFILE_UPDATE, message = "user.data.off_service")
@RequestRateLimit
@PostMapping("/profile/update")
public void updateProfile(@Valid UserRequest data) {
@@ -275,7 +274,7 @@ public class UserController implements TimiJava {
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/delete")
public void deleteComment(@RequestSingleParam Long commentId) {
public void deleteComment(@RequestBody Long commentId) {
commentService.get(commentId);
commentService.delete(commentId);
}
@@ -298,7 +297,7 @@ public class UserController implements TimiJava {
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/reply/delete")
public void deleteCommentReply(@RequestSingleParam Long replyId) {
public void deleteCommentReply(@RequestBody Long replyId) {
CommentReply reply = commentReplyService.get(replyId);
TimiException.requiredTrue(reply.getSenderId().equals(service.getLoginUser().getId()), "user.comment.reply.delete.not_owner");
commentReplyService.delete(replyId);
@@ -308,7 +307,7 @@ public class UserController implements TimiJava {
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/reply/ignore")
public void ignoreCommentReply(@RequestSingleParam Long replyId) {
public void ignoreCommentReply(@RequestBody Long replyId) {
CommentReply reply = commentReplyService.get(replyId);
TimiException.requiredTrue(reply.getReceiverId().equals(service.getLoginUser().getId()), "user.comment.reply.ignore.not_owner");
reply.setIgnoredAt(Time.now());

View File

@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.common.entity;
import com.google.gson.JsonObject;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.bean.MultilingualHandler;
import com.imyeyu.api.modules.common.service.AttachmentService;
@@ -81,7 +81,7 @@ public class Attachment extends Entity implements MultilingualHandler {
protected String mimeType;
protected JsonObject metadata;
protected JsonNode metadata;
protected Long size;

View File

@@ -1,10 +1,13 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.spring.annotation.table.Id;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.Entity;
import com.imyeyu.spring.entity.Updatable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 系统配置
@@ -13,7 +16,9 @@ import lombok.Data;
* @since 2021-07-20 21:46
*/
@Data
public class Setting implements Creatable, Updatable {
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Setting extends Entity implements Creatable, Updatable {
/**
*
@@ -21,31 +26,377 @@ public class Setting implements Creatable, Updatable {
* @author 夜雨
* @since 2025-01-10 17:08
*/
public enum Type {
INTEGER,
public enum ValueType {
/** 字符串 */
STRING,
JSON,
/** 单选 */
SELECT_RADIO,
YAML,
/** 多选 */
SELECT_CHECKBOX,
/** 整数 */
INTEGER,
/** 颜色 */
COLOR,
/** 浮点数 */
FLOAT,
/** 日期 */
DATE,
/** 日期时间 */
DATETIME,
/** 时间 */
TIME,
/** 时长 */
DURATION,
/** 布尔值 */
BOOLEAN,
/** JSON 列表 */
JSON_LIST,
/** JSON 对象 */
JSON_OBJECT
}
@Id
private SettingKey key;
private String module;
private String key;
private String value;
private Type type;
private ValueType valueType;
private JsonNode valueArgs;
private boolean isPrivate;
private Long createdAt;
@Transient
private Enum<?> keyRaw;
private Long updatedAt;
public Setting(Enum<?> key) {
setKeyRaw(key);
}
public Setting(Enum<?> key, Object value, ValueType valueType) {
setKeyRaw(key);
this.value = value.toString();
this.valueType = valueType;
}
public void setKeyRaw(Enum<?> key) {
this.keyRaw = key;
this.module = key.getClass().getSimpleName().toUpperCase();
this.key = key.toString();
}
public boolean isPublic() {
return !isPrivate;
}
public boolean is(Enum<?> anEnum) {
return anEnum.getClass().getSimpleName().toUpperCase().equals(module) && anEnum.toString().equals(key);
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:23
*/
public static final class Module {
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:39
*/
public enum Common {
/** 微信收款码 */
WECHAT_RECEIVE_QR_CODE
}
/**
* 域名
*
* @author 夜雨
* @since 2026-04-26 17:18
*/
public enum Domain {
ROOT,
API,
GIT,
BLOG,
SPACE,
FOREVER_MC
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:14
*/
public enum Blog {
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:03
*/
public enum User {
/** 启用登录 */
ENABLE_LOGIN,
/** 启用注册 */
ENABLE_REGISTER,
/** 允许数据更新 */
ENABLE_PROFILE_UPDATE,
/** 默认头像 */
DEFAULT_AVATAR,
/** 默认封面 */
DEFAULT_WRAPPER,
/** 令牌有效期 */
TOKEN_TTL
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:21
*/
public enum Comment {
/** 启用服务 */
ENABLE
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:03
*/
public enum ICP {
IMYEYU_COM
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:01
*/
public enum ForeverMC {
PLAYER_BIND_MAX,
LOGIN_TOKEN_TTL
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:57
*/
public enum Multilingual {
/** 翻译 API */
TRANSLATE_API,
/** 翻译 AppId */
TRANSLATE_APP_ID,
/** 翻译密钥 */
TRANSLATE_KEY,
/** 翻译计划任务表达式 */
TRANSLATE_CORN,
/** 缓存时长 */
CACHE_TTL
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:55
*/
public enum Gitea {
/** 接口地址 */
API,
/** 关于页面所属文章 ID */
ABOUT_ARTICLE_ID,
/** 仓库文件地址 */
REPOSITORY_PATH
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:52
*/
public enum Music {
/** 启用服务 */
ENABLE,
/** 通信消息最大帧长度 */
MAX_FRAME_LENGTH,
/** 受控端 IP */
PLAYER_IP,
/** 受控端端口 */
PLAYER_PORT,
/** 控制端 IP */
CONTROLLER_IP,
/** 控制端端口 */
CONTROLLER_PORT,
/** 控制端入口地址 */
CONTROLLER_URI,
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:49
*/
public enum Journal {
/** 启用服务 */
ENABLE,
/** 访问密钥 */
KEY,
/** 微信小程序 AppId */
APP_ID,
/** 微信小程序 AppSecret */
APP_SECRET,
/** 备忘录内容 */
MEMO,
/** 高权限访问 OpenID 白名单(应付审核) */
OPEN_ID_WHITE_LIST
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:47
*/
public enum TempFile {
/** 启用服务 */
ENABLE,
/** 最小缓存时长 */
TTL_MIN,
/** 默认缓存时长 */
TTL_DEFAULT,
/** 最大缓存时长 */
TTL_MAX,
/** 已过期文件残留时长 */
RESIDUE_TTL,
/** 每个 IP 限制有效临时文件容量 */
LIMIT_SIZE_OF_IP
}
/**
*
*
* @author 夜雨
* @since 2026-04-27 14:35
*/
public enum FileSync {
ENABLE,
CRON,
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:23
*/
public enum System {
/** 一般密钥 */
API_KEY,
/** 超级密钥 */
API_KEY_SUPER,
/** 状态采集数据量 */
STATUS_LIMIT,
/** 状态采集网卡 */
STATUS_NETWORK_MAC,
/** 状态采集频率 */
STATUS_RATE,
/** 终端指令过滤 */
TERMINAL_FILTERS,
/** 终端会话有效时长 */
TERMINAL_TTL,
/** 文件系统基准路径 */
FILE_BASE_PATH,
/** 设置缓存时长 */
SETTING_TTL,
/** 哀悼滤镜 */
ENABLE_MOURN_FILTER,
}
}
}

View File

@@ -1,6 +1,5 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
@@ -13,10 +12,7 @@ import java.util.List;
* @author 夜雨
* @since 2021-07-20 22:26
*/
public interface SettingMapper extends BaseMapper<Setting, String> {
@Select("SELECT * FROM `setting` WHERE `key` = #{key}")
Setting selectByKey(SettingKey key);
public interface SettingMapper extends BaseMapper<Setting, Long> {
@Select("SELECT * FROM `setting`")
List<Setting> listAll();

View File

@@ -1,16 +1,17 @@
package com.imyeyu.api.modules.common.service;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.spring.service.UpdatableService;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.service.BaseService;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
/**
* 系统配置服务
@@ -18,15 +19,21 @@ import java.util.List;
* @author 夜雨
* @since 2021-07-20 22:06
*/
public interface SettingService extends UpdatableService<Setting> {
public interface SettingService extends BaseService<Setting, Long> {
default List<Setting> listByKeys(SettingKey... keys) {
@SuppressWarnings("unchecked")
static <T extends Enum<?>> T fromModuleKey(String module, String key) throws ClassNotFoundException {
Class<?> moduleClass = Class.forName(String.format("com.imyeyu.api.modules.common.entity.Setting$Module$%s", module));
return Ref.toType((Class<T>) moduleClass, key);
}
default List<Setting> listByKeys(Enum<?>... keys) {
return listByKeys(Arrays.asList(keys));
}
List<Setting> listByKeys(List<SettingKey> keyList);
List<Setting> listByKeys(List<Enum<?>> keyList);
Setting getByKey(SettingKey key);
Setting getByKey(Enum<?> key);
/**
* 获取指定类型配置值字符串
@@ -34,13 +41,15 @@ public interface SettingService extends UpdatableService<Setting> {
* @param key 键
* @return 配置值
*/
String getAsString(SettingKey key);
String getAsString(Enum<?> key);
int getAsInt(SettingKey key);
int getAsInt(Enum<?> key);
long getAsLong(SettingKey key);
long getAsLong(Enum<?> key);
double getAsDouble(SettingKey key);
double getAsDouble(Enum<?> key);
long getAsTime(Enum<?> key);
/**
* 获取为布尔值
@@ -49,7 +58,7 @@ public interface SettingService extends UpdatableService<Setting> {
* @return 配置值
* @throws TimiException 服务异常
*/
boolean is(SettingKey key);
boolean is(Enum<?> key);
/**
* 获取为布尔值,并取反
@@ -58,23 +67,23 @@ public interface SettingService extends UpdatableService<Setting> {
* @return 配置值
* @throws TimiException 服务异常
*/
boolean not(SettingKey key);
boolean not(Enum<?> key);
JsonElement getAsJsonElement(SettingKey key);
JsonNode getAsJsonNode(Enum<?> key);
JsonObject getAsJsonObject(SettingKey key);
ObjectNode getAsJsonObject(Enum<?> key);
JsonArray getAsJsonArray(SettingKey key);
ArrayNode getAsArrayNode(Enum<?> key);
<T> T fromJson(SettingKey key, Class<T> clazz);
Set<String> getAsSet(Enum<?> key);
<T> T fromJson(SettingKey key, TypeToken<T> typeToken);
List<String> getAsList(Enum<?> key);
<T> T fromYaml(SettingKey key, Class<T> clazz);
<T> T fromJson(Enum<?> key, Class<T> clazz);
<T> T fromJson(Enum<?> key, TypeReference<T> typeReference);
<T> T fromYaml(Enum<?> key, Class<T> clazz);
List<Setting> listAll();
void clearCache(SettingKey key);
void flushCache();
}

View File

@@ -1,7 +1,7 @@
package com.imyeyu.api.modules.common.service.implement;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.bean.MediaAttach;
import com.imyeyu.api.modules.common.bean.Metadata;
@@ -55,7 +55,7 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
private final AttachmentMapper mapper;
private final Gson gson;
private final ObjectMapper jackson;
private final GridFSBucket gridFSBucket;
private final GridFsTemplate gridFsTemplate;
@@ -90,10 +90,10 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
attachment.setIsDestroyed(false);
if (attachment.getMimeType().startsWith("image")) {
BufferedImage image = ImageIO.read(gridFSBucket.openDownloadStream(gridFSFile.getObjectId()));
attachment.setMetadata(TimiJava.defaultIfNull(attachment.getMetadata(), new JsonObject()));
JsonObject metadata = attachment.getMetadata();
metadata.addProperty("width", image.getWidth());
metadata.addProperty("height", image.getHeight());
attachment.setMetadata(TimiJava.defaultIfNull(attachment.getMetadata(), jackson.createObjectNode()));
ObjectNode metadata = (ObjectNode) attachment.getMetadata();
metadata.put("width", image.getWidth());
metadata.put("height", image.getHeight());
}
mapper.insert(attachment);
} catch (Exception e) {
@@ -166,7 +166,7 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
thumbAttach.setBizType(attachment.getBizType());
thumbAttach.setBizId(attachment.getBizId());
thumbAttach.setAttachTypeValue(MediaAttach.Type.THUMB);
thumbAttach.setMetadata(gson.toJsonTree(thumbMetadata).getAsJsonObject());
thumbAttach.setMetadata(jackson.valueToTree(thumbMetadata));
thumbAttach.setInputStream(new ByteArrayInputStream(thumbStream.toByteArray()));
create(thumbAttach);
@@ -182,7 +182,7 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
public void deleteMedia(Long thumbId) throws TimiException {
Attachment attachment = get(thumbId);
delete(attachment.getId());
Metadata.ThumbImage thumbMetadata = gson.fromJson(attachment.getMetadata(), Metadata.ThumbImage.class);
Metadata.ThumbImage thumbMetadata = jackson.convertValue(attachment.getMetadata(), Metadata.ThumbImage.class);
delete(thumbMetadata.getSourceId());
}

View File

@@ -1,10 +1,10 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.Language;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Multilingual;
import com.imyeyu.api.modules.common.mapper.MultilingualMapper;
import com.imyeyu.api.modules.common.service.MultilingualService;
@@ -66,7 +66,7 @@ public class MultilingualServiceImplement extends AbstractEntityService<Multilin
if (result == null) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not found language");
}
redisLanguage.set(id, result, Time.D * settingService.getAsInt(SettingKey.TTL_MULTILINGUAL));
redisLanguage.set(id, result, settingService.getAsTime(Setting.Module.Multilingual.CACHE_TTL));
}
return result.getValue(language);
}
@@ -80,7 +80,7 @@ public class MultilingualServiceImplement extends AbstractEntityService<Multilin
log.warn("not found language for key: {}", key);
return key;
}
long ttl = Time.D * settingService.getAsInt(SettingKey.TTL_MULTILINGUAL);
long ttl = settingService.getAsTime(Setting.Module.Multilingual.CACHE_TTL);
redisLanguage.set(result.getId(), result, ttl);
redisLanguageMap.set(result.getKey(), result.getId(), ttl);
return result.getValue(language);

View File

@@ -1,29 +1,32 @@
package com.imyeyu.api.modules.common.service.implement;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.mapper.SettingMapper;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArg;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.utils.Time;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.yaml.snakeyaml.Yaml;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* 系统配置服务实现
@@ -34,108 +37,174 @@ import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class SettingServiceImplement extends AbstractEntityService<Setting, String> implements SettingService {
public class SettingServiceImplement extends AbstractEntityService<Setting, Long> implements SettingService {
/** 缓存键模板参数module, key */
private static final String CACHE_KEY_TMPL = "%s:%s";
private final SettingMapper mapper;
private final Redis<String, String> redisSetting;
private final Gson gson;
private final ObjectMapper jackson;
@PostConstruct
private void initialize() {
CallbackArg<Setting> createIfNotExist = setting -> {
Setting dbSetting = mapper.selectByExample(new Setting(setting.getKeyRaw()));
if (dbSetting == null) {
create(setting);
}
};
createIfNotExist.handler(new Setting(Setting.Module.System.SETTING_TTL, "10m", Setting.ValueType.DURATION));
createIfNotExist.handler(new Setting(Setting.Module.System.API_KEY, "[]", Setting.ValueType.JSON_LIST));
createIfNotExist.handler(new Setting(Setting.Module.System.API_KEY_SUPER, "[]", Setting.ValueType.JSON_LIST));
createIfNotExist.handler(new Setting(Setting.Module.System.STATUS_RATE, 3000, Setting.ValueType.INTEGER));
createIfNotExist.handler(new Setting(Setting.Module.System.STATUS_LIMIT, 2048, Setting.ValueType.INTEGER));
createIfNotExist.handler(new Setting(Setting.Module.System.STATUS_NETWORK_MAC, "", Setting.ValueType.STRING));
createIfNotExist.handler(new Setting(Setting.Module.Music.ENABLE, false, Setting.ValueType.BOOLEAN));
createIfNotExist.handler(new Setting(Setting.Module.Journal.KEY, "[]", Setting.ValueType.JSON_LIST));
createIfNotExist.handler(new Setting(Setting.Module.Journal.OPEN_ID_WHITE_LIST, "[]", Setting.ValueType.JSON_LIST));
}
@PostConstruct
private void postConstruct() {
redisSetting.flushAll();
}
@Override
protected BaseMapper<Setting, String> mapper() {
protected BaseMapper<Setting, Long> mapper() {
return mapper;
}
public Setting getByKey(SettingKey key) {
@Override
public void update(Setting setting) {
super.update(setting);
redisSetting.destroy(CACHE_KEY_TMPL.formatted(setting.getModule(), setting.getKey()));
}
public Setting getByKey(Enum<?> key) {
if (key == null) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("key can not be null");
}
String cacheValue = redisSetting.get(key.toString());
String cacheKey = CACHE_KEY_TMPL.formatted(key.getClass().getSimpleName().toUpperCase(), key.toString());
String cacheValue = redisSetting.get(cacheKey);
if (TimiJava.isNotEmpty(cacheValue)) {
return gson.fromJson(cacheValue, Setting.class);
try {
return jackson.readValue(cacheValue, Setting.class);
} catch (JsonProcessingException e) {
throw new TimiException(TimiCode.ERROR, "read setting cache error", e);
}
Setting setting = mapper.selectByKey(key);
}
Setting setting = mapper.selectByExample(new Setting(key));
TimiException.required(setting, "not found setting: %s.%s".formatted(key.getClass().getName(), key));
if (TimiJava.isEmpty(setting.getValue())) {
// 无需缓存
return setting;
}
int settingTTL;
if (key == SettingKey.TTL_SETTING) {
settingTTL = Integer.parseInt(setting.getValue());
long settingTTL;
if (setting.is(Setting.Module.System.SETTING_TTL)) {
settingTTL = Time.parseToMS(setting.getValue());
} else {
settingTTL = Integer.parseInt(getByKey(SettingKey.TTL_SETTING).getValue());
settingTTL = Time.parseToMS(getByKey(Setting.Module.System.SETTING_TTL).getValue());
}
if (0 < settingTTL) {
redisSetting.set(key.toString(), gson.toJson(setting), Time.M * settingTTL);
try {
redisSetting.set(key.toString(), jackson.writeValueAsString(setting), settingTTL);
} catch (JsonProcessingException e) {
throw new TimiException(TimiCode.ERROR, "write setting cache error", e);
}
}
return setting;
}
@Override
public List<Setting> listByKeys(List<SettingKey> keyList) {
public List<Setting> listByKeys(List<Enum<?>> keyList) {
List<Setting> result = new ArrayList<>();
for (int i = 0; i < keyList.size(); i++) {
result.add(getByKey(keyList.get(i)));
for (Enum<?> anEnum : keyList) {
result.add(getByKey(anEnum));
}
return result;
}
@Override
public String getAsString(SettingKey key) {
public String getAsString(Enum<?> key) {
return getByKey(key).getValue();
}
@Override
public int getAsInt(SettingKey key) {
public int getAsInt(Enum<?> key) {
return Integer.parseInt(getAsString(key));
}
@Override
public long getAsLong(SettingKey key) {
public long getAsLong(Enum<?> key) {
return Long.parseLong(getAsString(key));
}
@Override
public double getAsDouble(SettingKey key) {
public double getAsDouble(Enum<?> key) {
return Double.parseDouble(getAsString(key));
}
@Override
public boolean is(SettingKey key) {
public long getAsTime(Enum<?> key) {
return Time.parseToMS(getAsString(key));
}
@Override
public boolean is(Enum<?> key) {
return Boolean.parseBoolean(getAsString(key));
}
@Override
public boolean not(SettingKey key) {
public boolean not(Enum<?> key) {
return !is(key);
}
@Override
public JsonElement getAsJsonElement(SettingKey key) {
return JsonParser.parseString(getAsString(key));
public JsonNode getAsJsonNode(Enum<?> key) {
try {
return jackson.readTree(getAsString(key));
} catch (JsonProcessingException e) {
throw new TimiException(TimiCode.ERROR, "read setting json error", e);
}
}
@Override
public JsonObject getAsJsonObject(SettingKey key) {
return getAsJsonElement(key).getAsJsonObject();
public ObjectNode getAsJsonObject(Enum<?> key) {
return (ObjectNode) getAsJsonNode(key);
}
@Override
public JsonArray getAsJsonArray(SettingKey key) {
return getAsJsonElement(key).getAsJsonArray();
public ArrayNode getAsArrayNode(Enum<?> key) {
return (ArrayNode) getAsJsonNode(key);
}
@Override
public <T> T fromJson(SettingKey key, Class<T> clazz) {
return gson.fromJson(getAsJsonElement(key), clazz);
public Set<String> getAsSet(Enum<?> key) {
return new HashSet<>(getAsList(key));
}
@Override
public <T> T fromJson(SettingKey key, TypeToken<T> typeToken) {
return gson.fromJson(getAsJsonElement(key), typeToken);
public List<String> getAsList(Enum<?> key) {
return getAsArrayNode(key).valueStream().map(JsonNode::asText).toList();
}
public <T> T fromYaml(SettingKey key, Class<T> clazz) {
@Override
public <T> T fromJson(Enum<?> key, Class<T> clazz) {
try {
return jackson.treeToValue(getAsJsonNode(key), clazz);
} catch (JsonProcessingException e) {
throw new TimiException(TimiCode.ERROR, "read setting json error", e);
}
}
@Override
public <T> T fromJson(Enum<?> key, TypeReference<T> typeReference) {
return jackson.convertValue(getAsJsonNode(key), typeReference);
}
public <T> T fromYaml(Enum<?> key, Class<T> clazz) {
return new Yaml().loadAs(getAsString(key), clazz);
}
@@ -143,14 +212,4 @@ public class SettingServiceImplement extends AbstractEntityService<Setting, Stri
public List<Setting> listAll() {
return mapper.listAll();
}
@Override
public void clearCache(SettingKey key) {
redisSetting.destroy(key.toString());
}
@Override
public void flushCache() {
redisSetting.flushAll();
}
}

View File

@@ -1,8 +1,6 @@
package com.imyeyu.api.modules.common.service.implement;
import com.google.gson.Gson;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.service.TempFileService;
@@ -31,21 +29,19 @@ import java.util.List;
@RequiredArgsConstructor
public class TempFileServiceImplement implements TempFileService {
private final Gson gson;
private final SettingService settingService;
private final AttachmentService attachmentService;
public List<TempFileResponse> store(List<MultipartFile> files, Long ttl) throws TimiException {
String ttlMinStr = settingService.getAsString(SettingKey.TEMP_FILE_TTL_MIN);
String ttlMaxStr = settingService.getAsString(SettingKey.TEMP_FILE_TTL_MAX);
String limitStr = settingService.getAsString(SettingKey.TEMP_FILE_LIMIT);
String ttlMinStr = settingService.getAsString(Setting.Module.TempFile.TTL_MIN);
String ttlMaxStr = settingService.getAsString(Setting.Module.TempFile.TTL_MAX);
String limitStr = settingService.getAsString(Setting.Module.TempFile.LIMIT_SIZE_OF_IP);
long minTTL = Time.parseToMS(ttlMinStr);
long maxTTL = Time.parseToMS(ttlMaxStr);
long defaultTTL = Time.parseToMS(settingService.getAsString(SettingKey.TEMP_FILE_TTL_DEFAULT));
long defaultTTL = Time.parseToMS(settingService.getAsString(Setting.Module.TempFile.TTL_DEFAULT));
long limit = IOSize.parse(limitStr);
long residueTime = Time.parseToMS(settingService.getAsString(SettingKey.TEMP_FILE_RESIDUE_TIME));
long residueTime = Time.parseToMS(settingService.getAsString(Setting.Module.TempFile.RESIDUE_TTL));
ttl = TimiJava.defaultIfNull(ttl, defaultTTL);
TimiException.requiredTrue(minTTL < ttl && ttl <= maxTTL, String.format("ttl must be between %s and %s", ttlMinStr, ttlMaxStr));
@@ -69,9 +65,7 @@ public class TempFileServiceImplement implements TempFileService {
long deletedAt = Time.now() + ttl;
long destroyAt = deletedAt + residueTime;
for (int i = 0; i < files.size(); i++) {
MultipartFile file = files.get(i);
for (MultipartFile file : files) {
Attachment attach = new Attachment();
attach.setBizType(Attachment.BizType.TEMP_FILE);
attach.setName(file.getOriginalFilename());

View File

@@ -1,11 +1,10 @@
package com.imyeyu.api.modules.common.task;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Multilingual;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.MultilingualService;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.java.TimiJava;
@@ -81,6 +80,7 @@ public class MultilingualTranslateTask {
}
}
private final ObjectMapper jackson;
private final SettingService settingService;
private final MultilingualService service;
@@ -131,8 +131,8 @@ public class MultilingualTranslateTask {
private synchronized Map<String, String> doTranslate(String text, BaiduLanguage to) throws Exception {
String random = String.valueOf(Time.now());
String appId = settingService.getAsString(SettingKey.MULTILINGUAL_TRANSLATE_APP_ID);
String key = settingService.getAsString(SettingKey.MULTILINGUAL_TRANSLATE_KEY);
String appId = settingService.getAsString(Setting.Module.Multilingual.TRANSLATE_APP_ID);
String key = settingService.getAsString(Setting.Module.Multilingual.TRANSLATE_KEY);
ArgMap<String, Object> args = new ArgMap<>();
args.put("q", text);
@@ -142,23 +142,22 @@ public class MultilingualTranslateTask {
args.put("salt", random);
args.put("sign", Digest.md5(appId + text + random + key));
String response = Request.post(settingService.getAsString(SettingKey.MULTILINGUAL_TRANSLATE_API))
String response = Request.post(settingService.getAsString(Setting.Module.Multilingual.TRANSLATE_API))
.bodyForm(args.toNameValuePair())
.execute()
.returnContent()
.asString();
JsonObject jo = JsonParser.parseString(response).getAsJsonObject();
JsonNode jo = jackson.readTree(response);
if (jo.has("error_code")) {
System.err.println(jo);
throw new TimiException(TimiCode.ERROR, jo.get("error_msg").getAsString());
throw new TimiException(TimiCode.ERROR, jo.get("error_msg").asText());
}
JsonArray ja = jo.get("trans_result").getAsJsonArray();
JsonNode ja = jo.get("trans_result");
JsonObject resultJO;
Map<String, String> result = new HashMap<>();
for (int i = 0; i < ja.size(); i++) {
resultJO = ja.get(i).getAsJsonObject();
result.put(resultJO.get("src").getAsString(), resultJO.get("dst").getAsString());
JsonNode resultJO = ja.get(i);
result.put(resultJO.get("src").asText(), resultJO.get("dst").asText());
}
wait(200);
return result;

View File

@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.git.bean.gitea;
import com.google.gson.annotations.SerializedName;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
@@ -12,6 +12,6 @@ public class Branch {
private String name;
@SerializedName("protected")
@JsonProperty("protected")
private boolean isProtected;
}

View File

@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.git.bean.gitea;
import com.google.gson.annotations.JsonAdapter;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.imyeyu.api.modules.git.util.GiteaTimestampAdapter;
import lombok.Data;
@@ -36,6 +37,7 @@ public class File {
private String lastCommitSha;
@JsonAdapter(GiteaTimestampAdapter.class)
@JsonSerialize(using = GiteaTimestampAdapter.Serializer.class)
@JsonDeserialize(using = GiteaTimestampAdapter.Deserializer.class)
private Long lastCommitterDate;
}

View File

@@ -1,9 +1,9 @@
package com.imyeyu.api.modules.git.bean.gitea;
import com.imyeyu.java.TimiJava;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.utils.Encoder;
import com.imyeyu.utils.StringInterpolator;
@@ -13,7 +13,7 @@ import java.util.Map;
* @author 夜雨
* @since 2025-06-26 16:04
*/
public enum API {
public enum GiteaAPI {
REPO_LIST("/api/v1/repos/search"),
@@ -36,7 +36,7 @@ public enum API {
final String uri;
API(String uri) {
GiteaAPI(String uri) {
this.uri = uri;
}
@@ -48,7 +48,7 @@ public enum API {
public String buildURL(Map<String, Object> argsURI, Map<String, Object> argsURL) {
SettingService settingService = TimiServerAPI.applicationContext.getBean(SettingService.class);
StringBuilder url = new StringBuilder();
url.append(settingService.getAsString(SettingKey.GIT_API));
url.append(settingService.getAsString(Setting.Module.Gitea.API));
url.append(INTERPOLATOR.inject(uri, argsURI));
if (TimiJava.isNotEmpty(argsURL)) {
url.append("?");

View File

@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.git.bean.gitea;
import com.google.gson.annotations.JsonAdapter;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.imyeyu.api.modules.git.util.GiteaTimestampAdapter;
import lombok.Data;
@@ -35,13 +36,16 @@ public class Repository {
private Boolean archived;
@JsonAdapter(GiteaTimestampAdapter.class)
@JsonSerialize(using = GiteaTimestampAdapter.Serializer.class)
@JsonDeserialize(using = GiteaTimestampAdapter.Deserializer.class)
private Long createdAt;
@JsonAdapter(GiteaTimestampAdapter.class)
@JsonSerialize(using = GiteaTimestampAdapter.Serializer.class)
@JsonDeserialize(using = GiteaTimestampAdapter.Deserializer.class)
private Long updatedAt;
@JsonAdapter(GiteaTimestampAdapter.class)
@JsonSerialize(using = GiteaTimestampAdapter.Serializer.class)
@JsonDeserialize(using = GiteaTimestampAdapter.Deserializer.class)
private Long archivedAt;
private List<String> licenses;

View File

@@ -1,13 +1,13 @@
package com.imyeyu.api.modules.git.controller;
import lombok.RequiredArgsConstructor;
import com.imyeyu.api.annotation.EnableSetting;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.git.entity.Developer;
import com.imyeyu.api.modules.git.service.DeveloperService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequiredToken;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -34,7 +34,7 @@ public class DeveloperController {
@AOPLog
@RequiredToken
@EnableSetting(value = SettingKey.ENABLE_USER_UPDATE, message = "user.data.off_service")
@EnableSetting(user = Setting.Module.User.ENABLE_PROFILE_UPDATE, message = "user.data.off_service")
@RequestRateLimit
@PostMapping("/update")
public void update(@RequestBody Developer developer) {

View File

@@ -1,24 +1,23 @@
package com.imyeyu.api.modules.git.service.implement;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.Network;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.git.bean.gitea.API;
import com.imyeyu.api.modules.git.bean.gitea.Branch;
import com.imyeyu.api.modules.git.bean.gitea.File;
import com.imyeyu.api.modules.git.bean.gitea.GiteaAPI;
import com.imyeyu.api.modules.git.bean.gitea.GiteaResponse;
import com.imyeyu.api.modules.git.bean.gitea.Repository;
import com.imyeyu.api.modules.git.service.RepositoryService;
import com.imyeyu.api.modules.gitea.entity.User;
import com.imyeyu.api.modules.gitea.service.GiteaService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.Network;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import jakarta.activation.MimetypesFileTypeMap;
@@ -56,23 +55,24 @@ import java.util.List;
@RequiredArgsConstructor
public class RepositoryServiceImplement implements RepositoryService {
private final Gson gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
private final UserService userService;
private final GiteaService giteaService;
private final SettingService settingService;
private final ObjectMapper jackson;
private User owner;
private ObjectMapper giteaJackson;
@PostConstruct
private void postConstruct() {
owner = giteaService.getOwner();
giteaJackson = jackson.copy().setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
}
@Override
public PageResult<Repository> page(Page page) {
try {
ClassicHttpResponse resp = (ClassicHttpResponse) Request.get(API.REPO_LIST.buildURL(null, new HashMap<>() {{
ClassicHttpResponse resp = (ClassicHttpResponse) Request.get(GiteaAPI.REPO_LIST.buildURL(null, new HashMap<>() {{
put("uid", owner.getId());
put("sort", "updated");
put("order", "desc");
@@ -80,8 +80,7 @@ public class RepositoryServiceImplement implements RepositoryService {
put("limit", page.getLimit());
}})).execute().returnResponse();
String respText = EntityUtils.toString(resp.getEntity());
GiteaResponse<List<Repository>> respObj = gson.fromJson(respText, new TypeToken<GiteaResponse<List<Repository>>>() {
}.getType());
GiteaResponse<List<Repository>> respObj = giteaJackson.readValue(respText, new TypeReference<>() {});
PageResult<Repository> result = new PageResult<>();
result.setTotal(Long.parseLong(resp.getHeader("X-Total-Count").getValue()));
@@ -96,11 +95,11 @@ public class RepositoryServiceImplement implements RepositoryService {
@Override
public Repository get(String repoName) {
try {
String respText = Request.get(API.REPO_GET.buildURL(new HashMap<>() {{
String respText = Request.get(GiteaAPI.REPO_GET.buildURL(new HashMap<>() {{
put("owner", owner.getName());
put("repoName", repoName);
}})).execute().returnContent().asString();
return gson.fromJson(respText, Repository.class);
return giteaJackson.readValue(respText, Repository.class);
} catch (Exception e) {
log.error("get repository error", e);
throw new TimiException(TimiCode.ERROR, "get repository error", e);
@@ -110,11 +109,11 @@ public class RepositoryServiceImplement implements RepositoryService {
@Override
public List<Branch> listBranches(String repoName) {
try {
String respText = Request.get(API.REPO_BRANCHES_LIST.buildURL(new HashMap<>() {{
String respText = Request.get(GiteaAPI.REPO_BRANCHES_LIST.buildURL(new HashMap<>() {{
put("owner", owner.getName());
put("repoName", repoName);
}})).execute().returnContent().asString();
return gson.fromJson(respText, new TypeToken<List<Branch>>() {}.getType());
return giteaJackson.readValue(respText, new TypeReference<>() {});
} catch (Exception e) {
log.error("list repository branches error", e);
throw new TimiException(TimiCode.ERROR, "list repository branches error", e);
@@ -124,14 +123,14 @@ public class RepositoryServiceImplement implements RepositoryService {
@Override
public List<File> listFile(String repoName, String branch, String path) {
try {
String respText = Request.get(API.REPO_FILE_LIST.buildURL(new HashMap<>() {{
String respText = Request.get(GiteaAPI.REPO_FILE_LIST.buildURL(new HashMap<>() {{
put("owner", owner.getName());
put("repoName", repoName);
put("path", path);
}}, new HashMap<>() {{
put("ref", branch);
}})).execute().returnContent().asString();
List<File> list = gson.fromJson(respText, new TypeToken<List<File>>() {}.getType());
List<File> list = giteaJackson.readValue(respText, new TypeReference<>() {});
// 排序
list.sort((f1, f2) -> {
if (f1.getType() == File.Type.dir && f2.getType() == File.Type.file) {
@@ -153,7 +152,7 @@ public class RepositoryServiceImplement implements RepositoryService {
@Override
public InputStream getFileRaw(String repoName, String branch, String path) {
try {
ClassicHttpResponse resp = (ClassicHttpResponse) Request.get(API.REPO_FILE_RAW.buildURL(new HashMap<>() {{
ClassicHttpResponse resp = (ClassicHttpResponse) Request.get(GiteaAPI.REPO_FILE_RAW.buildURL(new HashMap<>() {{
put("owner", owner.getName());
put("repoName", repoName);
put("path", path);
@@ -170,7 +169,7 @@ public class RepositoryServiceImplement implements RepositoryService {
@Override
public String getFileMimeType(String repoName, String branch, String path) {
try {
String repoPath = settingService.getAsString(SettingKey.GIT_REPO_PATH);
String repoPath = settingService.getAsString(Setting.Module.Gitea.REPOSITORY_PATH);
if (TimiJava.isEmpty(repoPath)) {
// 根据文件名猜测
return new MimetypesFileTypeMap().getContentType(Network.uriFileName(path));
@@ -211,7 +210,7 @@ public class RepositoryServiceImplement implements RepositoryService {
@Override
public InputStream getArchive(String repoName, String branch) {
try {
ClassicHttpResponse resp = (ClassicHttpResponse) Request.get(API.REPO_ARCHIVE.buildURL(new HashMap<>() {{
ClassicHttpResponse resp = (ClassicHttpResponse) Request.get(GiteaAPI.REPO_ARCHIVE.buildURL(new HashMap<>() {{
put("owner", owner.getName());
put("repoName", repoName);
put("archiveFormat", "%s.tar.gz".formatted(branch));

View File

@@ -1,28 +1,54 @@
package com.imyeyu.api.modules.git.util;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import java.lang.reflect.Type;
import java.io.IOException;
import java.time.Instant;
/**
* @author 夜雨
* @since 2025-06-26 11:40
*/
public class GiteaTimestampAdapter implements JsonSerializer<Long>, JsonDeserializer<Long> {
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class GiteaTimestampAdapter {
/**
*
*
* @author 夜雨
* @since 2026-04-07 22:16
*/
public static class Serializer extends JsonSerializer<Long> {
@Override
public Long deserialize(JsonElement json, Type type, JsonDeserializationContext context) {
return Instant.parse(json.getAsString()).toEpochMilli();
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null) {
gen.writeNull();
} else {
gen.writeNumber(value);
}
}
}
/**
*
*
* @author 夜雨
* @since 2026-04-07 22:19
*/
public static class Deserializer extends JsonDeserializer<Long> {
@Override
public JsonElement serialize(Long src, Type type, JsonSerializationContext context) {
return new JsonPrimitive(src);
public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
return Instant.parse(p.getText()).toEpochMilli();
}
}
}

View File

@@ -1,7 +1,8 @@
package com.imyeyu.api.modules.gitea.bean;
import com.google.gson.annotations.JsonAdapter;
import com.google.gson.annotations.SerializedName;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.imyeyu.api.modules.gitea.util.GiteaUTCTimestampAdapter;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -50,14 +51,15 @@ public class ActionLogDTO {
@Data
public static class Commit {
@SerializedName("Sha1")
@JsonProperty("Sha1")
private String sha;
@SerializedName("Message")
@JsonProperty("Message")
private String message;
@JsonAdapter(GiteaUTCTimestampAdapter.class)
@SerializedName("Timestamp")
@JsonProperty("Timestamp")
@JsonSerialize(using = GiteaUTCTimestampAdapter.Serializer.class)
@JsonDeserialize(using = GiteaUTCTimestampAdapter.Deserializer.class)
private Long timestamp;
}
}

View File

@@ -1,13 +1,14 @@
package com.imyeyu.api.modules.gitea.util;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.lang.reflect.Type;
import java.io.IOException;
import java.time.Instant;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
@@ -18,15 +19,21 @@ import java.time.ZoneOffset;
* @author 夜雨
* @since 2025-06-27 16:18
*/
public class GiteaUTCTimestampAdapter implements JsonDeserializer<Long>, JsonSerializer<Long> {
public class GiteaUTCTimestampAdapter {
public static class Deserializer extends JsonDeserializer<Long> {
@Override
public Long deserialize(JsonElement json, Type type, JsonDeserializationContext context) {
return OffsetDateTime.parse(json.getAsString()).toInstant().toEpochMilli();
public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JacksonException {
return OffsetDateTime.parse(p.getText()).toInstant().toEpochMilli();
}
}
public static class Serializer extends JsonSerializer<Long> {
@Override
public JsonElement serialize(Long timestamp, Type type, JsonSerializationContext context) {
return new JsonPrimitive(OffsetDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).toString());
public void serialize(Long timestamp, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(OffsetDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).toString());
}
}
}

View File

@@ -1,9 +1,8 @@
package com.imyeyu.api.modules.gitea.vo;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.java.TimiJava;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.modules.common.service.UserService;
@@ -34,7 +33,7 @@ public class ActionLogView {
private UserView operator;
public static ActionLogView fromDTO(ActionLogDTO dto) {
Gson gson = TimiServerAPI.applicationContext.getBean(Gson.class);
ObjectMapper jackson = TimiServerAPI.applicationContext.getBean(ObjectMapper.class);
UserService userService = TimiServerAPI.applicationContext.getBean(UserService.class);
ActionLogView view = new ActionLogView();
@@ -45,8 +44,9 @@ public class ActionLogView {
view.setOperator(userService.view((long) dto.getOperatorId()).doFilter());
{
if (TimiJava.isNotEmpty(dto.getContent())) {
JsonObject content = JsonParser.parseString(dto.getContent()).getAsJsonObject();
List<ActionLogDTO.Commit> commitList = gson.fromJson(content.get("Commits"), new TypeToken<List<ActionLogDTO.Commit>>() {}.getType());
try {
JsonNode content = jackson.readTree(dto.getContent());
List<ActionLogDTO.Commit> commitList = jackson.convertValue(content.get("Commits"), new TypeReference<>() {});
view.setCommitList(new ArrayList<>());
for (ActionLogDTO.Commit dtoCommit : commitList) {
Commit commit = new Commit();
@@ -55,6 +55,9 @@ public class ActionLogView {
commit.setCommittedAt(dtoCommit.getTimestamp());
view.getCommitList().add(commit);
}
} catch (Exception e) {
throw new IllegalStateException("read gitea action log content error", e);
}
}
}
return view;

View File

@@ -0,0 +1,8 @@
package com.imyeyu.api.modules.journal.bean;
/**
* @author 夜雨
* @since 2026-01-29 10:59
*/
public @interface RequiredUploadPermission {
}

View File

@@ -1,14 +1,17 @@
package com.imyeyu.api.modules.journal.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.bean.PreviewPage;
import com.imyeyu.api.bean.wechat.InitCodeResponse;
import com.imyeyu.api.modules.common.bean.MediaAttach;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.journal.bean.RequiredUploadPermission;
import com.imyeyu.api.modules.journal.entity.Journal;
import com.imyeyu.api.modules.journal.service.JournalService;
import com.imyeyu.api.modules.journal.util.JournalAPIInterceptor;
import com.imyeyu.api.modules.journal.vo.journal.ArchiveRequest;
import com.imyeyu.api.modules.journal.vo.journal.JournalRequest;
import com.imyeyu.api.modules.journal.vo.journal.JournalResponse;
@@ -16,10 +19,8 @@ import com.imyeyu.api.modules.journal.vo.journal.UpdateRequest;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.ArgMap;
import com.imyeyu.network.GsonRequest;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequestSingleParam;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import jakarta.validation.Valid;
@@ -51,6 +52,9 @@ public class JournalController {
private final JournalService service;
private final SettingService settingService;
private final AttachmentService attachmentService;
private final ObjectMapper jackson;
private final JournalAPIInterceptor apiInterceptor;
/**
* 初始化用户 OpenId
@@ -61,14 +65,18 @@ public class JournalController {
@AOPLog
@RequestRateLimit
@PostMapping("/openid")
public String initOpenId(@RequestSingleParam String code) {
public String initOpenId(@RequestBody String code) {
try {
ArgMap<String, String> args = new ArgMap<>();
args.put("appid", settingService.getAsString(SettingKey.JOURNAL_APP_ID));
args.put("secret", settingService.getAsString(SettingKey.JOURNAL_APP_SECRET));
args.put("appid", settingService.getAsString(Setting.Module.Journal.APP_ID));
args.put("secret", settingService.getAsString(Setting.Module.Journal.APP_SECRET));
args.put("js_code", code);
args.put("grant_type", "authorization_code");
InitCodeResponse resp = GsonRequest.get(args.toURL("https://api.weixin.qq.com/sns/jscode2session")).resultAs(InitCodeResponse.class);
String response = org.apache.hc.client5.http.fluent.Request.get(args.toURL("https://api.weixin.qq.com/sns/jscode2session"))
.execute()
.returnContent()
.asString();
InitCodeResponse resp = jackson.readValue(response, InitCodeResponse.class);
return resp.getOpenid();
} catch (Exception e) {
log.error("init WeChat openId error", e);
@@ -76,6 +84,11 @@ public class JournalController {
}
}
@PostMapping("/can-upload")
public boolean canUpload() {
return apiInterceptor.canUploadKey();
}
@AOPLog
@RequestRateLimit
@RequestMapping("/{id}")
@@ -103,6 +116,7 @@ public class JournalController {
*/
@AOPLog
@RequestRateLimit
@RequiredUploadPermission
@PostMapping("/create")
public void create(@RequestBody JournalRequest request) {
service.create(request);
@@ -115,6 +129,7 @@ public class JournalController {
*/
@AOPLog
@RequestRateLimit
@RequiredUploadPermission
@PostMapping("/update")
public void update(@RequestBody @Valid UpdateRequest request) {
service.update(request);
@@ -126,8 +141,9 @@ public class JournalController {
* @param id 记录 ID
*/
@AOPLog
@RequiredUploadPermission
@PostMapping("/delete")
public void delete(@RequestSingleParam Long id) {
public void delete(@RequestBody Long id) {
service.delete(id);
}
@@ -213,6 +229,7 @@ public class JournalController {
*/
@AOPLog
@RequestRateLimit
@RequiredUploadPermission
@PostMapping("/moment/create")
public List<Attachment> createMoment(@RequestBody String[] tempFileIds) {
return service.createMoment(tempFileIds);
@@ -260,6 +277,7 @@ public class JournalController {
*/
@AOPLog
@RequestRateLimit
@RequiredUploadPermission
@PostMapping("/moment/delete")
public void deleteMoment(@RequestBody Long[] thumbIds) {
service.deleteMoment(thumbIds);
@@ -272,6 +290,7 @@ public class JournalController {
*/
@AOPLog
@RequestRateLimit
@RequiredUploadPermission
@PostMapping("/moment/archive")
public void archiveMoment(@RequestBody ArchiveRequest request) {
service.archiveMoment(request);

View File

@@ -0,0 +1,38 @@
package com.imyeyu.api.modules.journal.controller;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.journal.bean.RequiredUploadPermission;
import com.imyeyu.spring.annotation.RequestBodyValue;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author 夜雨
* @since 2026-01-28 11:36
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/journal/tool")
public class ToolController {
private final SettingService settingService;
@GetMapping("/memo")
public String getMemo() {
return settingService.getAsString(Setting.Module.Journal.MEMO);
}
@RequiredUploadPermission
@PostMapping("/memo/update")
public void updateMemo(@RequestBodyValue String data) {
Setting setting = settingService.getByKey(Setting.Module.Journal.MEMO);
setting.setValue(data);
settingService.update(setting);
}
}

View File

@@ -1,11 +1,10 @@
package com.imyeyu.api.modules.journal.controller;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.journal.bean.RequiredUploadPermission;
import com.imyeyu.api.modules.journal.entity.Travel;
import com.imyeyu.api.modules.journal.service.TravelService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequestSingleParam;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import jakarta.validation.Valid;
@@ -31,7 +30,6 @@ import org.springframework.web.bind.annotation.RestController;
public class TravelController {
private final TravelService service;
private final AttachmentService attachmentService;
/**
* 创建旅行计划
@@ -40,6 +38,7 @@ public class TravelController {
*/
@AOPLog
@RequestRateLimit
@RequiredUploadPermission
@PostMapping("/create")
public void create(@RequestBody @Valid Travel travel) {
service.create(travel);
@@ -52,6 +51,7 @@ public class TravelController {
*/
@AOPLog
@RequestRateLimit
@RequiredUploadPermission
@PostMapping("/update")
public void update(@RequestBody @Valid Travel travel) {
service.update(travel);
@@ -64,8 +64,9 @@ public class TravelController {
*/
@AOPLog
@RequestRateLimit
@RequiredUploadPermission
@PostMapping("/delete")
public void delete(@RequestSingleParam Long id) {
public void delete(@RequestBody Long id) {
service.delete(id);
}

View File

@@ -4,11 +4,11 @@ import com.imyeyu.api.bean.PreviewPage;
import com.imyeyu.api.modules.common.bean.MediaAttach;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.journal.bean.RequiredUploadPermission;
import com.imyeyu.api.modules.journal.entity.TravelLocation;
import com.imyeyu.api.modules.journal.service.TravelLocationService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequestSingleParam;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import jakarta.validation.Valid;
@@ -45,6 +45,7 @@ public class TravelLocationController {
*/
@AOPLog
@RequestRateLimit
@RequiredUploadPermission
@PostMapping("/create")
public void create(@RequestBody @Valid TravelLocation location) {
service.create(location);
@@ -57,6 +58,7 @@ public class TravelLocationController {
*/
@AOPLog
@RequestRateLimit
@RequiredUploadPermission
@PostMapping("/update")
public void update(@RequestBody @Valid TravelLocation location) {
service.update(location);
@@ -69,8 +71,9 @@ public class TravelLocationController {
*/
@AOPLog
@RequestRateLimit
@RequiredUploadPermission
@PostMapping("/delete")
public void delete(@RequestSingleParam Long id) {
public void delete(@RequestBody Long id) {
service.delete(id);
}

View File

@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.journal.service.implement;
import com.google.gson.Gson;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.bean.MediaAttach;
import com.imyeyu.api.modules.common.bean.Metadata;
@@ -40,7 +40,7 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class JournalServiceImplement extends AbstractEntityService<Journal, Long> implements JournalService {
private final Gson gson;
private final ObjectMapper jackson;
private final SettingService settingService;
private final TempFileService tempFileService;
@@ -192,7 +192,7 @@ public class JournalServiceImplement extends AbstractEntityService<Journal, Long
thumbAttach.setBizId(journal.getId());
attachmentService.update(thumbAttach);
Metadata.ThumbImage thumbMetadata = gson.fromJson(thumbAttach.getMetadata(), Metadata.ThumbImage.class);
Metadata.ThumbImage thumbMetadata = jackson.convertValue(thumbAttach.getMetadata(), Metadata.ThumbImage.class);
Attachment sourceAttach = attachmentService.get(thumbMetadata.getSourceId());
sourceAttach.setBizType(Attachment.BizType.JOURNAL);
sourceAttach.setBizId(journal.getId());

View File

@@ -1,7 +1,8 @@
package com.imyeyu.api.modules.journal.util;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.journal.bean.RequiredUploadPermission;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
@@ -13,8 +14,11 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Set;
/**
* @author 夜雨
* @version 2023-11-23 17:09
@@ -28,20 +32,34 @@ public class JournalAPIInterceptor implements HandlerInterceptor {
private final SettingService settingService;
private String[] keys;
private Set<String> keys, openIds;
@PostConstruct
private void postConstruct() {
keys = settingService.getAsString(SettingKey.JOURNAL_KEY).split(",");
keys = settingService.getAsSet(Setting.Module.Journal.KEY);
openIds = settingService.getAsSet(Setting.Module.Journal.OPEN_ID_WHITE_LIST);
}
public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) {
String key = TimiJava.firstNotEmpty(TimiSpring.getHeader("Key"), TimiSpring.getRequestArg("key"));
for (int i = 0; i < keys.length; i++) {
if (keys[i].equals(key)) {
return true;
}
boolean requiredUploadPermission = false;
if (handler instanceof HandlerMethod handlerMethod) {
requiredUploadPermission = handlerMethod.getMethodAnnotation(RequiredUploadPermission.class) != null;
}
if (!canAccess()) {
throw new TimiException(TimiCode.PERMISSION_MISS).msgKey("invalid.key");
}
return !requiredUploadPermission || canUploadKey();
}
public boolean canAccess() {
String reqKey = TimiJava.defaultIfEmpty(TimiSpring.getHeader("Key"), TimiSpring.getRequestArg("key"));
return TimiJava.isNotEmpty(reqKey) && keys.contains(reqKey);
}
public boolean canUploadKey() {
String reqKey = TimiJava.defaultIfEmpty(TimiSpring.getHeader("Key"), TimiSpring.getRequestArg("key"));
// String reqOpenId = TimiJava.defaultIfEmpty(TimiSpring.getHeader("OpenId"), TimiSpring.getRequestArg("openid"));
// return canAccess() && reqKey.startsWith("i") && openIds.contains(reqOpenId);
return canAccess() && reqKey.startsWith("i");
}
}

View File

@@ -1,7 +1,7 @@
package com.imyeyu.api.modules.lyric.service.implement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.lyric.entity.Lyric;
@@ -50,6 +50,7 @@ public class LyricServiceImplement implements LyricService {
private final LyricMapper mapper;
private final Map<Long, String> allLyric;
private final ObjectMapper jackson;
@Override
public Lyric get(Long id) {
@@ -113,12 +114,12 @@ public class LyricServiceImplement implements LyricService {
Request request = Request.get(API_GET_SONG + Encoder.urlArgs(args));
HEADER.forEach(request::addHeader);
response = request.execute().returnContent().asString();
JsonObject data = JsonParser.parseString(response).getAsJsonObject().get("data").getAsJsonObject().get("info").getAsJsonArray().get(0).getAsJsonObject();
JsonNode data = jackson.readTree(response).path("data").path("info").get(0);
args.clear();
args.put("cmd", "100");
args.put("keyword", data.get("songname").getAsString());
args.put("hash", data.get("hash").getAsString());
args.put("keyword", data.path("songname").asText());
args.put("hash", data.path("hash").asText());
args.put("timelength", tl);
args.put("d", String.valueOf(Math.random()));
request = Request.get(API_GET_LRC + Encoder.urlArgs(args));
@@ -128,8 +129,8 @@ public class LyricServiceImplement implements LyricService {
}
Lyric lyric = new Lyric();
lyric.setSong(data.get("songname").getAsString());
lyric.setSinger(data.get("singername").getAsString());
lyric.setSong(data.path("songname").asText());
lyric.setSinger(data.path("singername").asText());
lyric.setData(response);
return lyric;
} catch (Exception e) {

View File

@@ -1,13 +1,9 @@
package com.imyeyu.api.modules.minecraft.annotation;
import com.imyeyu.api.modules.common.service.SettingService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.spring.TimiSpring;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
@@ -41,9 +37,9 @@ public class RequiredFMCServerTokenInterceptor implements HandlerInterceptor {
if (requiredFMCServerToken == null) {
return true;
}
if (!settingService.getAsString(SettingKey.FMC_SERVER_TOKEN).equals(TimiSpring.getToken())) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("token.illegal");
}
// if (!settingService.getAsString(SettingKey.FMC_SERVER_TOKEN).equals(TimiSpring.getToken())) {
// throw new TimiException(TimiCode.ARG_MISS).msgKey("token.illegal");
// }
return true;
}
return true;

View File

@@ -1,18 +1,16 @@
package com.imyeyu.api.modules.minecraft.controller;
import com.imyeyu.api.modules.blog.util.UserToken;
import com.imyeyu.api.modules.minecraft.service.FMCImageMapService;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequiredToken;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.annotation.EnableSetting;
import com.imyeyu.api.modules.blog.util.UserToken;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.minecraft.service.FMCImageMapService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequiredToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
@@ -50,7 +48,6 @@ public class MinecraftController {
*/
@AOPLog
@RequiredToken
@EnableSetting(value = SettingKey.FMC_ENABLE_IMAGE_MAP_UPLOAD, message = "minecraft.fmc_image_map.off_service")
@RequestRateLimit
@PostMapping("/imagemap/upload")
public String uploadFMCImageMap(@NotNull @RequestParam("request") MultipartFile file) {

View File

@@ -1,26 +1,23 @@
package com.imyeyu.api.modules.minecraft.controller;
import jakarta.annotation.PostConstruct;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.annotation.EnableSetting;
import com.imyeyu.api.config.RedisConfig;
import com.imyeyu.api.modules.blog.util.UserToken;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.minecraft.entity.MinecraftPlayer;
import com.imyeyu.api.modules.minecraft.service.PlayerService;
import com.imyeyu.api.modules.minecraft.vo.TokenRequest;
import com.imyeyu.api.modules.minecraft.vo.TokenResponse;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequestSingleParam;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.spring.util.RedisSerializers;
import jakarta.annotation.PostConstruct;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@@ -63,7 +60,6 @@ public class PlayerController {
* @return true 为已绑定
*/
@RequestRateLimit
@EnableSetting(value = SettingKey.FMC_PLAYER_LOGIN_ENABLE, message = "登录服务未启用")
@GetMapping("/bound/{name}")
public boolean isBound(@PathVariable("name") String name) {
return service.getByName(name) != null;
@@ -73,7 +69,7 @@ public class PlayerController {
@RequiredToken
@RequestRateLimit
@PostMapping("/bind")
public void bind(@RequestSingleParam String name) {
public void bind(@RequestBody String name) {
MinecraftPlayer player = new MinecraftPlayer();
player.setName(name);
player.setUserId(userService.getLoginUser().getId());
@@ -84,7 +80,7 @@ public class PlayerController {
@RequiredToken
@RequestRateLimit
@PostMapping("/unbind")
public void unbind(@RequestSingleParam Long id) {
public void unbind(@RequestBody Long id) {
service.listByUserId(userService.getLoginUser().getId())
.stream()
.filter(player -> player.getId().equals(id))
@@ -111,15 +107,13 @@ public class PlayerController {
@AOPLog
@RequestRateLimit
@EnableSetting(value = SettingKey.FMC_PLAYER_LOGIN_ENABLE, message = "登录服务未启用")
@PostMapping("/login")
public TokenResponse login(@RequestSingleParam Long playerId, @RequestHeader("Token") String token) {
public TokenResponse login(@RequestBody Long playerId, @RequestHeader("Token") String token) {
return service.login(playerId, token);
}
@AOPLog
@RequestRateLimit
@EnableSetting(value = SettingKey.FMC_PLAYER_LOGIN_ENABLE, message = "登录服务未启用")
@PostMapping("/login/token")
public TokenResponse genLoginToken(@Valid @RequestBody TokenRequest request) {
return service.genLoginToken(request);

View File

@@ -1,9 +1,7 @@
package com.imyeyu.api.modules.minecraft.service.implement;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.config.RedisConfig;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.entity.User;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.service.UserService;
@@ -12,6 +10,8 @@ import com.imyeyu.api.modules.minecraft.mapper.PlayerMapper;
import com.imyeyu.api.modules.minecraft.service.PlayerService;
import com.imyeyu.api.modules.minecraft.vo.TokenRequest;
import com.imyeyu.api.modules.minecraft.vo.TokenResponse;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
@@ -62,11 +62,12 @@ public class PlayerServiceImplement extends AbstractEntityService<MinecraftPlaye
}
// 自查重
List<MinecraftPlayer> playerList = listByUserId(player.getUserId());
if (settingService.getAsInt(SettingKey.FMC_MAX_BIND) <= playerList.size()) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("已达到最大绑定数量:%s 个".formatted(settingService.getAsInt(SettingKey.FMC_MAX_BIND)));
int maxValue = settingService.getAsInt(Setting.Module.ForeverMC.PLAYER_BIND_MAX);
if (maxValue <= playerList.size()) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("已达到最大绑定数量:%s 个".formatted(maxValue));
}
for (int i = 0; i < playerList.size(); i++) {
if (playerList.get(i).getName().equals(player.getName())) {
for (MinecraftPlayer minecraftPlayer : playerList) {
if (minecraftPlayer.getName().equals(player.getName())) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("已绑定此玩家名称");
}
}
@@ -113,7 +114,7 @@ public class PlayerServiceImplement extends AbstractEntityService<MinecraftPlaye
mapper.update(player);
String playerToken = UUID.randomUUID().toString();
long ttl = settingService.getAsInt(SettingKey.FMC_PLAYER_LOGIN_TOKEN_TTL) * Time.D;
long ttl = settingService.getAsTime(Setting.Module.ForeverMC.LOGIN_TOKEN_TTL);
redis.set(playerToken, playerId, ttl);
return new TokenResponse(userId, playerToken, Time.now() + ttl);
}
@@ -135,7 +136,8 @@ public class PlayerServiceImplement extends AbstractEntityService<MinecraftPlaye
user = userService.get(playerByName.getUserId());
}
if (user == null) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("未注册或绑定此玩家昵称,请到 https://%s 进行注册或绑定".formatted(settingService.getAsString(SettingKey.DOMAIN_SPACE)));
String spaceDomain = settingService.getAsString(Setting.Module.Domain.SPACE);
throw new TimiException(TimiCode.RESULT_NULL).msgKey("未注册或绑定此玩家昵称,请到 https://%s 进行注册或绑定".formatted(spaceDomain));
}
if (user.isBanning()) {
throw new TimiException(TimiCode.RESULT_BAN).msgKey("账号封禁中");
@@ -144,7 +146,7 @@ public class PlayerServiceImplement extends AbstractEntityService<MinecraftPlaye
throw new TimiException(TimiCode.ARG_BAD).msgKey("密码错误");
}
String token = UUID.randomUUID().toString();
long ttl = settingService.getAsInt(SettingKey.FMC_PLAYER_LOGIN_TOKEN_TTL) * Time.D;
long ttl = settingService.getAsTime(Setting.Module.ForeverMC.LOGIN_TOKEN_TTL);
redis.set(token, user.getId(), ttl);
return new TokenResponse(user.getId(), token, Time.now() + ttl);
}

View File

@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.mirror;
import com.google.gson.Gson;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.mirror.bean.AttachType;
@@ -47,7 +47,7 @@ public class FabricAPIMirror extends AttachmentMirror {
/** 版本匹配正则 */
private static final Pattern versionRegex = Pattern.compile("^(\\d+\\.){1,2}(\\*|\\d+)$");
private final Gson gson;
private final ObjectMapper jackson;
private final MirrorService service;
private final AttachmentService attachmentService;
@@ -100,7 +100,7 @@ public class FabricAPIMirror extends AttachmentMirror {
result.add(item);
}
}
mirror.setData(gson.toJsonTree(result));
mirror.setData(jackson.valueToTree(result));
service.update(mirror);
}

View File

@@ -1,16 +1,13 @@
package com.imyeyu.api.modules.mirror;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.ref.Ref;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.modules.mirror.data.OpenJDK;
import com.imyeyu.api.modules.mirror.entity.Mirror;
import com.imyeyu.api.modules.mirror.service.MirrorService;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.utils.OS;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -27,9 +24,9 @@ import java.util.List;
import java.util.Map;
/**
* Github JDK 镜像,仅同步下载链接等信息,不储存文件
* Github JDK 闀滃儚锛屼粎鍚屾涓嬭浇閾炬帴绛変俊鎭紝涓嶅偍瀛樻枃浠?
*
* @author 夜雨
* @author 澶滈洦
* @version 2024-06-10 10:51
*/
@Slf4j
@@ -37,10 +34,10 @@ import java.util.Map;
@RequiredArgsConstructor
public class OpenJDKGithubMirror extends AbstractMirror {
/** 版本发布列表接口,插值 {@link #REPOS_MAP} 的值 */
/** 鐗堟湰鍙戝竷鍒楄〃鎺ュ彛锛屾彃鍊?{@link #REPOS_MAP} 鐨勫€?*/
private static final String API_RELEASE = "https://api.github.com/repos/adoptium/%s/releases?page=1";
/** 版本仓库映射key 为版本value 为对应仓库,只读 */
/** 鐗堟湰浠撳簱鏄犲皠锛宬ey 涓虹増鏈紝value 涓哄搴斾粨搴擄紝鍙 */
private static final Map<String, String> REPOS_MAP = Collections.unmodifiableMap(new HashMap<>() {{
put("8", "temurin8-binaries");
put("11", "temurin11-binaries");
@@ -48,31 +45,30 @@ public class OpenJDKGithubMirror extends AbstractMirror {
put("21", "temurin21-binaries");
}});
private final Gson gson;
private final ObjectMapper jackson;
private final MirrorService service;
@Override
protected void sync(Mirror mirror) throws Exception {
mirror.setData(gson.toJsonTree(fetch()));
mirror.setData(jackson.valueToTree(fetch()));
service.update(mirror);
}
/**
* 获取 {@link OpenJDK} 列表,{@link OpenJDKMirror} 也会使用此接口
* 鑾峰彇 {@link OpenJDK} 鍒楄〃锛寋@link OpenJDKMirror} 涔熶細浣跨敤姝ゆ帴鍙?
*
* @return jdk 列表
* @throws Exception 获取异常
* @return jdk 鍒楄〃
* @throws Exception 鑾峰彇寮傚父
*/
final List<OpenJDK> fetch() throws Exception {
List<OpenJDK> result = new ArrayList<>();
for (Map.Entry<String, String> repo : REPOS_MAP.entrySet()) {
String respText = Request.get(API_RELEASE.formatted(repo.getValue())).connectTimeout(Timeout.ofSeconds(60)).execute().returnContent().asString();
JsonArray root = JsonParser.parseString(respText).getAsJsonArray();
JsonObject itemRel = null;
for (JsonElement el : root) {
itemRel = el.getAsJsonObject();
if (itemRel.get("prerelease").getAsBoolean() || itemRel.get("draft").getAsBoolean()) {
// 忽略草稿或预发布版本
JsonNode root = jackson.readTree(respText);
JsonNode itemRel = null;
for (JsonNode el : root) {
itemRel = el;
if (itemRel.path("prerelease").asBoolean() || itemRel.path("draft").asBoolean()) {
itemRel = null;
} else {
break;
@@ -81,12 +77,8 @@ public class OpenJDKGithubMirror extends AbstractMirror {
if (itemRel == null) {
throw new TimiException(TimiCode.ERROR, "not found release item for " + repo.getValue());
}
JsonArray assets = itemRel.get("assets").getAsJsonArray();
for (JsonElement asset : assets) {
JsonObject itemAsset = asset.getAsJsonObject();
// OpenJDK21U-jdk_x64_windows_hotspot_21.0.3_9.zip
String name = itemAsset.get("name").getAsString();
for (JsonNode itemAsset : itemRel.path("assets")) {
String name = itemAsset.path("name").asText();
if (!name.contains("-") || !name.contains("_")) {
continue;
}
@@ -102,14 +94,13 @@ public class OpenJDKGithubMirror extends AbstractMirror {
}
OS.Platform platform = Ref.toType(OS.Platform.class, split[2].toUpperCase());
OpenJDK.Type type = Ref.toType(OpenJDK.Type.class, split[0].toUpperCase());
if (platform != null && type != null) {
OpenJDK jdk = new OpenJDK();
jdk.setPlatform(platform);
jdk.setType(type);
jdk.setName(name);
jdk.setVersion(repo.getKey());
jdk.setData(URLDecoder.decode(itemAsset.get("browser_download_url").getAsString(), StandardCharsets.UTF_8));
jdk.setData(URLDecoder.decode(itemAsset.path("browser_download_url").asText(), StandardCharsets.UTF_8));
result.add(jdk);
}
}

View File

@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.mirror;
import com.google.gson.Gson;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.mirror.bean.AttachType;
@@ -30,7 +30,7 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class OpenJDKMirror extends AttachmentMirror {
private final Gson gson;
private final ObjectMapper jackson;
private final MirrorService service;
private final AttachmentService attachmentService;
private final OpenJDKGithubMirror githubMirror;
@@ -64,7 +64,7 @@ public class OpenJDKMirror extends AttachmentMirror {
result.add(jdk);
}
}
mirror.setData(gson.toJsonTree(result));
mirror.setData(jackson.valueToTree(result));
service.update(mirror);
}

View File

@@ -1,12 +1,12 @@
package com.imyeyu.api.modules.mirror;
import com.google.gson.Gson;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.modules.mirror.data.OpenJDK;
import com.imyeyu.api.modules.mirror.entity.Mirror;
import com.imyeyu.api.modules.mirror.service.MirrorService;
import com.imyeyu.utils.OS;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
@@ -41,18 +41,18 @@ public class OpenJDKTunaMirror extends AbstractMirror {
/** 版本列表 */
private static final String[] VERSIONS = {"8", "11", "17", "21"};
private final Gson gson;
private final ObjectMapper jackson;
private final MirrorService service;
@Override
protected void sync(Mirror mirror) throws Exception {
List<OpenJDK> result = new ArrayList<>();
for (int i = 0; i < VERSIONS.length; i++) {
for (String version : VERSIONS) {
OpenJDK.Type[] types = OpenJDK.Type.values();
for (int j = 0; j < types.length; j++) {
for (OpenJDK.Type type : types) {
OS.Platform[] platforms = OS.Platform.values();
for (int k = 0; k < platforms.length; k++) {
String url = PAGE_URL_TEMPLATE.formatted(VERSIONS[i], types[j].toString().toLowerCase(), platforms[k].toString().toLowerCase());
for (OS.Platform platform : platforms) {
String url = PAGE_URL_TEMPLATE.formatted(version, type.toString().toLowerCase(), platform.toString().toLowerCase());
Document document = Jsoup.connect(url).get();
Element fileList = document.getElementById("list");
Elements linkTDList = fileList.getElementsByClass("link");
@@ -64,10 +64,10 @@ public class OpenJDKTunaMirror extends AbstractMirror {
continue;
}
OpenJDK jdk = new OpenJDK();
jdk.setPlatform(platforms[k]);
jdk.setType(types[j]);
jdk.setPlatform(platform);
jdk.setType(type);
jdk.setName(href);
jdk.setVersion(VERSIONS[i]);
jdk.setVersion(version);
jdk.setData(url + href);
result.add(jdk);
@@ -77,7 +77,7 @@ public class OpenJDKTunaMirror extends AbstractMirror {
}
}
}
mirror.setData(gson.toJsonTree(result));
mirror.setData(jackson.valueToTree(result));
service.update(mirror);
}
}

View File

@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.mirror.controller;
import com.google.gson.JsonElement;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.modules.mirror.entity.Mirror;
import com.imyeyu.api.modules.mirror.service.MirrorService;
import com.imyeyu.api.modules.mirror.vo.MirrorView;
@@ -38,7 +38,7 @@ public class MirrorController {
*/
@RequestRateLimit
@GetMapping("/{mirrorName}")
public JsonElement get(@PathVariable String mirrorName) {
public JsonNode get(@PathVariable String mirrorName) {
return service.getByName(mirrorName).getData();
}

View File

@@ -1,9 +1,9 @@
package com.imyeyu.api.modules.mirror.entity;
import com.google.gson.JsonElement;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.spring.entity.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* 镜像
@@ -22,7 +22,7 @@ public class Mirror extends Entity {
protected String name;
/** 同步数据 */
protected JsonElement data;
protected JsonNode data;
/** 周期(分钟) */
protected int period;

View File

@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.music.core;
import com.google.gson.Gson;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
@@ -36,7 +37,7 @@ public class Middleware implements SchedulingConfigurer {
private static final String CACHE_CLEAR_CORN = "0 0/20 * * * ?";
@Autowired
private Gson gson;
private ObjectMapper jackson;
/** 缓存频道Map&lt;频道 ID, 关联频道&gt; */
private final Map<String, ChannelBinding> channels = new HashMap<>();
@@ -67,7 +68,7 @@ public class Middleware implements SchedulingConfigurer {
channelBinding.setPlayerChannel(ctx);
channelBinding.setLastActiviedAt(Time.now());
if (channelBinding.getControllerChannel() != null) {
channelBinding.getControllerChannel().writeAndFlush(new TextWebSocketFrame(gson.toJson(pkg)));
channelBinding.getControllerChannel().writeAndFlush(new TextWebSocketFrame(writeValueAsString(pkg)));
}
}
@@ -90,11 +91,19 @@ public class Middleware implements SchedulingConfigurer {
channelBinding.setControllerChannel(ctx);
channelBinding.setLastActiviedAt(Time.now());
if (pkg.getAction() != null && channelBinding.getPlayerChannel() != null) {
byte[] bytes = gson.toJson(pkg).getBytes(StandardCharsets.UTF_8);
byte[] bytes = writeValueAsString(pkg).getBytes(StandardCharsets.UTF_8);
ByteBuf buffer = Unpooled.buffer();
buffer.writeInt(bytes.length);
buffer.writeBytes(bytes);
channelBinding.getPlayerChannel().writeAndFlush(buffer);
}
}
private String writeValueAsString(Object value) {
try {
return jackson.writeValueAsString(value);
} catch (JsonProcessingException e) {
throw new IllegalStateException("write music websocket payload error", e);
}
}
}

View File

@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.music.handler;
import com.google.gson.Gson;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
@@ -26,7 +26,7 @@ import org.springframework.stereotype.Component;
public class ControllerMessageHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
@Autowired
private Gson gson;
private ObjectMapper jackson;
@Autowired
private Middleware middleware;
@@ -34,7 +34,11 @@ public class ControllerMessageHandler extends SimpleChannelInboundHandler<WebSoc
@Override
protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame msg) {
if (msg instanceof TextWebSocketFrame textWebSocketFrame) {
middleware.pushControllerData(ctx, gson.fromJson(textWebSocketFrame.text(), ControllerPackage.class));
try {
middleware.pushControllerData(ctx, jackson.readValue(textWebSocketFrame.text(), ControllerPackage.class));
} catch (Exception e) {
throw new IllegalStateException("read controller websocket payload error", e);
}
} else {
ctx.channel().writeAndFlush(WebSocketCloseStatus.INVALID_MESSAGE_TYPE).addListener(ChannelFutureListener.CLOSE);
}

View File

@@ -1,6 +1,6 @@
package com.imyeyu.api.modules.music.handler;
import com.google.gson.Gson;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
@@ -22,14 +22,18 @@ import org.springframework.stereotype.Component;
public class PlayerMessageHandler extends SimpleChannelInboundHandler<String> {
@Autowired
private Gson gson;
private ObjectMapper jackson;
@Autowired
private Middleware middleware;
@Override
protected synchronized void channelRead0(ChannelHandlerContext ctx, String result) {
middleware.pushPlayerData(ctx, gson.fromJson(result, PlayerPackage.class));
try {
middleware.pushPlayerData(ctx, jackson.readValue(result, PlayerPackage.class));
} catch (Exception e) {
throw new IllegalStateException("read player websocket payload error", e);
}
}
@Override

View File

@@ -1,5 +1,9 @@
package com.imyeyu.api.modules.music.runner;
import com.imyeyu.api.config.CORSConfig;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.music.handler.ControllerMessageHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
@@ -24,10 +28,6 @@ import io.netty.handler.stream.ChunkedWriteHandler;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.api.config.CORSConfig;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.music.handler.ControllerMessageHandler;
import org.springframework.beans.BeansException;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
@@ -63,6 +63,10 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application
}
public void run(ApplicationArguments args) {
if (settingService.not(Setting.Module.Music.ENABLE)) {
return;
}
final DefaultFullHttpResponse NFP = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND);
final DefaultFullHttpResponse BRP = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST);
@@ -72,7 +76,7 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, worker);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.localAddress(new InetSocketAddress(settingService.getAsInt(SettingKey.MUSIC_CONTROLLER_PORT)));
bootstrap.localAddress(new InetSocketAddress(settingService.getAsInt(Setting.Module.Music.CONTROLLER_PORT)));
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
@@ -86,7 +90,7 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application
@Override
public void channelRead(ChannelHandlerContext ctx, Object obj) throws Exception {
if (obj instanceof FullHttpRequest req) {
if (!req.uri().equals(settingService.getAsString(SettingKey.MUSIC_CONTROLLER_URI))) {
if (!req.uri().equals(settingService.getAsString(Setting.Module.Music.CONTROLLER_URI))) {
// 访问的路径不是 Web Socket 的端点地址,响应 404
ctx.channel().writeAndFlush(NFP).addListener(ChannelFutureListener.CLOSE);
return;
@@ -108,10 +112,10 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application
});
pipeline.addLast(new WebSocketServerCompressionHandler());
pipeline.addLast(new WebSocketServerProtocolHandler(
settingService.getAsString(SettingKey.MUSIC_CONTROLLER_URI),
settingService.getAsString(Setting.Module.Music.CONTROLLER_URI),
null,
true,
settingService.getAsInt(SettingKey.MUSIC_MAX_FRAME_LENGTH)
settingService.getAsInt(Setting.Module.Music.MAX_FRAME_LENGTH)
)
);
@@ -120,7 +124,7 @@ public class ControllerBootstrapRunner implements ApplicationRunner, Application
}
});
channel = bootstrap.bind().sync().channel();
log.info("TimiMusicRC controller service startup with " + settingService.getAsString(SettingKey.MUSIC_CONTROLLER_IP));
log.info("TimiMusicRC controller service startup with {}", settingService.getAsString(Setting.Module.Music.CONTROLLER_IP));
} catch (Exception e) {
log.error("TimiMusicRC controller service startup error", e);
}

View File

@@ -1,5 +1,8 @@
package com.imyeyu.api.modules.music.runner;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.music.handler.PlayerMessageHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
@@ -12,9 +15,6 @@ import io.netty.handler.codec.string.StringDecoder;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.music.handler.PlayerMessageHandler;
import org.springframework.beans.BeansException;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
@@ -50,19 +50,22 @@ public class PlayerBootstrapRunner implements ApplicationRunner, ApplicationList
}
public void run(ApplicationArguments args) {
if (settingService.not(Setting.Module.Music.ENABLE)) {
return;
}
ServerBootstrap bootstrap = new ServerBootstrap();
boss = new NioEventLoopGroup();
worker = new NioEventLoopGroup();
try {
bootstrap.group(boss, worker);
bootstrap.channel(NioServerSocketChannel.class);
bootstrap.localAddress(new InetSocketAddress(settingService.getAsInt(SettingKey.MUSIC_PLAYER_PORT)));
bootstrap.localAddress(new InetSocketAddress(settingService.getAsInt(Setting.Module.Music.PLAYER_PORT)));
bootstrap.childHandler(new ChannelInitializer<>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(
settingService.getAsInt(SettingKey.MUSIC_MAX_FRAME_LENGTH),
settingService.getAsInt(Setting.Module.Music.MAX_FRAME_LENGTH),
0,
4,
0,
@@ -77,7 +80,7 @@ public class PlayerBootstrapRunner implements ApplicationRunner, ApplicationList
channel = bootstrap.bind().sync().channel();
log.info("TimiMusicRC player service startup with " + settingService.getAsString(SettingKey.MUSIC_PLAYER_IP));
log.info("TimiMusicRC player service startup with {}", settingService.getAsString(Setting.Module.Music.PLAYER_IP));
} catch (Exception e) {
log.error("TimiMusicRC player service startup error", e);
}

View File

@@ -0,0 +1,142 @@
package com.imyeyu.api.modules.system.bean;
import lombok.Data;
import org.springframework.stereotype.Component;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Docker 容器状态缓存
*
* @author Codex
* @since 2026-04-06
*/
@Data
@Component
public class DockerStatusStore {
/** 容器状态映射 */
private final Map<String, Container> containers = new LinkedHashMap<>();
/**
* 容器缓存
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class Container {
/** 容器 ID */
private String id;
/** 容器名称 */
private String name;
/** 镜像 */
private String image;
/** 镜像 ID */
private String imageId;
/** 创建时间 */
private long createdAt;
/** 运行状态 */
private String state;
/** 状态描述 */
private String status;
/** 健康状态 */
private String healthStatus;
/** 启动时间 */
private String startedAt;
/** 结束时间 */
private String finishedAt;
/** 退出码 */
private Integer exitCode;
/** 重启次数 */
private int restartCount;
/** OOM 标记 */
private boolean oomKilled;
/** CPU 百分比 */
private Double cpuPercent;
/** 内存使用量 */
private Long memoryUsageBytes;
/** 内存限制 */
private Long memoryLimitBytes;
/** 内存百分比 */
private Double memoryPercent;
/** 网络接收字节 */
private Long networkRxBytes;
/** 网络发送字节 */
private Long networkTxBytes;
/** 块设备读取字节 */
private Long blockReadBytes;
/** 块设备写入字节 */
private Long blockWriteBytes;
/** 进程数 */
private Integer pids;
/** 采样时间 */
private long updatedAt;
/** 历史点 */
private final Deque<Point> history = new ArrayDeque<>();
}
/**
* 容器指标历史点
*
* @author Codex
* @since 2026-04-06
*/
@Data
public static class Point {
/** 时间 */
private long at;
/** CPU 百分比 */
private Double cpuPercent;
/** 内存使用量 */
private Long memoryUsageBytes;
/** 内存百分比 */
private Double memoryPercent;
/** 网络接收字节 */
private Long networkRxBytes;
/** 网络发送字节 */
private Long networkTxBytes;
/** 块设备读取字节 */
private Long blockReadBytes;
/** 块设备写入字节 */
private Long blockWriteBytes;
/** 进程数 */
private Integer pids;
}
}

View File

@@ -1,11 +1,11 @@
package com.imyeyu.api.modules.system.bean;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.service.SettingService;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
@@ -174,7 +174,7 @@ public class ServerFile implements Serializable {
name = file.getName();
absolutePath = file.getAbsolutePath().replaceAll("\\\\", "/");
SettingService settingService = TimiServerAPI.applicationContext.getBean(SettingService.class);
absolutePath = absolutePath.substring(settingService.getAsString(SettingKey.SYSTEM_FILE_BASE).length());
absolutePath = absolutePath.substring(settingService.getAsString(Setting.Module.System.FILE_BASE_PATH).length());
modifiedAt = file.lastModified();
isFile = file.isFile();

View File

@@ -1,7 +1,7 @@
package com.imyeyu.api.modules.system.bean;
import lombok.Data;
import com.imyeyu.java.TimiJava;
import lombok.Data;
import org.springframework.stereotype.Component;
import java.util.ArrayDeque;
@@ -11,22 +11,24 @@ import java.util.LinkedList;
import java.util.List;
/**
* 服务状态数据,所有动态数据左出右进,此对象由 IOC 托管
* 服务状态缓存
*
* <p>该对象只用于采集任务内部缓存,不直接作为接口协议返回。</p>
*
* @author 夜雨
* @version 2022-01-31 15:35
* @since 2022-01-31 15:35
*/
@Data
@Component
public class ServerStatus implements TimiJava {
/** 动态数据更新时轴 */
/** 采样时间轴 */
private LinkedList<Number> updateAxis = new LinkedList<>();
/** 系统 */
/** 操作系统 */
private OS os = new OS();
/** CPU 使用率 */
/** CPU */
private CPU cpu = new CPU();
/** 系统内存 */
@@ -35,17 +37,20 @@ public class ServerStatus implements TimiJava {
/** 网络 */
private Network network = new Network();
/** 本程序状态 */
/** 硬件 */
private Hardware hardware = new Hardware();
/** JVM */
private JVM jvm = new JVM();
/** 磁盘 */
private List<Partition> partitions = new ArrayList<>();
/** 存储分区 */
private List<StoragePartition> storagePartitions = new ArrayList<>();
/**
* 系统
* 操作系统
*
* @author 夜雨
* @version 2022-08-12 20:55
* @since 2022-08-12 20:55
*/
@Data
public static class OS {
@@ -58,10 +63,10 @@ public class ServerStatus implements TimiJava {
}
/**
* 虚拟机状态
* JVM
*
* @author 夜雨
* @version 2022-01-31 21:10
* @since 2022-01-31 21:10
*/
@Data
public static class JVM {
@@ -69,80 +74,83 @@ public class ServerStatus implements TimiJava {
/** 启动时间 */
private long bootAt;
/** JVM 名称 */
/** 名称 */
private String name;
/** JVM 版本 */
/** 版本 */
private String version;
/** 内存 */
/** GC 名称 */
private String gcName;
/** 堆内存 */
private Memory memory = new Memory();
/** 内存回收 */
private ZGC zgc = new ZGC();
/** GC 状态 */
private GC gc = new GC();
/**
* 内存
* JVM 内存
*
* @author 夜雨
* @version 2022-08-12 20:32
* @since 2022-08-12 20:32
*/
@Data
public static class Memory {
/** 初始 */
/** 初始大小 */
private long init;
/** 最大 */
/** 最大大小 */
private long max;
/** 已使用 */
/** 已用大小 */
private final Deque<Number> used = new ArrayDeque<>();
/** 已提交 */
/** 已提交大小 */
private final Deque<Number> committed = new ArrayDeque<>();
}
/**
* 内存回收
* GC 状态
*
* @author 夜雨
* @version 2022-08-12 20:32
* @since 2022-08-12 20:32
*/
@Data
public static class ZGC {
public static class GC {
/** 并发周期 */
private long syncCycles = 0;
/** 周期次数 */
private long syncCycles;
/** 累计并发周期耗时(毫秒) */
private long syncCyclesTimeTotal = 0;
/** 周期累计耗时 */
private long syncCyclesTimeTotal;
/** 累计次数 */
private long pauses = 0;
/** 暂停次数 */
private long pauses;
/** 累计回收暂停时长(毫秒) */
private long pausesTimeTotal = 0;
/** 暂停累计耗时 */
private long pausesTimeTotal;
/** 上一次回收时间 */
private long lastPauseAt = 0;
/** 上次暂停时间 */
private long lastPauseAt;
/** 上次回收大小 */
private long lastRecoverySize = 0;
/** 上次回收大小 */
private long lastRecoverySize;
/** 并发周期耗时 */
/** 周期耗时序列 */
private final Deque<Number> syncCyclesTime = new ArrayDeque<>();
/** 回收暂停时长 */
/** 暂停耗时序列 */
private final Deque<Number> pausesTime = new ArrayDeque<>();
}
}
/**
* 中央处理器
* CPU
*
* @author 夜雨
* @version 2022-01-31 15:40
* @since 2022-01-31 15:40
*/
@Data
public static class CPU {
@@ -150,19 +158,19 @@ public class ServerStatus implements TimiJava {
/** 名称 */
private String name;
/** 物理核心数 */
/** 物理核心数 */
private int coreCount;
/** 线程数量 */
/** 逻辑核心数 */
private int logicalCount;
/** 温度 */
private double temperature;
/** 系统使用 */
/** 系统用 */
private final Deque<Number> system = new ArrayDeque<>();
/** 总共已使用 */
/** 总用 */
private final Deque<Number> used = new ArrayDeque<>();
}
@@ -170,7 +178,7 @@ public class ServerStatus implements TimiJava {
* 系统内存
*
* @author 夜雨
* @version 2022-01-31 15:50
* @since 2022-01-31 15:50
*/
@Data
public static class Memory {
@@ -178,69 +186,174 @@ public class ServerStatus implements TimiJava {
/** 物理内存大小 */
private long size;
/** 交换区大小 */
/** 交换区大小 */
private long swapSize;
/** 已使用 */
/** 已用内存 */
private final Deque<Number> used = new ArrayDeque<>();
/** 交换区已使用 */
/** 已用交换分区 */
private final Deque<Number> swapUsed = new ArrayDeque<>();
}
/**
* 网卡网速
* 网
*
* @author 夜雨
* @version 2022-08-10 21:41
* @since 2022-08-10 21:41
*/
@Data
public static class Network {
/** 网卡名称 */
private String name;
/** 累计接收 */
private long recvTotal;
/** 累计发送 */
private long sentTotal;
/** 实时接收速 */
/** 实时接收速 */
private long recvNow;
/** 实时发送速 */
/** 实时发送速 */
private long sentNow;
/** MAC 地址 */
private String mac;
/** 发送 */
/** 接收包总数 */
private long recvPacketsTotal;
/** 发送包总数 */
private long sentPacketsTotal;
/** 输入错误包总数 */
private long inErrors;
/** 输出错误包总数 */
private long outErrors;
/** 输入丢弃包总数 */
private long inDrops;
/** 碰撞总数 */
private long collisions;
/** 发送序列 */
private final Deque<Number> sent = new ArrayDeque<>();
/** 接收 */
/** 接收序列 */
private final Deque<Number> recv = new ArrayDeque<>();
}
/**
* 分区
* 硬件信息
*
* @author 夜雨
* @version 2022-01-31 20:19
* @since 2026-04-06
*/
@Data
public static class Partition {
public static class Hardware {
/** 识别 UUID */
/** 风扇转速 */
private List<Integer> fanSpeeds = new ArrayList<>();
/** 主板信息 */
private Baseboard baseboard = new Baseboard();
/** BIOS 信息 */
private Firmware firmware = new Firmware();
}
/**
* 主板信息
*
* @author 夜雨
* @since 2026-04-06
*/
@Data
public static class Baseboard {
/** 厂商 */
private String manufacturer;
/** 型号 */
private String model;
/** 版本 */
private String version;
/** 序列号 */
private String serialNumber;
}
/**
* BIOS 信息
*
* @author 夜雨
* @since 2026-04-06
*/
@Data
public static class Firmware {
/** 厂商 */
private String manufacturer;
/** 名称 */
private String name;
/** 描述 */
private String description;
/** 版本 */
private String version;
/** 发布时间 */
private String releaseDate;
}
/**
* 存储分区
*
* @author 夜雨
* @since 2026-04-06
*/
@Data
public static class StoragePartition {
/** 物理磁盘名称 */
private String diskName;
/** 物理磁盘型号 */
private String diskModel;
/** 物理磁盘序列号 */
private String diskSerial;
/** 分区标识 */
private String partitionId;
/** 分区名称 */
private String partitionName;
/** 分区类型 */
private String partitionType;
/** 分区 UUID */
private String uuid;
/** 路径 */
private String path;
/** 挂载点 */
private String mountPoint;
/** 文件系统类型 */
private String type;
/** 已使用 */
private long used;
/** 总大小 */
/** 分区总空间 */
private long total;
/** 分区已用空间 */
private Long used;
/** 磁盘传输耗时 */
private long transferTimeMs;
}
}

View File

@@ -0,0 +1,169 @@
package com.imyeyu.api.modules.system.bean;
import lombok.Data;
import org.springframework.stereotype.Component;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
/**
* UPS 状态缓存
*
* @author Codex
* @since 2026-04-07
*/
@Data
@Component
public class UpsStatusStore {
/** 当前状态 */
private Snapshot current;
/** 历史点 */
private final Deque<Point> history = new ArrayDeque<>();
/**
* UPS 当前快照
*
* @author Codex
* @since 2026-04-07
*/
@Data
public static class Snapshot {
/** 采样时间 */
private long updatedAt;
/** UPS 数据时间 */
private Long upsTime;
/** 上游主机地址 */
private String hostName;
/** 厂商名称 */
private String customer;
/** 上游版本 */
private String version;
/** 设备标识 */
private String deviceId;
/** UPS 类型 */
private String upsType;
/** UPS 形态 */
private String morphological;
/** 输入输出相位 */
private String ioPhase;
/** 工作模式 */
private String workMode;
/** 输入电压 */
private Double inputVoltage;
/** 输入频率 */
private Double inputFrequency;
/** 输出电压 */
private Double outputVoltage;
/** 输出频率 */
private Double outputFrequency;
/** 输出负载百分比 */
private Integer outputLoadPercent;
/** 电池电压 */
private Double batteryVoltage;
/** 电池容量百分比 */
private Integer batteryCapacity;
/** 电池剩余时间 */
private Integer batteryRemainTime;
/** 温度 */
private Double temperature;
/** 是否旁路运行 */
private boolean bypassActive;
/** 是否关机中 */
private boolean shutdownActive;
/** 是否输出开启 */
private boolean outputOn;
/** 是否充电中 */
private boolean charging;
/** 故障类型 */
private String faultType;
/** 故障明细 */
private String faultKind;
/** 告警列表 */
private List<String> warnings = new ArrayList<>();
}
/**
* UPS 历史点
*
* @author Codex
* @since 2026-04-07
*/
@Data
public static class Point {
/** 采样时间 */
private long at;
/** UPS 数据时间 */
private Long upsTime;
/** 工作模式 */
private String workMode;
/** 输入电压 */
private Double inputVoltage;
/** 输入频率 */
private Double inputFrequency;
/** 输出电压 */
private Double outputVoltage;
/** 输出频率 */
private Double outputFrequency;
/** 输出负载百分比 */
private Integer outputLoadPercent;
/** 电池电压 */
private Double batteryVoltage;
/** 电池容量百分比 */
private Integer batteryCapacity;
/** 电池剩余时间 */
private Integer batteryRemainTime;
/** 温度 */
private Double temperature;
/** 是否旁路运行 */
private boolean bypassActive;
/** 是否关机中 */
private boolean shutdownActive;
/** 是否输出开启 */
private boolean outputOn;
}
}

View File

@@ -0,0 +1,43 @@
package com.imyeyu.api.modules.system.controller;
import com.imyeyu.api.modules.system.service.DockerService;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerHistoryView;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerStatusView;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerSummaryView;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* Docker 控制器
*
* @author Codex
* @since 2026-04-06
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/system/docker")
public class DockerController {
private final DockerService dockerService;
@GetMapping("/container")
public List<DockerContainerSummaryView> listContainers() {
return dockerService.listContainers();
}
@GetMapping("/container/{containerId}/status")
public DockerContainerStatusView getContainerStatus(@PathVariable String containerId) {
return dockerService.getContainerStatus(containerId);
}
@GetMapping("/container/{containerId}/history")
public DockerContainerHistoryView getContainerHistory(@PathVariable String containerId, @RequestParam(required = false) String window) {
return dockerService.getContainerHistory(containerId, window);
}
}

View File

@@ -1,11 +1,6 @@
package com.imyeyu.api.modules.system.controller;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.Network;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.entity.Tag;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.service.TagService;
@@ -23,6 +18,11 @@ import com.imyeyu.api.modules.system.task.async.FileUnZipAsyncTask;
import com.imyeyu.api.modules.system.task.async.FileZipAsyncTask;
import com.imyeyu.api.modules.system.util.ResourceHandler;
import com.imyeyu.api.modules.system.vo.ListFileToRequest;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.Network;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
@@ -51,6 +51,7 @@ import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.RandomAccessFile;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -187,7 +188,7 @@ public class FileController implements TimiJava, OS.FileSystem {
public void read(HttpServletRequest req, HttpServletResponse resp) {
try {
String path = req.getServletPath().substring("/system/file/read".length());
path = settingService.getAsString(SettingKey.SYSTEM_FILE_BASE) + path;
path = settingService.getAsString(Setting.Module.System.FILE_BASE_PATH) + path;
if (TimiJava.isEmpty(path)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
@@ -239,7 +240,7 @@ public class FileController implements TimiJava, OS.FileSystem {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
path = settingService.getAsString(SettingKey.SYSTEM_FILE_BASE) + path;
path = settingService.getAsString(Setting.Module.System.FILE_BASE_PATH) + path;
File file = new File(path);
if (!file.exists()) {
log.warn("preview a not exist file: {}", path);
@@ -466,8 +467,8 @@ public class FileController implements TimiJava, OS.FileSystem {
public void download(HttpServletResponse resp) {
try {
String path = TimiSpring.cutURIStartAt("/system/file/download");
path = settingService.getAsString(SettingKey.SYSTEM_FILE_BASE) + path;
File file = new File(path);
path = settingService.getAsString(Setting.Module.System.FILE_BASE_PATH) + path;
File file = Paths.get(URI.create(path)).toFile();
service.checkAccessPermission(file.getAbsolutePath());
String mimeType = new Tika().detect(file);
@@ -475,7 +476,7 @@ public class FileController implements TimiJava, OS.FileSystem {
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(file.getName()));
resp.setHeader("Accept-Ranges", "bytes");
RequestRange range = TimiSpring.requestRange(file.length());
RequestRange range = TimiSpring.getRequestRange(file.length());
if (range == null) {
// 完整文件
resp.setContentLengthLong(file.length());

View File

@@ -0,0 +1,19 @@
package com.imyeyu.api.modules.system.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
/**
*
*
* @author 夜雨
* @since 2026-04-23 16:31
*/
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/system/setting")
public class SettingController {
}

View File

@@ -2,16 +2,15 @@ package com.imyeyu.api.modules.system.controller;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.system.bean.ServerStatus;
import com.imyeyu.api.modules.system.service.SystemService;
import com.imyeyu.api.modules.system.service.StatusService;
import com.imyeyu.api.modules.system.vo.SystemStatusHistoryView;
import com.imyeyu.api.modules.system.vo.SystemStatusSnapshotView;
import com.imyeyu.api.modules.system.vo.TempAttachRequest;
import com.imyeyu.io.IO;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -23,10 +22,10 @@ import java.io.IOException;
import java.util.concurrent.Semaphore;
/**
* 服务控制接口
* 服务控制
*
* @author 夜雨
* @version 2022-01-31 22:47
* @since 2022-01-31 22:47
*/
@Slf4j
@RestController
@@ -34,85 +33,45 @@ import java.util.concurrent.Semaphore;
@RequestMapping("/system/server")
public class SystemController {
private final ServerStatus serverStatus;
private final SystemService service;
private final StatusService statusService;
private final AttachmentService attachmentService;
private final Semaphore updateSemaphore = new Semaphore(1);
private final Semaphore rebootSemaphore = new Semaphore(1);
private final Semaphore restoreSemaphore = new Semaphore(1);
/** @return 实时服务器状态 */
@RequestMapping("/status")
public ServerStatus getStatus() {
return serverStatus;
}
/**
* 更新系统
* 获取系统状态快照
*
* @param file
* @param metrics 返回指标,使用逗号分隔
* @return 状态快照
*/
@AOPLog
@PostMapping("/update")
public void update(@NotNull @RequestParam("file") MultipartFile file) {
if (updateSemaphore.tryAcquire()) {
try {
service.update(file);
} finally {
updateSemaphore.release();
}
} else {
throw new TimiException(TimiCode.ERROR_SERVICE_OFF).msgKey("TODO updating");
}
@GetMapping("/status")
public SystemStatusSnapshotView getStatus(@RequestParam(required = false) String metrics) {
return statusService.getStatus(metrics);
}
/**
* 恢复系统
*/
@AOPLog
@RequestMapping("/restore")
public void restore() {
if (restoreSemaphore.tryAcquire()) {
try {
service.restore();
} finally {
restoreSemaphore.release();
}
} else {
throw new TimiException(TimiCode.ERROR_SERVICE_OFF).msgKey("TODO updating");
}
}
/**
* 停止系统
* 获取系统状态历史
*
* @param window 历史窗口,支持 s/m/h/d 后缀
* @param metrics 返回指标,使用逗号分隔
* @return 状态历史
*/
@AOPLog
@RequestMapping("/shutdown")
public void shutdown() {
service.shutdown();
@GetMapping("/status/history")
public SystemStatusHistoryView getStatusHistory(
@RequestParam(required = false) String window,
@RequestParam(required = false) String metrics
) {
return statusService.getStatusHistory(window, metrics);
}
/**
* 重启系统
* 上传临时附件
*
* @param request 上传请求
*/
@AOPLog
@RequestMapping("/reboot")
public void reboot() {
if (rebootSemaphore.tryAcquire()) {
try {
service.reboot();
} finally {
rebootSemaphore.release();
}
} else {
throw new TimiException(TimiCode.ERROR_SERVICE_OFF).msgKey("TODO rebooting");
}
}
// TODO 临时接口
@AOPLog
@PostMapping("/attach")
public void uploadAttachment(TempAttachRequest request) {
try {

View File

@@ -7,7 +7,6 @@ import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.service.TerminalService;
import com.imyeyu.api.modules.system.vo.terminal.ExecCommand;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestSingleParam;
import com.imyeyu.spring.annotation.RequiredToken;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@@ -51,7 +50,7 @@ public class TerminalController {
*/
@RequiredToken
@PostMapping("/alive")
public boolean isAlive(@RequestSingleParam String sessionId) {
public boolean isAlive(@RequestBody String sessionId) {
return service.isAlive(sessionId);
}
@@ -64,7 +63,7 @@ public class TerminalController {
@AOPLog
@RequiredToken
@PostMapping("/fill")
public synchronized String pathFill(@RequestSingleParam String path) {
public synchronized String pathFill(@RequestBody String path) {
return service.pathFill(path);
}
@@ -94,7 +93,7 @@ public class TerminalController {
@AOPLog
@RequiredToken
@PostMapping("/close")
public void close(@RequestSingleParam String sessionId) {
public void close(@RequestBody String sessionId) {
service.close(sessionId);
}
}

View File

@@ -0,0 +1,34 @@
package com.imyeyu.api.modules.system.controller;
import com.imyeyu.api.modules.system.service.UpsService;
import com.imyeyu.api.modules.system.vo.ups.UpsHistoryView;
import com.imyeyu.api.modules.system.vo.ups.UpsStatusView;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* UPS 控制器
*
* @author Codex
* @since 2026-04-07
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/system/ups")
public class UpsController {
private final UpsService upsService;
@GetMapping("/status")
public UpsStatusView getStatus() {
return upsService.getStatus();
}
@GetMapping("/history")
public UpsHistoryView getHistory(@RequestParam(required = false) String window) {
return upsService.getHistory(window);
}
}

View File

@@ -0,0 +1,40 @@
package com.imyeyu.api.modules.system.service;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerHistoryView;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerStatusView;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerSummaryView;
import java.util.List;
/**
* Docker 查询服务
*
* @author Codex
* @since 2026-04-06
*/
public interface DockerService {
/**
* 获取容器列表
*
* @return 容器列表
*/
List<DockerContainerSummaryView> listContainers();
/**
* 获取容器状态
*
* @param containerId 容器 ID
* @return 容器状态
*/
DockerContainerStatusView getContainerStatus(String containerId);
/**
* 获取容器历史
*
* @param containerId 容器 ID
* @param window 历史窗口
* @return 容器历史
*/
DockerContainerHistoryView getContainerHistory(String containerId, String window);
}

View File

@@ -0,0 +1,30 @@
package com.imyeyu.api.modules.system.service;
import com.imyeyu.api.modules.system.vo.SystemStatusHistoryView;
import com.imyeyu.api.modules.system.vo.SystemStatusSnapshotView;
/**
* 系统状态查询服务
*
* @author Codex
* @since 2026-04-06
*/
public interface StatusService {
/**
* 获取当前状态快照
*
* @param metrics 返回指标
* @return 状态快照
*/
SystemStatusSnapshotView getStatus(String metrics);
/**
* 获取状态历史
*
* @param window 历史窗口
* @param metrics 返回指标
* @return 状态历史
*/
SystemStatusHistoryView getStatusHistory(String window, String metrics);
}

View File

@@ -1,18 +0,0 @@
package com.imyeyu.api.modules.system.service;
import org.springframework.web.multipart.MultipartFile;
/**
* @author 夜雨
* @version 2024-03-13 00:44
*/
public interface SystemService {
void update(MultipartFile file);
void restore();
void shutdown();
void reboot();
}

View File

@@ -0,0 +1,28 @@
package com.imyeyu.api.modules.system.service;
import com.imyeyu.api.modules.system.vo.ups.UpsHistoryView;
import com.imyeyu.api.modules.system.vo.ups.UpsStatusView;
/**
* UPS 查询服务
*
* @author Codex
* @since 2026-04-07
*/
public interface UpsService {
/**
* 获取 UPS 当前状态
*
* @return UPS 状态
*/
UpsStatusView getStatus();
/**
* 获取 UPS 历史
*
* @param window 历史窗口
* @return UPS 历史
*/
UpsHistoryView getHistory(String window);
}

View File

@@ -0,0 +1,171 @@
package com.imyeyu.api.modules.system.service.implement;
import com.imyeyu.api.modules.system.bean.DockerStatusStore;
import com.imyeyu.api.modules.system.service.DockerService;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerHistoryPointView;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerHistoryView;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerStatusView;
import com.imyeyu.api.modules.system.vo.docker.DockerContainerSummaryView;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
* Docker 查询服务实现
*
* @author Codex
* @since 2026-04-06
*/
@Service
@RequiredArgsConstructor
public class DockerServiceImplement implements DockerService {
private final DockerStatusStore dockerStatusStore;
@Value("${docker.engine.collect-rate-ms:10000}")
private long collectRateMs;
@Override
public List<DockerContainerSummaryView> listContainers() {
synchronized (dockerStatusStore) {
List<DockerContainerSummaryView> result = new ArrayList<>(dockerStatusStore.getContainers().size());
for (DockerStatusStore.Container container : dockerStatusStore.getContainers().values()) {
DockerContainerSummaryView item = new DockerContainerSummaryView();
item.setId(container.getId());
item.setName(container.getName());
item.setImage(container.getImage());
item.setState(container.getState());
item.setStatus(container.getStatus());
item.setHealthStatus(container.getHealthStatus());
item.setCpuPercent(container.getCpuPercent());
item.setMemoryUsageBytes(container.getMemoryUsageBytes());
item.setMemoryLimitBytes(container.getMemoryLimitBytes());
item.setMemoryPercent(container.getMemoryPercent());
item.setNetworkRxBytes(container.getNetworkRxBytes());
item.setNetworkTxBytes(container.getNetworkTxBytes());
item.setUpdatedAt(container.getUpdatedAt());
result.add(item);
}
return result;
}
}
@Override
public DockerContainerStatusView getContainerStatus(String containerId) {
synchronized (dockerStatusStore) {
DockerStatusStore.Container container = findContainer(containerId);
if (container == null) {
return null;
}
DockerContainerStatusView view = new DockerContainerStatusView();
view.setId(container.getId());
view.setName(container.getName());
view.setImage(container.getImage());
view.setImageId(container.getImageId());
view.setCreatedAt(container.getCreatedAt());
view.setState(container.getState());
view.setStatus(container.getStatus());
view.setHealthStatus(container.getHealthStatus());
view.setStartedAt(container.getStartedAt());
view.setFinishedAt(container.getFinishedAt());
view.setExitCode(container.getExitCode());
view.setRestartCount(container.getRestartCount());
view.setOomKilled(container.isOomKilled());
view.setCpuPercent(container.getCpuPercent());
view.setMemoryUsageBytes(container.getMemoryUsageBytes());
view.setMemoryLimitBytes(container.getMemoryLimitBytes());
view.setMemoryPercent(container.getMemoryPercent());
view.setNetworkRxBytes(container.getNetworkRxBytes());
view.setNetworkTxBytes(container.getNetworkTxBytes());
view.setBlockReadBytes(container.getBlockReadBytes());
view.setBlockWriteBytes(container.getBlockWriteBytes());
view.setPids(container.getPids());
view.setUpdatedAt(container.getUpdatedAt());
return view;
}
}
@Override
public DockerContainerHistoryView getContainerHistory(String containerId, String window) {
synchronized (dockerStatusStore) {
DockerStatusStore.Container container = findContainer(containerId);
if (container == null) {
return null;
}
DockerContainerHistoryView view = new DockerContainerHistoryView();
view.setId(container.getId());
view.setName(container.getName());
view.setServerTime(Time.now());
view.setSampleRateMs(collectRateMs);
long windowMs = parseWindowMs(window);
long threshold = Time.now() - windowMs;
for (DockerStatusStore.Point point : container.getHistory()) {
if (point.getAt() < threshold) {
continue;
}
if (0 == view.getFrom()) {
view.setFrom(point.getAt());
}
view.setTo(point.getAt());
DockerContainerHistoryPointView item = new DockerContainerHistoryPointView();
item.setAt(point.getAt());
item.setCpuPercent(point.getCpuPercent());
item.setMemoryUsageBytes(point.getMemoryUsageBytes());
item.setMemoryPercent(point.getMemoryPercent());
item.setNetworkRxBytes(point.getNetworkRxBytes());
item.setNetworkTxBytes(point.getNetworkTxBytes());
item.setBlockReadBytes(point.getBlockReadBytes());
item.setBlockWriteBytes(point.getBlockWriteBytes());
item.setPids(point.getPids());
view.getPoints().add(item);
}
return view;
}
}
private long parseWindowMs(String window) {
if (window == null || window.isBlank()) {
return 60L * collectRateMs;
}
String normalized = window.trim().toLowerCase();
char suffix = normalized.charAt(normalized.length() - 1);
long unit = switch (suffix) {
case 's' -> 1000L;
case 'm' -> 60_000L;
case 'h' -> 3_600_000L;
case 'd' -> 86_400_000L;
default -> collectRateMs;
};
String valueText = Character.isDigit(suffix) ? normalized : normalized.substring(0, normalized.length() - 1);
try {
return Math.max(collectRateMs, Long.parseLong(valueText) * unit);
} catch (NumberFormatException e) {
return 60L * collectRateMs;
}
}
/**
* 按容器 ID 或前缀查找容器
*
* @param containerId 容器 ID
* @return 容器缓存
*/
private DockerStatusStore.Container findContainer(String containerId) {
DockerStatusStore.Container exact = dockerStatusStore.getContainers().get(containerId);
if (exact != null) {
return exact;
}
for (DockerStatusStore.Container container : dockerStatusStore.getContainers().values()) {
if (container.getId() != null && container.getId().startsWith(containerId)) {
return container;
}
}
return null;
}
}

View File

@@ -1,26 +1,22 @@
package com.imyeyu.api.modules.system.service.implement;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.ServerFile;
import com.imyeyu.api.modules.system.bean.TransferFile;
import com.imyeyu.api.modules.system.service.AsyncTaskService;
import com.imyeyu.api.modules.system.service.FileService;
import com.imyeyu.api.modules.system.util.SystemAPIInterceptor;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.FileSyncConfig;
import com.imyeyu.api.modules.system.bean.ServerFile;
import com.imyeyu.api.modules.system.bean.TransferFile;
import com.imyeyu.api.modules.system.entity.AsyncTask;
import com.imyeyu.api.modules.system.service.AsyncTaskService;
import com.imyeyu.api.modules.system.service.FileService;
import com.imyeyu.api.modules.system.task.async.FileSyncTask;
import com.imyeyu.api.modules.system.util.SystemAPIInterceptor;
import com.imyeyu.utils.Decoder;
import com.imyeyu.utils.OS;
import jakarta.annotation.PostConstruct;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.config.CronTask;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.stereotype.Service;
@@ -33,7 +29,6 @@ import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import java.util.Map;
/**
* 文件服务,不可直接使用入参路径
@@ -70,30 +65,30 @@ public class FileServiceImplement implements TimiJava, FileService {
@PostConstruct
private void fileSyncPostConstruct() {
FileSyncConfig config = settingService.fromYaml(SettingKey.SYSTEM_FILE_SYNC, FileSyncConfig.class);
if (config.isEnable()) {
scheduledTaskRegistrar.scheduleCronTask(new CronTask(() -> {
Map<String, FileSyncConfig.Task> tasks = config.getTasks();
configTask: for (Map.Entry<String, FileSyncConfig.Task> item : tasks.entrySet()) {
if (item.getValue().isEnable()) {
List<AbstractAsyncTask> runningTaskList = asyncTaskService.listAll();
for (int i = 0; i < runningTaskList.size(); i++) {
AbstractAsyncTask runningTask = runningTaskList.get(i);
if (runningTask.getType() == AsyncTask.Type.FILE_SYNC && runningTask.getName().equals(item.getKey())) {
continue configTask;
}
}
FileSyncTask task = new FileSyncTask(item.getKey(), item.getValue());
asyncTaskService.addAsyncTask(task);
}
}
}, config.getCron()));
}
// FileSyncConfig config = settingService.fromYaml(SettingKey.SYSTEM_FILE_SYNC, FileSyncConfig.class);
// if (config.isEnable()) {
// scheduledTaskRegistrar.scheduleCronTask(new CronTask(() -> {
// Map<String, FileSyncConfig.Task> tasks = config.getTasks();
// configTask: for (Map.Entry<String, FileSyncConfig.Task> item : tasks.entrySet()) {
// if (item.getValue().isEnable()) {
// List<AbstractAsyncTask> runningTaskList = asyncTaskService.listAll();
// for (int i = 0; i < runningTaskList.size(); i++) {
// AbstractAsyncTask runningTask = runningTaskList.get(i);
// if (runningTask.getType() == AsyncTask.Type.FILE_SYNC && runningTask.getName().equals(item.getKey())) {
// continue configTask;
// }
// }
// FileSyncTask task = new FileSyncTask(item.getKey(), item.getValue());
// asyncTaskService.addAsyncTask(task);
// }
// }
// }, config.getCron()));
// }
}
@PostConstruct
private void fileFilterPostConstruct() {
option = settingService.fromYaml(SettingKey.SYSTEM_FILE_FILTER, FilterOption.class);
// option = settingService.fromYaml(SettingKey.SYSTEM_FILE_FILTER, FilterOption.class);
}
@Override
@@ -262,7 +257,7 @@ public class FileServiceImplement implements TimiJava, FileService {
}
private String getPath(String path) {
String basePath = settingService.getAsString(SettingKey.SYSTEM_FILE_BASE);
String basePath = settingService.getAsString(Setting.Module.System.FILE_BASE_PATH);
if (TimiJava.isEmpty(path)) {
return basePath;
}

View File

@@ -0,0 +1,412 @@
package com.imyeyu.api.modules.system.service.implement;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.ServerStatus;
import com.imyeyu.api.modules.system.service.StatusService;
import com.imyeyu.api.modules.system.vo.SystemStatusDataView;
import com.imyeyu.api.modules.system.vo.SystemStatusHistoryView;
import com.imyeyu.api.modules.system.vo.SystemStatusSnapshotView;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Deque;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
/**
* 系统状态查询服务实现
*
* @author Codex
* @since 2026-04-06
*/
@Service
@RequiredArgsConstructor
public class StatusServiceImplement implements StatusService {
private final ServerStatus serverStatus;
private final SettingService settingService;
@Override
public SystemStatusSnapshotView getStatus(String metrics) {
long serverTime = Time.now();
int sampleRateMs = settingService.getAsInt(Setting.Module.System.STATUS_RATE);
EnumSet<Metric> selectedMetrics = parseMetrics(metrics);
synchronized (serverStatus) {
SystemStatusSnapshotView view = new SystemStatusSnapshotView();
view.setServerTime(serverTime);
view.setSampleRateMs(sampleRateMs);
view.setSnapshot(buildSnapshot(serverTime, selectedMetrics));
return view;
}
}
@Override
public SystemStatusHistoryView getStatusHistory(String window, String metrics) {
long serverTime = Time.now();
int sampleRateMs = settingService.getAsInt(Setting.Module.System.STATUS_RATE);
EnumSet<Metric> selectedMetrics = parseMetrics(metrics);
synchronized (serverStatus) {
SystemStatusHistoryView history = buildHistory(window, sampleRateMs, selectedMetrics);
history.setServerTime(serverTime);
history.setSampleRateMs(sampleRateMs);
return history;
}
}
/**
* 构建当前快照
*
* @param serverTime 服务端时间
* @param selectedMetrics 指标集合
* @return 当前快照
*/
private SystemStatusDataView.Snapshot buildSnapshot(long serverTime, EnumSet<Metric> selectedMetrics) {
SystemStatusDataView.Snapshot snapshot = new SystemStatusDataView.Snapshot();
if (selectedMetrics.contains(Metric.OS)) {
SystemStatusDataView.OS os = new SystemStatusDataView.OS();
os.setName(serverStatus.getOs().getName());
os.setBootAt(serverStatus.getOs().getBootAt());
snapshot.setOs(os);
}
if (selectedMetrics.contains(Metric.CPU)) {
SystemStatusDataView.CPU cpu = new SystemStatusDataView.CPU();
cpu.setModel(serverStatus.getCpu().getName());
cpu.setPhysicalCores(serverStatus.getCpu().getCoreCount());
cpu.setLogicalCores(serverStatus.getCpu().getLogicalCount());
cpu.setUsageTotal(lastDouble(serverStatus.getCpu().getUsed()));
cpu.setUsageSystem(lastDouble(serverStatus.getCpu().getSystem()));
cpu.setTemperatureCelsius(serverStatus.getCpu().getTemperature());
snapshot.setCpu(cpu);
}
if (selectedMetrics.contains(Metric.MEMORY)) {
SystemStatusDataView.Memory memory = new SystemStatusDataView.Memory();
Long usedBytes = lastLong(serverStatus.getMemory().getUsed());
Long swapUsedBytes = lastLong(serverStatus.getMemory().getSwapUsed());
memory.setTotalBytes(serverStatus.getMemory().getSize());
memory.setUsedBytes(usedBytes);
memory.setSwapTotalBytes(serverStatus.getMemory().getSwapSize());
memory.setSwapUsedBytes(swapUsedBytes);
snapshot.setMemory(memory);
}
if (selectedMetrics.contains(Metric.JVM)) {
SystemStatusDataView.JVM jvm = new SystemStatusDataView.JVM();
SystemStatusDataView.GC gc = new SystemStatusDataView.GC();
jvm.setName(serverStatus.getJvm().getName());
jvm.setVersion(serverStatus.getJvm().getVersion());
jvm.setBootAt(serverStatus.getJvm().getBootAt());
jvm.setHeapInitBytes(serverStatus.getJvm().getMemory().getInit());
jvm.setHeapMaxBytes(serverStatus.getJvm().getMemory().getMax());
jvm.setHeapUsedBytes(lastLong(serverStatus.getJvm().getMemory().getUsed()));
jvm.setHeapCommittedBytes(lastLong(serverStatus.getJvm().getMemory().getCommitted()));
gc.setCollector(serverStatus.getJvm().getGcName());
gc.setCycleCount(serverStatus.getJvm().getGc().getSyncCycles());
gc.setPauseCount(serverStatus.getJvm().getGc().getPauses());
gc.setLastPauseAt(serverStatus.getJvm().getGc().getLastPauseAt());
gc.setLastRecoveredBytes(serverStatus.getJvm().getGc().getLastRecoverySize());
jvm.setGc(gc);
snapshot.setJvm(jvm);
}
if (selectedMetrics.contains(Metric.NETWORK)) {
SystemStatusDataView.Network network = new SystemStatusDataView.Network();
network.setInterfaceName(serverStatus.getNetwork().getName());
network.setMac(serverStatus.getNetwork().getMac());
network.setRxBytesPerSecond(serverStatus.getNetwork().getRecvNow());
network.setTxBytesPerSecond(serverStatus.getNetwork().getSentNow());
network.setRxTotalBytes(serverStatus.getNetwork().getRecvTotal());
network.setTxTotalBytes(serverStatus.getNetwork().getSentTotal());
network.setRxPacketsTotal(serverStatus.getNetwork().getRecvPacketsTotal());
network.setTxPacketsTotal(serverStatus.getNetwork().getSentPacketsTotal());
network.setInErrors(serverStatus.getNetwork().getInErrors());
network.setOutErrors(serverStatus.getNetwork().getOutErrors());
network.setInDrops(serverStatus.getNetwork().getInDrops());
network.setCollisions(serverStatus.getNetwork().getCollisions());
snapshot.setNetwork(network);
}
if (selectedMetrics.contains(Metric.HARDWARE)) {
SystemStatusDataView.Hardware hardware = new SystemStatusDataView.Hardware();
SystemStatusDataView.Baseboard baseboard = new SystemStatusDataView.Baseboard();
SystemStatusDataView.Firmware firmware = new SystemStatusDataView.Firmware();
hardware.setFanSpeeds(new ArrayList<>(serverStatus.getHardware().getFanSpeeds()));
baseboard.setManufacturer(serverStatus.getHardware().getBaseboard().getManufacturer());
baseboard.setModel(serverStatus.getHardware().getBaseboard().getModel());
baseboard.setVersion(serverStatus.getHardware().getBaseboard().getVersion());
baseboard.setSerialNumber(serverStatus.getHardware().getBaseboard().getSerialNumber());
firmware.setManufacturer(serverStatus.getHardware().getFirmware().getManufacturer());
firmware.setName(serverStatus.getHardware().getFirmware().getName());
firmware.setDescription(serverStatus.getHardware().getFirmware().getDescription());
firmware.setVersion(serverStatus.getHardware().getFirmware().getVersion());
firmware.setReleaseDate(serverStatus.getHardware().getFirmware().getReleaseDate());
hardware.setBaseboard(baseboard);
hardware.setFirmware(firmware);
snapshot.setHardware(hardware);
}
if (selectedMetrics.contains(Metric.STORAGE)) {
List<SystemStatusDataView.StoragePartition> storagePartitions = new ArrayList<>();
for (ServerStatus.StoragePartition partition : serverStatus.getStoragePartitions()) {
SystemStatusDataView.StoragePartition item = new SystemStatusDataView.StoragePartition();
item.setDiskName(partition.getDiskName());
item.setDiskModel(partition.getDiskModel());
item.setDiskSerial(partition.getDiskSerial());
item.setPartitionId(partition.getPartitionId());
item.setPartitionName(partition.getPartitionName());
item.setPartitionType(partition.getPartitionType());
item.setUuid(partition.getUuid());
item.setMountPoint(partition.getMountPoint());
item.setTotal(partition.getTotal());
item.setUsed(partition.getUsed());
item.setUsagePercent(toPercent(partition.getUsed(), partition.getTotal()));
item.setTransferTimeMs(partition.getTransferTimeMs());
storagePartitions.add(item);
}
snapshot.setStoragePartitions(storagePartitions);
}
return snapshot;
}
/**
* 构建历史数据
*
* @param window 历史窗口
* @param sampleRateMs 采样周期
* @param selectedMetrics 指标集合
* @return 历史数据
*/
private SystemStatusHistoryView buildHistory(String window, int sampleRateMs, EnumSet<Metric> selectedMetrics) {
SystemStatusHistoryView view = new SystemStatusHistoryView();
List<Long> axis = copyLongs(serverStatus.getUpdateAxis());
if (axis.isEmpty()) {
return view;
}
int startIndex = resolveStartIndex(axis, parseWindowMs(window, sampleRateMs));
view.setFrom(axis.get(startIndex));
view.setTo(axis.get(axis.size() - 1));
List<Double> cpuUsed = selectedMetrics.contains(Metric.CPU) ? copyDoubles(serverStatus.getCpu().getUsed()) : List.of();
List<Double> cpuSystem = selectedMetrics.contains(Metric.CPU) ? copyDoubles(serverStatus.getCpu().getSystem()) : List.of();
List<Long> memoryUsed = selectedMetrics.contains(Metric.MEMORY) ? copyLongs(serverStatus.getMemory().getUsed()) : List.of();
List<Long> swapUsed = selectedMetrics.contains(Metric.MEMORY) ? copyLongs(serverStatus.getMemory().getSwapUsed()) : List.of();
List<Long> heapUsed = selectedMetrics.contains(Metric.JVM) ? copyLongs(serverStatus.getJvm().getMemory().getUsed()) : List.of();
List<Long> heapCommitted = selectedMetrics.contains(Metric.JVM) ? copyLongs(serverStatus.getJvm().getMemory().getCommitted()) : List.of();
List<Long> gcCycleTime = selectedMetrics.contains(Metric.JVM) ? copyLongs(serverStatus.getJvm().getGc().getSyncCyclesTime()) : List.of();
List<Long> gcPauseTime = selectedMetrics.contains(Metric.JVM) ? copyLongs(serverStatus.getJvm().getGc().getPausesTime()) : List.of();
List<Long> rx = selectedMetrics.contains(Metric.NETWORK) ? toRate(copyLongs(serverStatus.getNetwork().getRecv()), sampleRateMs) : List.of();
List<Long> tx = selectedMetrics.contains(Metric.NETWORK) ? toRate(copyLongs(serverStatus.getNetwork().getSent()), sampleRateMs) : List.of();
for (int index = startIndex; index < axis.size(); index++) {
SystemStatusDataView.Point point = new SystemStatusDataView.Point();
point.setAt(axis.get(index));
if (selectedMetrics.contains(Metric.CPU)) {
point.setCpuUsagePercent(getAlignedValue(cpuUsed, axis.size(), index));
point.setCpuSystemPercent(getAlignedValue(cpuSystem, axis.size(), index));
}
if (selectedMetrics.contains(Metric.MEMORY)) {
point.setMemoryUsedBytes(getAlignedValue(memoryUsed, axis.size(), index));
point.setSwapUsedBytes(getAlignedValue(swapUsed, axis.size(), index));
}
if (selectedMetrics.contains(Metric.JVM)) {
point.setHeapUsedBytes(getAlignedValue(heapUsed, axis.size(), index));
point.setHeapCommittedBytes(getAlignedValue(heapCommitted, axis.size(), index));
point.setGcCycleTimeMs(getAlignedValue(gcCycleTime, axis.size(), index));
point.setGcPauseTimeMs(getAlignedValue(gcPauseTime, axis.size(), index));
}
if (selectedMetrics.contains(Metric.NETWORK)) {
point.setRxBytesPerSecond(getAlignedValue(rx, axis.size(), index));
point.setTxBytesPerSecond(getAlignedValue(tx, axis.size(), index));
}
view.getPoints().add(point);
}
return view;
}
/**
* 解析指标
*
* @param metrics 指标字符串
* @return 指标集合
*/
private EnumSet<Metric> parseMetrics(String metrics) {
if (metrics == null || metrics.isBlank()) {
return EnumSet.allOf(Metric.class);
}
EnumSet<Metric> selected = EnumSet.noneOf(Metric.class);
for (String metric : metrics.split(",")) {
switch (metric.trim().toLowerCase(Locale.ROOT)) {
case "os" -> selected.add(Metric.OS);
case "cpu" -> selected.add(Metric.CPU);
case "memory" -> selected.add(Metric.MEMORY);
case "jvm", "gc" -> selected.add(Metric.JVM);
case "network" -> selected.add(Metric.NETWORK);
case "storage", "disk", "disks" -> selected.add(Metric.STORAGE);
case "hardware", "board", "bios", "fan", "fans" -> selected.add(Metric.HARDWARE);
default -> {
}
}
}
return selected.isEmpty() ? EnumSet.allOf(Metric.class) : selected;
}
/**
* 解析窗口毫秒数
*
* @param window 窗口字符串
* @param sampleRateMs 采样周期
* @return 窗口毫秒数
*/
private long parseWindowMs(String window, int sampleRateMs) {
if (window == null || window.isBlank()) {
return (long) settingService.getAsInt(Setting.Module.System.STATUS_LIMIT) * sampleRateMs;
}
String normalized = window.trim().toLowerCase(Locale.ROOT);
char suffix = normalized.charAt(normalized.length() - 1);
long unit = switch (suffix) {
case 's' -> 1000L;
case 'm' -> 60_000L;
case 'h' -> 3_600_000L;
case 'd' -> 86_400_000L;
default -> sampleRateMs;
};
String valueText = Character.isDigit(suffix) ? normalized : normalized.substring(0, normalized.length() - 1);
try {
return Math.max(sampleRateMs, Long.parseLong(valueText) * unit);
} catch (NumberFormatException e) {
return (long) settingService.getAsInt(Setting.Module.System.STATUS_LIMIT) * sampleRateMs;
}
}
/**
* 解析历史起点
*
* @param axis 时间轴
* @param windowMs 窗口毫秒数
* @return 起点下标
*/
private int resolveStartIndex(List<Long> axis, long windowMs) {
long threshold = axis.get(axis.size() - 1) - windowMs;
for (int index = 0; index < axis.size(); index++) {
if (threshold <= axis.get(index)) {
return index;
}
}
return axis.size() - 1;
}
/**
* 将周期累计值转换为每秒速率
*
* @param source 原始列表
* @param sampleRateMs 采样周期
* @return 速率列表
*/
private List<Long> toRate(List<Long> source, int sampleRateMs) {
List<Long> result = new ArrayList<>(source.size());
for (Long value : source) {
result.add(value == null ? null : value * 1000 / sampleRateMs);
}
return result;
}
/**
* 获取与时间轴尾部对齐的值
*
* @param values 数据列表
* @param axisSize 时间轴长度
* @param axisIndex 时间轴下标
* @param <T> 数据类型
* @return 对齐后的值
*/
private <T> T getAlignedValue(List<T> values, int axisSize, int axisIndex) {
int valueIndex = axisIndex - (axisSize - values.size());
if (valueIndex < 0 || values.size() <= valueIndex) {
return null;
}
return values.get(valueIndex);
}
/**
* 复制长整型列表
*
* @param source 原始队列
* @return 列表
*/
private List<Long> copyLongs(Deque<Number> source) {
List<Long> result = new ArrayList<>(source.size());
for (Number number : source) {
result.add(number == null ? null : number.longValue());
}
return result;
}
/**
* 复制浮点列表
*
* @param source 原始队列
* @return 列表
*/
private List<Double> copyDoubles(Deque<Number> source) {
List<Double> result = new ArrayList<>(source.size());
for (Number number : source) {
result.add(number == null ? null : number.doubleValue());
}
return result;
}
/**
* 获取最后一个长整型值
*
* @param source 队列
* @return 值
*/
private Long lastLong(Deque<Number> source) {
Number number = source.peekLast();
return number == null ? null : number.longValue();
}
/**
* 获取最后一个浮点值
*
* @param source 队列
* @return 值
*/
private Double lastDouble(Deque<Number> source) {
Number number = source.peekLast();
return number == null ? null : number.doubleValue();
}
/**
* 计算百分比
*
* @param used 已用值
* @param total 总值
* @return 百分比
*/
private Double toPercent(Long used, long total) {
if (used == null || total <= 0) {
return null;
}
return used * 100D / total;
}
/**
* 状态指标
*
* @author Codex
* @since 2026-04-06
*/
private enum Metric {
OS,
CPU,
MEMORY,
JVM,
NETWORK,
HARDWARE,
STORAGE
}
}

View File

@@ -1,73 +0,0 @@
package com.imyeyu.api.modules.system.service.implement;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.service.SystemService;
import com.imyeyu.utils.OS;
import org.springframework.boot.SpringApplication;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import javax.naming.NoPermissionException;
import java.io.IOException;
/**
* @author 夜雨
* @version 2024-03-13 00:45
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SystemServiceImplement implements SystemService {
private final SettingService settingService;
@Override
public void update(MultipartFile file) {
try {
IO.toFile(IO.file("timi-server-api.jar"), file.getInputStream());
} catch (NoPermissionException | IOException e) {
log.error("update core error", e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO update core error");
}
}
@Override
public void restore() {
try {
IO.copy(IO.file("default.jar"), IO.file("timi-server-api.jar").getAbsolutePath());
} catch (NoPermissionException | IOException e) {
log.error("restore core error", e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO restore core error");
}
}
@Override
public void shutdown() {
if (TimiServerAPI.applicationContext instanceof AbstractApplicationContext ctx) {
ctx.close();
}
}
@Async
@Override
public void reboot() {
if (TimiServerAPI.applicationContext instanceof AbstractApplicationContext ctx) {
String command = settingService.getAsString(SettingKey.SYSTEM_REBOOT_COMMAND);
if (TimiJava.isEmpty(command)) {
throw new TimiException(TimiCode.ERROR_NPE_VARIABLE).msgKey("TODO not support reboot");
}
OS.runAfterShutdown(command);
SpringApplication.exit(ctx);
}
}
}

View File

@@ -1,29 +1,31 @@
package com.imyeyu.api.modules.system.service.implement;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArg;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.TerminalPipe;
import com.imyeyu.api.modules.system.service.TerminalService;
import com.imyeyu.api.modules.system.vo.terminal.ExecCommand;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArg;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.utils.OS;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* 指令服务
@@ -50,7 +52,8 @@ public class TerminalServiceImplement implements TerminalService, TimiJava, OS.F
@PostConstruct
private void postConstruct() {
execLogFilters.addAll(List.of(settingService.getAsString(SettingKey.SYSTEM_TERMINAL_FILTERS).split(",")));
// ArrayNode array = settingService.getAsArrayNode(Setting.Module.System.TERMINAL_FILTERS);
// execLogFilters.addAll(array.valueStream().map(JsonNode::asText).collect(Collectors.toSet()));
}
@Override

View File

@@ -0,0 +1,151 @@
package com.imyeyu.api.modules.system.service.implement;
import com.imyeyu.api.modules.system.bean.UpsStatusStore;
import com.imyeyu.api.modules.system.service.UpsService;
import com.imyeyu.api.modules.system.task.UpsStatusTask;
import com.imyeyu.api.modules.system.vo.ups.UpsHistoryPointView;
import com.imyeyu.api.modules.system.vo.ups.UpsHistoryView;
import com.imyeyu.api.modules.system.vo.ups.UpsStatusView;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* UPS 查询服务实现
*
* @author Codex
* @since 2026-04-07
*/
@Service
@RequiredArgsConstructor
public class UpsServiceImplement implements UpsService {
private final UpsStatusTask upsStatusTask;
private final UpsStatusStore upsStatusStore;
@Value("${ups.collect-rate-ms:3000}")
private long collectRateMs;
@Override
public UpsStatusView getStatus() {
ensureCurrentSnapshot();
synchronized (upsStatusStore) {
UpsStatusStore.Snapshot snapshot = upsStatusStore.getCurrent();
if (snapshot == null) {
return null;
}
UpsStatusView view = new UpsStatusView();
view.setServerTime(System.currentTimeMillis());
view.setUpsTime(snapshot.getUpsTime());
view.setHostName(snapshot.getHostName());
view.setCustomer(snapshot.getCustomer());
view.setVersion(snapshot.getVersion());
view.setDeviceId(snapshot.getDeviceId());
view.setUpsType(snapshot.getUpsType());
view.setMorphological(snapshot.getMorphological());
view.setIoPhase(snapshot.getIoPhase());
view.setWorkMode(snapshot.getWorkMode());
view.setInputVoltage(snapshot.getInputVoltage());
view.setInputFrequency(snapshot.getInputFrequency());
view.setOutputVoltage(snapshot.getOutputVoltage());
view.setOutputFrequency(snapshot.getOutputFrequency());
view.setOutputLoadPercent(snapshot.getOutputLoadPercent());
view.setBatteryVoltage(snapshot.getBatteryVoltage());
view.setBatteryCapacity(snapshot.getBatteryCapacity());
view.setBatteryRemainTime(snapshot.getBatteryRemainTime());
view.setTemperature(snapshot.getTemperature());
view.setBypassActive(snapshot.isBypassActive());
view.setShutdownActive(snapshot.isShutdownActive());
view.setOutputOn(snapshot.isOutputOn());
view.setCharging(snapshot.isCharging());
view.setFaultType(snapshot.getFaultType());
view.setFaultKind(snapshot.getFaultKind());
view.setWarnings(snapshot.getWarnings());
return view;
}
}
@Override
public UpsHistoryView getHistory(String window) {
ensureCurrentSnapshot();
UpsHistoryView view = new UpsHistoryView();
view.setServerTime(System.currentTimeMillis());
view.setSampleRateMs(collectRateMs);
long windowMs = parseWindowMs(window);
long threshold = view.getServerTime() - windowMs;
synchronized (upsStatusStore) {
for (UpsStatusStore.Point point : upsStatusStore.getHistory()) {
if (point.getAt() < threshold) {
continue;
}
if (0 == view.getFrom()) {
view.setFrom(point.getAt());
}
view.setTo(point.getAt());
UpsHistoryPointView item = new UpsHistoryPointView();
item.setAt(point.getAt());
item.setUpsTime(point.getUpsTime());
item.setWorkMode(point.getWorkMode());
item.setInputVoltage(point.getInputVoltage());
item.setInputFrequency(point.getInputFrequency());
item.setOutputVoltage(point.getOutputVoltage());
item.setOutputFrequency(point.getOutputFrequency());
item.setOutputLoadPercent(point.getOutputLoadPercent());
item.setBatteryVoltage(point.getBatteryVoltage());
item.setBatteryCapacity(point.getBatteryCapacity());
item.setBatteryRemainTime(point.getBatteryRemainTime());
item.setTemperature(point.getTemperature());
item.setBypassActive(point.isBypassActive());
item.setShutdownActive(point.isShutdownActive());
item.setOutputOn(point.isOutputOn());
view.getPoints().add(item);
}
}
return view;
}
/**
* 保证当前快照存在
*/
private void ensureCurrentSnapshot() {
synchronized (upsStatusStore) {
if (upsStatusStore.getCurrent() != null) {
return;
}
}
upsStatusTask.collectOnce();
}
/**
* 解析历史窗口
*
* @param window 历史窗口
* @return 窗口毫秒数
*/
private long parseWindowMs(String window) {
long defaultWindowMs = 24L * 60 * 60 * 1000;
if (window == null || window.isBlank()) {
return defaultWindowMs;
}
String normalized = window.trim().toLowerCase();
char suffix = normalized.charAt(normalized.length() - 1);
long unit = switch (suffix) {
case 's' -> 1000L;
case 'm' -> 60_000L;
case 'h' -> 3_600_000L;
case 'd' -> 86_400_000L;
default -> collectRateMs;
};
String valueText = Character.isDigit(suffix) ? normalized : normalized.substring(0, normalized.length() - 1);
try {
long parsed = Long.parseLong(valueText) * unit;
long maxWindowMs = UpsStatusTask.MAX_HISTORY_MS;
return Math.max(collectRateMs, Math.min(parsed, maxWindowMs));
} catch (NumberFormatException e) {
return defaultWindowMs;
}
}
}

View File

@@ -0,0 +1,319 @@
package com.imyeyu.api.modules.system.task;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.imyeyu.api.modules.system.bean.DockerStatusStore;
import com.imyeyu.api.modules.system.util.DockerEngineClient;
import com.imyeyu.java.TimiJava;
import com.imyeyu.utils.Time;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.PeriodicTrigger;
import org.springframework.stereotype.Service;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Docker 容器状态采集任务
*
* @author Codex
* @since 2026-04-06
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class DockerStatusTask implements SchedulingConfigurer {
private final DockerEngineClient dockerEngineClient;
private final DockerStatusStore dockerStatusStore;
@Value("${docker.engine.collect-enabled:false}")
private boolean collectEnabled;
@Value("${docker.engine.collect-rate-ms:10000}")
private long collectRateMs;
@Value("${docker.engine.history-limit:120}")
private int historyLimit;
@Override
public void configureTasks(@NotNull ScheduledTaskRegistrar taskRegistrar) {
if (!collectEnabled) {
return;
}
PeriodicTrigger trigger = new PeriodicTrigger(collectRateMs, TimeUnit.MILLISECONDS);
trigger.setInitialDelay(0);
taskRegistrar.addTriggerTask(this::collect, trigger);
}
private void collect() {
try {
ArrayNode containers = (ArrayNode) dockerEngineClient.getJson("/containers/json", DockerEngineClient.query("all", "true"));
long now = Time.now();
Map<String, DockerStatusStore.Container> collectedContainers = new LinkedHashMap<>();
for (JsonNode summary : containers) {
try {
String containerId = getAsString(summary, "Id");
DockerStatusStore.Container container = new DockerStatusStore.Container();
if (TimiJava.isEmpty(container)) {
continue;
}
updateContainerSummary(container, summary);
updateContainerInspect(containerId, container);
updateContainerStats(containerId, container, now);
collectedContainers.put(container.getId(), container);
} catch (Exception e) {
log.error("collect docker container item error", e);
}
}
synchronized (dockerStatusStore) {
Set<String> activeIds = new HashSet<>(collectedContainers.keySet());
for (Map.Entry<String, DockerStatusStore.Container> item : collectedContainers.entrySet()) {
DockerStatusStore.Container container = dockerStatusStore.getContainers().computeIfAbsent(item.getKey(), key -> new DockerStatusStore.Container());
applyCollectedContainer(container, item.getValue());
}
dockerStatusStore.getContainers().entrySet().removeIf(item -> !activeIds.contains(item.getKey()));
}
} catch (Exception e) {
log.error("collect docker container status error", e);
}
}
private void applyCollectedContainer(DockerStatusStore.Container target, DockerStatusStore.Container source) {
target.setId(source.getId());
target.setName(source.getName());
target.setImage(source.getImage());
target.setImageId(source.getImageId());
target.setCreatedAt(source.getCreatedAt());
target.setState(source.getState());
target.setStatus(source.getStatus());
target.setHealthStatus(source.getHealthStatus());
target.setStartedAt(source.getStartedAt());
target.setFinishedAt(source.getFinishedAt());
target.setExitCode(source.getExitCode());
target.setRestartCount(source.getRestartCount());
target.setOomKilled(source.isOomKilled());
target.setCpuPercent(source.getCpuPercent());
target.setMemoryUsageBytes(source.getMemoryUsageBytes());
target.setMemoryLimitBytes(source.getMemoryLimitBytes());
target.setMemoryPercent(source.getMemoryPercent());
target.setNetworkRxBytes(source.getNetworkRxBytes());
target.setNetworkTxBytes(source.getNetworkTxBytes());
target.setBlockReadBytes(source.getBlockReadBytes());
target.setBlockWriteBytes(source.getBlockWriteBytes());
target.setPids(source.getPids());
target.setUpdatedAt(source.getUpdatedAt());
DockerStatusStore.Point point = source.getHistory().peekLast();
if (point != null) {
target.getHistory().addLast(point);
while (historyLimit < target.getHistory().size()) {
target.getHistory().pollFirst();
}
}
}
private void updateContainerSummary(DockerStatusStore.Container container, JsonNode summary) {
container.setId(getAsString(summary, "Id"));
container.setName(trimContainerName(readFirstArrayText(summary, "Names")));
container.setImage(getAsString(summary, "Image"));
container.setImageId(getAsString(summary, "ImageID"));
container.setCreatedAt(getAsLong(summary, "Created") * 1000);
container.setState(getAsString(summary, "State"));
container.setStatus(getAsString(summary, "Status"));
}
private void updateContainerInspect(String containerId, DockerStatusStore.Container container) {
JsonNode inspect = dockerEngineClient.getJson("/containers/%s/json".formatted(containerId), Map.of());
JsonNode state = getAsObject(inspect, "State");
container.setStartedAt(getAsString(state, "StartedAt"));
container.setFinishedAt(getAsString(state, "FinishedAt"));
container.setExitCode(getAsInteger(state, "ExitCode"));
container.setRestartCount(getAsInteger(inspect, "RestartCount", 0));
container.setOomKilled(getAsBoolean(state, "OOMKilled"));
JsonNode health = getAsObject(state, "Health");
container.setHealthStatus(health == null ? null : getAsString(health, "Status"));
}
private void updateContainerStats(String containerId, DockerStatusStore.Container container, long now) {
JsonNode stats = dockerEngineClient.getJson("/containers/%s/stats".formatted(containerId), DockerEngineClient.query("stream", "false"));
Double cpuPercent = calculateCpuPercent(stats);
Long memoryUsageBytes = getNestedLong(stats, "memory_stats", "usage");
Long memoryLimitBytes = getNestedLong(stats, "memory_stats", "limit");
Double memoryPercent = null;
if (memoryUsageBytes != null && memoryLimitBytes != null && 0 < memoryLimitBytes) {
memoryPercent = memoryUsageBytes * 100D / memoryLimitBytes;
}
long networkRxBytes = 0L;
long networkTxBytes = 0L;
JsonNode networks = getAsObject(stats, "networks");
if (networks != null) {
for (Map.Entry<String, JsonNode> item : (Iterable<Map.Entry<String, JsonNode>>) networks::fields) {
JsonNode network = item.getValue();
networkRxBytes += getAsLong(network, "rx_bytes", 0L);
networkTxBytes += getAsLong(network, "tx_bytes", 0L);
}
}
long blockReadBytes = 0L;
long blockWriteBytes = 0L;
JsonNode blkioStats = getAsObject(stats, "blkio_stats");
ArrayNode ioServiceBytes = blkioStats == null ? null : getAsArray(blkioStats, "io_service_bytes_recursive");
if (ioServiceBytes != null) {
for (JsonNode io : ioServiceBytes) {
String op = getAsString(io, "op");
long value = getAsLong(io, "value", 0L);
if ("Read".equalsIgnoreCase(op)) {
blockReadBytes += value;
} else if ("Write".equalsIgnoreCase(op)) {
blockWriteBytes += value;
}
}
}
Integer pids = getNestedInteger(stats, "pids_stats", "current");
container.setCpuPercent(cpuPercent);
container.setMemoryUsageBytes(memoryUsageBytes);
container.setMemoryLimitBytes(memoryLimitBytes);
container.setMemoryPercent(memoryPercent);
container.setNetworkRxBytes(networkRxBytes);
container.setNetworkTxBytes(networkTxBytes);
container.setBlockReadBytes(blockReadBytes);
container.setBlockWriteBytes(blockWriteBytes);
container.setPids(pids);
container.setUpdatedAt(now);
DockerStatusStore.Point point = new DockerStatusStore.Point();
point.setAt(now);
point.setCpuPercent(cpuPercent);
point.setMemoryUsageBytes(memoryUsageBytes);
point.setMemoryPercent(memoryPercent);
point.setNetworkRxBytes(networkRxBytes);
point.setNetworkTxBytes(networkTxBytes);
point.setBlockReadBytes(blockReadBytes);
point.setBlockWriteBytes(blockWriteBytes);
point.setPids(pids);
container.getHistory().addLast(point);
while (historyLimit < container.getHistory().size()) {
container.getHistory().pollFirst();
}
}
private Double calculateCpuPercent(JsonNode stats) {
Long cpuTotal = getNestedLong(stats, "cpu_stats", "cpu_usage", "total_usage");
Long preCpuTotal = getNestedLong(stats, "precpu_stats", "cpu_usage", "total_usage");
Long systemTotal = getNestedLong(stats, "cpu_stats", "system_cpu_usage");
Long preSystemTotal = getNestedLong(stats, "precpu_stats", "system_cpu_usage");
Integer onlineCpus = getNestedInteger(stats, "cpu_stats", "online_cpus");
if (onlineCpus == null || onlineCpus <= 0) {
ArrayNode perCpuUsage = getNestedArray(stats, "cpu_stats", "cpu_usage", "percpu_usage");
onlineCpus = perCpuUsage == null ? 1 : perCpuUsage.size();
}
if (cpuTotal == null || preCpuTotal == null || systemTotal == null || preSystemTotal == null) {
return null;
}
long cpuDelta = cpuTotal - preCpuTotal;
long systemDelta = systemTotal - preSystemTotal;
if (cpuDelta <= 0 || systemDelta <= 0) {
return 0D;
}
return cpuDelta * 100D * onlineCpus / systemDelta;
}
private JsonNode getAsObject(JsonNode source, String key) {
if (source == null || !source.has(key) || source.get(key).isNull() || !source.get(key).isObject()) {
return null;
}
return source.get(key);
}
private ArrayNode getAsArray(JsonNode source, String key) {
if (source == null || !source.has(key) || !source.get(key).isArray()) {
return null;
}
return (ArrayNode) source.get(key);
}
private ArrayNode getNestedArray(JsonNode source, String... keys) {
JsonNode current = source;
for (String key : keys) {
if (current == null || !current.isObject() || !current.has(key)) {
return null;
}
current = current.get(key);
}
return current != null && current.isArray() ? (ArrayNode) current : null;
}
private Long getNestedLong(JsonNode source, String... keys) {
JsonNode current = source;
for (String key : keys) {
if (current == null || !current.isObject() || !current.has(key)) {
return null;
}
current = current.get(key);
}
return current == null || current.isNull() ? null : current.asLong();
}
private Integer getNestedInteger(JsonNode source, String... keys) {
Long value = getNestedLong(source, keys);
return value == null ? null : value.intValue();
}
private String readFirstArrayText(JsonNode source, String key) {
ArrayNode array = getAsArray(source, key);
if (array == null || array.isEmpty()) {
return null;
}
return array.get(0).asText();
}
private String getAsString(JsonNode source, String key) {
if (source == null || !source.has(key) || source.get(key).isNull()) {
return null;
}
return source.get(key).asText();
}
private long getAsLong(JsonNode source, String key) {
return getAsLong(source, key, 0L);
}
private long getAsLong(JsonNode source, String key, long defaultValue) {
if (source == null || !source.has(key) || source.get(key).isNull()) {
return defaultValue;
}
return source.get(key).asLong();
}
private Integer getAsInteger(JsonNode source, String key) {
if (source == null || !source.has(key) || source.get(key).isNull()) {
return null;
}
return source.get(key).asInt();
}
private int getAsInteger(JsonNode source, String key, int defaultValue) {
if (source == null || !source.has(key) || source.get(key).isNull()) {
return defaultValue;
}
return source.get(key).asInt();
}
private boolean getAsBoolean(JsonNode source, String key) {
return source != null && source.has(key) && !source.get(key).isNull() && source.get(key).asBoolean();
}
private String trimContainerName(String name) {
if (name == null) {
return null;
}
return name.startsWith("/") ? name.substring(1) : name;
}
}

View File

@@ -1,235 +1,72 @@
package com.imyeyu.api.modules.system.task;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.ServerStatus;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import com.imyeyu.api.modules.system.task.status.StatusCollector;
import com.imyeyu.utils.Time;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.utils.OS;
import com.imyeyu.utils.Time;
import com.imyeyu.java.TimiJava;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.ServerStatus;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Service;
import oshi.SystemInfo;
import oshi.hardware.CentralProcessor;
import oshi.hardware.GlobalMemory;
import oshi.hardware.HardwareAbstractionLayer;
import oshi.hardware.NetworkIF;
import oshi.software.os.OSFileStore;
import oshi.software.os.OperatingSystem;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryMXBean;
import java.util.Deque;
import java.util.List;
/**
* 服务状态集任务
* 服务状态集任务
*
* @author 夜雨
* @version 2022-01-31 15:18
* @since 2022-01-31 15:18
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ServerStatusTask implements SchedulingConfigurer, TimiJava {
public class ServerStatusTask implements SchedulingConfigurer {
private final ServerStatus status;
private final SettingService settingService;
private GlobalMemory globalMemory; // 内存
private MemoryMXBean jvmMemory; // JVM 内存
private OperatingSystem os; // 操作系统
private CentralProcessor processor; // 中央处理器
private HardwareAbstractionLayer hardware; // 硬件
private long[] lastCPUTicks; // CPU 上一时刻状态
private long lastLinuxAPIMemoryUsed; // 上一周期 JVM 内存大小
private final List<StatusCollector> statusCollectors;
@Override
public void configureTasks(@NotNull ScheduledTaskRegistrar taskRegistrar) {
lastLinuxAPIMemoryUsed = -1;
SystemInfo systemInfo = new SystemInfo();
HardwareAbstractionLayer hardware = systemInfo.getHardware();
// 系统信息
SystemInfo system = new SystemInfo();
// 硬件信息
hardware = system.getHardware();
// 操作系统
os = system.getOperatingSystem();
// ---------- 静态数据 ----------
// 系统
status.getOs().setName(OS.NAME);
status.getOs().setBootAt(os.getSystemBootTime() * 1000);
// JVM
jvmMemory = ManagementFactory.getMemoryMXBean();
status.getJvm().setBootAt(Time.now());
status.getJvm().setName(System.getProperty("java.vm.name"));
status.getJvm().setVersion(System.getProperty("java.version"));
// CPU
processor = hardware.getProcessor();
status.getCpu().setName(processor.getProcessorIdentifier().getName().trim());
status.getCpu().setCoreCount(processor.getPhysicalProcessorCount());
status.getCpu().setLogicalCount(processor.getLogicalProcessorCount());
// 内存
globalMemory = hardware.getMemory();
status.getMemory().setSize(globalMemory.getTotal());
status.getMemory().setSwapSize(globalMemory.getVirtualMemory().getSwapTotal());
// 网卡
List<NetworkIF> networkIFs = hardware.getNetworkIFs();
{
NetworkIF networkIF;
boolean isFound = false;
for (int i = 0; i < networkIFs.size(); i++) {
networkIF = networkIFs.get(i);
if (networkIF.getMacaddr().equals(settingService.getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC))) {
status.getNetwork().setMac(networkIF.getMacaddr());
status.getNetwork().setRecvTotal(networkIF.getBytesRecv());
status.getNetwork().setSentTotal(networkIF.getBytesSent());
isFound = true;
break;
}
}
if (!isFound) {
log.error("not found setting networkIF MAC: %s" + settingService.getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC));
for (int i = 0; i < networkIFs.size(); i++) {
log.info("Network Interface: {} -> {}", networkIFs.get(i).getMacaddr(), networkIFs.get(i).getDisplayName());
}
StatusCollectContext context = new StatusCollectContext(
status,
hardware.getMemory(),
ManagementFactory.getMemoryMXBean(),
settingService,
hardware.getComputerSystem(),
systemInfo.getOperatingSystem(),
hardware.getProcessor(),
hardware,
Time.now()
);
synchronized (status) {
for (StatusCollector collector : statusCollectors) {
collector.initialize(context);
}
}
taskRegistrar.addTriggerTask(() -> {
long now = Time.now();
// ---------- JVM 内存 ----------
long nowLinuxAPIMemoryUsed = jvmMemory.getHeapMemoryUsage().getUsed();
long linuxAPIMemoryDiff = Math.abs(nowLinuxAPIMemoryUsed - lastLinuxAPIMemoryUsed);
lastLinuxAPIMemoryUsed = nowLinuxAPIMemoryUsed;
status.getJvm().getMemory().setInit(jvmMemory.getHeapMemoryUsage().getInit());
status.getJvm().getMemory().setMax(jvmMemory.getHeapMemoryUsage().getMax());
putDeque(status.getJvm().getMemory().getUsed(), nowLinuxAPIMemoryUsed);
putDeque(status.getJvm().getMemory().getCommitted(), jvmMemory.getHeapMemoryUsage().getCommitted());
// ---------- JVM GC ----------
long gcSyncCycles = 0;
long gcSyncCyclesTime = 0;
long gcPauses = 0;
long gcPausesTime = 0;
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
gcSyncCycles += gc.getCollectionCount();
gcSyncCyclesTime += gc.getCollectionTime();
switch (gc.getName()) {
case "ZGC Cycles" -> {
gcSyncCycles += gc.getCollectionCount();
gcSyncCyclesTime += gc.getCollectionTime();
synchronized (status) {
context.setCollectAt(Time.now());
for (StatusCollector collector : statusCollectors) {
collector.collect(context);
}
case "ZGC Pauses" -> {
gcPauses += gc.getCollectionCount();
gcPausesTime += gc.getCollectionTime();
if (status.getJvm().getZgc().getPauses() < gcPauses) {
// 发生 GC 回收
status.getJvm().getZgc().setLastPauseAt(now);
status.getJvm().getZgc().setLastRecoverySize(linuxAPIMemoryDiff);
status.getUpdateAxis().addLast(context.getCollectAt());
if (settingService.getAsInt(Setting.Module.System.STATUS_LIMIT) < status.getUpdateAxis().size()) {
status.getUpdateAxis().pollFirst();
}
}
}
}
putDeque(status.getJvm().getZgc().getSyncCyclesTime(), gcSyncCyclesTime - status.getJvm().getZgc().getSyncCyclesTimeTotal());
putDeque(status.getJvm().getZgc().getPausesTime(), gcPausesTime - status.getJvm().getZgc().getPausesTimeTotal());
status.getJvm().getZgc().setSyncCycles(gcSyncCycles);
status.getJvm().getZgc().setSyncCyclesTimeTotal(gcSyncCyclesTime);
status.getJvm().getZgc().setPauses(gcPauses);
status.getJvm().getZgc().setPausesTimeTotal(gcPausesTime);
// ---------- CPU ----------
if (lastCPUTicks != null) {
long[] ticks = processor.getSystemCpuLoadTicks();
long user = ticks[CentralProcessor.TickType.USER.getIndex()] - lastCPUTicks[CentralProcessor.TickType.USER.getIndex()];
long nice = ticks[CentralProcessor.TickType.NICE.getIndex()] - lastCPUTicks[CentralProcessor.TickType.NICE.getIndex()];
long sys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()] - lastCPUTicks[CentralProcessor.TickType.SYSTEM.getIndex()];
long idle = ticks[CentralProcessor.TickType.IDLE.getIndex()] - lastCPUTicks[CentralProcessor.TickType.IDLE.getIndex()];
long ioWait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()] - lastCPUTicks[CentralProcessor.TickType.IOWAIT.getIndex()];
long irq = ticks[CentralProcessor.TickType.IRQ.getIndex()] - lastCPUTicks[CentralProcessor.TickType.IRQ.getIndex()];
long softIRQ = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()] - lastCPUTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()];
long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()] - lastCPUTicks[CentralProcessor.TickType.STEAL.getIndex()];
long total = user + nice + sys + idle + ioWait + irq + softIRQ + steal;
putDeque(status.getCpu().getSystem(), 100D * sys / total);
putDeque(status.getCpu().getUsed(), 100 - 100D * idle / total);
}
lastCPUTicks = processor.getSystemCpuLoadTicks();
status.getCpu().setTemperature(hardware.getSensors().getCpuTemperature());
// ---------- 内存 ----------
putDeque(status.getMemory().getUsed(), globalMemory.getTotal() - globalMemory.getAvailable());
putDeque(status.getMemory().getSwapUsed(), globalMemory.getVirtualMemory().getSwapUsed());
// ---------- 网络 ----------
networkIFs.clear();
networkIFs.addAll(hardware.getNetworkIFs());
NetworkIF networkIF;
for (int i = 0; i < networkIFs.size(); i++) {
networkIF = networkIFs.get(i);
if (networkIF.getMacaddr().equals(settingService.getAsString(SettingKey.SYSTEM_STATUS_NETWORK_MAC))) {
long recv = networkIF.getBytesRecv() - status.getNetwork().getRecvTotal();
long sent = networkIF.getBytesSent() - status.getNetwork().getSentTotal();
status.getNetwork().setRecvNow(recv / (settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE) / 1000));
status.getNetwork().setSentNow(sent / (settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE) / 1000));
status.getNetwork().setRecvTotal(networkIF.getBytesRecv());
status.getNetwork().setSentTotal(networkIF.getBytesSent());
putDeque(status.getNetwork().getRecv(), recv);
putDeque(status.getNetwork().getSent(), sent);
break;
}
}
// ---------- 磁盘分区 ----------
ServerStatus.Partition partition;
status.getPartitions().clear();
// 分区从文件系统获取,而非物理分区
List<OSFileStore> fileStores = os.getFileSystem().getFileStores();
for (OSFileStore fileStore : fileStores) {
partition = new ServerStatus.Partition();
partition.setUuid(fileStore.getUUID());
partition.setPath(fileStore.getMount());
partition.setType(fileStore.getType());
partition.setUsed(fileStore.getTotalSpace() - fileStore.getUsableSpace());
partition.setTotal(fileStore.getTotalSpace());
status.getPartitions().add(partition);
}
// ---------- 更新时轴 ----------
putDeque(status.getUpdateAxis(), Time.now());
}, tc -> new CronTrigger("0/%s * * * * ?".formatted(settingService.getAsInt(SettingKey.SYSTEM_STATUS_RATE) / 1000)).nextExecution(tc));
}
/**
* 有所限制地添加队列数据,达到配置 {@link SettingKey#SYSTEM_STATUS_LIMIT} 个时移除最旧的
*
* @param deque 队列
* @param t 数据
* @param <T> 数据类型
*/
private <T> void putDeque(Deque<T> deque, T t) {
deque.addLast(t);
if (settingService.getAsInt(SettingKey.SYSTEM_STATUS_LIMIT) < deque.size()) {
deque.pollFirst();
}
}, triggerContext -> new CronTrigger("0/%s * * * * ?".formatted(settingService.getAsInt(Setting.Module.System.STATUS_RATE) / 1000)).nextExecution(triggerContext));
}
}

View File

@@ -1,12 +1,12 @@
package com.imyeyu.api.modules.system.task;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.TerminalPipe;
import com.imyeyu.api.modules.system.service.TerminalService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
@@ -30,10 +30,10 @@ public class TerminalTask {
private void run() {
service.listTerminalPipe().entrySet().removeIf(entry -> {
if (entry.getValue().getStatus() == TerminalPipe.Status.DIED) {
log.info("kill the died terminal session: " + entry.getKey());
log.info("kill the died terminal session: {}", entry.getKey());
return true;
}
long diedAt = entry.getValue().getLastExecAt() + Time.M * settingService.getAsInt(SettingKey.SYSTEM_TERMINAL_TTL);
long diedAt = entry.getValue().getLastExecAt() + settingService.getAsTime(Setting.Module.System.TERMINAL_TTL);
if (diedAt < Time.now()) {
entry.getValue().destroy();
log.info("kill the timeout terminal session [DIED at: {}]: {}", Time.toDateTime(diedAt), entry.getKey());

View File

@@ -0,0 +1,89 @@
package com.imyeyu.api.modules.system.task;
import com.imyeyu.api.modules.system.bean.UpsStatusStore;
import com.imyeyu.api.modules.system.util.UpsStatusClient;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.PeriodicTrigger;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* UPS 状态采集任务
*
* @author Codex
* @since 2026-04-07
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UpsStatusTask implements SchedulingConfigurer {
public static final long MAX_HISTORY_MS = 3L * 24 * 60 * 60 * 1000;
private final UpsStatusClient upsStatusClient;
private final UpsStatusStore upsStatusStore;
@Value("${ups.collect-enabled:false}")
private boolean collectEnabled;
@Value("${ups.collect-rate-ms:60000}")
private long collectRateMs;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
if (!collectEnabled) {
return;
}
PeriodicTrigger trigger = new PeriodicTrigger(collectRateMs, TimeUnit.MILLISECONDS);
trigger.setInitialDelay(0);
taskRegistrar.addTriggerTask(this::collect, trigger);
}
/**
* 立即采集一次
*/
public void collectOnce() {
collect();
}
private void collect() {
try {
UpsStatusStore.Snapshot snapshot = upsStatusClient.fetchSnapshot();
long now = System.currentTimeMillis();
snapshot.setUpdatedAt(now);
UpsStatusStore.Point point = new UpsStatusStore.Point();
point.setAt(now);
point.setUpsTime(snapshot.getUpsTime());
point.setWorkMode(snapshot.getWorkMode());
point.setInputVoltage(snapshot.getInputVoltage());
point.setInputFrequency(snapshot.getInputFrequency());
point.setOutputVoltage(snapshot.getOutputVoltage());
point.setOutputFrequency(snapshot.getOutputFrequency());
point.setOutputLoadPercent(snapshot.getOutputLoadPercent());
point.setBatteryVoltage(snapshot.getBatteryVoltage());
point.setBatteryCapacity(snapshot.getBatteryCapacity());
point.setBatteryRemainTime(snapshot.getBatteryRemainTime());
point.setTemperature(snapshot.getTemperature());
point.setBypassActive(snapshot.isBypassActive());
point.setShutdownActive(snapshot.isShutdownActive());
point.setOutputOn(snapshot.isOutputOn());
synchronized (upsStatusStore) {
upsStatusStore.setCurrent(snapshot);
upsStatusStore.getHistory().addLast(point);
long threshold = now - MAX_HISTORY_MS;
while (!upsStatusStore.getHistory().isEmpty() && upsStatusStore.getHistory().peekFirst().getAt() < threshold) {
upsStatusStore.getHistory().pollFirst();
}
}
} catch (Exception e) {
log.error("collect ups status error", e);
}
}
}

View File

@@ -0,0 +1,29 @@
package com.imyeyu.api.modules.system.task.status;
import com.imyeyu.api.modules.common.entity.Setting;
import java.util.Deque;
/**
* 带有限长队列工具的采集器基类
*
* @author Codex
* @since 2026-04-06
*/
public abstract class AbstractDequeStatusCollector implements StatusCollector {
/**
* 有限长度追加队列数据
*
* @param context 采集上下文
* @param deque 队列
* @param value 值
* @param <T> 类型
*/
protected <T> void putDeque(StatusCollectContext context, Deque<T> deque, T value) {
deque.addLast(value);
if (context.getSettingService().getAsInt(Setting.Module.System.STATUS_LIMIT) < deque.size()) {
deque.pollFirst();
}
}
}

View File

@@ -0,0 +1,37 @@
package com.imyeyu.api.modules.system.task.status;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.system.bean.ServerStatus;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import oshi.hardware.CentralProcessor;
import oshi.hardware.ComputerSystem;
import oshi.hardware.GlobalMemory;
import oshi.hardware.HardwareAbstractionLayer;
import oshi.software.os.OperatingSystem;
import java.lang.management.MemoryMXBean;
/**
* 状态采集上下文
*
* @author Codex
* @since 2026-04-06
*/
@Getter
@AllArgsConstructor
public class StatusCollectContext {
private final ServerStatus status;
private final GlobalMemory globalMemory;
private final MemoryMXBean jvmMemory;
private final SettingService settingService;
private final ComputerSystem computerSystem;
private final OperatingSystem operatingSystem;
private final CentralProcessor processor;
private final HardwareAbstractionLayer hardware;
@Setter
private long collectAt;
}

View File

@@ -0,0 +1,25 @@
package com.imyeyu.api.modules.system.task.status;
/**
* 状态采集器
*
* @author Codex
* @since 2026-04-06
*/
public interface StatusCollector {
/**
* 初始化采集器
*
* @param context 采集上下文
*/
default void initialize(StatusCollectContext context) {
}
/**
* 采集状态
*
* @param context 采集上下文
*/
void collect(StatusCollectContext context);
}

View File

@@ -0,0 +1,50 @@
package com.imyeyu.api.modules.system.task.status.collector;
import com.imyeyu.api.modules.system.task.status.AbstractDequeStatusCollector;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import oshi.hardware.CentralProcessor;
/**
* CPU 状态采集器
*
* @author 夜雨
* @since 2026-04-07 11:18
*/
@Component
@Order(20)
public class CpuStatusCollector extends AbstractDequeStatusCollector {
private long[] lastCpuTicks;
@Override
public void initialize(StatusCollectContext context) {
context.getStatus().getCpu().setName(context.getProcessor().getProcessorIdentifier().getName().trim());
context.getStatus().getCpu().setCoreCount(context.getProcessor().getPhysicalProcessorCount());
context.getStatus().getCpu().setLogicalCount(context.getProcessor().getLogicalProcessorCount());
lastCpuTicks = context.getProcessor().getSystemCpuLoadTicks();
}
@Override
public void collect(StatusCollectContext context) {
long[] ticks = context.getProcessor().getSystemCpuLoadTicks();
if (lastCpuTicks != null) {
long user = ticks[CentralProcessor.TickType.USER.getIndex()] - lastCpuTicks[CentralProcessor.TickType.USER.getIndex()];
long nice = ticks[CentralProcessor.TickType.NICE.getIndex()] - lastCpuTicks[CentralProcessor.TickType.NICE.getIndex()];
long sys = ticks[CentralProcessor.TickType.SYSTEM.getIndex()] - lastCpuTicks[CentralProcessor.TickType.SYSTEM.getIndex()];
long idle = ticks[CentralProcessor.TickType.IDLE.getIndex()] - lastCpuTicks[CentralProcessor.TickType.IDLE.getIndex()];
long ioWait = ticks[CentralProcessor.TickType.IOWAIT.getIndex()] - lastCpuTicks[CentralProcessor.TickType.IOWAIT.getIndex()];
long irq = ticks[CentralProcessor.TickType.IRQ.getIndex()] - lastCpuTicks[CentralProcessor.TickType.IRQ.getIndex()];
long softIrq = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()] - lastCpuTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()];
long steal = ticks[CentralProcessor.TickType.STEAL.getIndex()] - lastCpuTicks[CentralProcessor.TickType.STEAL.getIndex()];
double total = user + nice + sys + idle + ioWait + irq + softIrq + steal;
if (0 < total) {
putDeque(context, context.getStatus().getCpu().getSystem(), sys / total);
putDeque(context, context.getStatus().getCpu().getUsed(), 1 - idle / total);
}
}
lastCpuTicks = ticks;
context.getStatus().getCpu().setTemperature(context.getHardware().getSensors().getCpuTemperature());
}
}

View File

@@ -0,0 +1,46 @@
package com.imyeyu.api.modules.system.task.status.collector;
import com.imyeyu.api.modules.system.bean.ServerStatus;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import com.imyeyu.api.modules.system.task.status.StatusCollector;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
/**
* 硬件状态采集器
*
* @author 夜雨
* @since 2026-04-07 11:18
*/
@Component
@Order(35)
public class HardwareStatusCollector implements StatusCollector {
@Override
public void initialize(StatusCollectContext context) {
ServerStatus.Baseboard baseboard = context.getStatus().getHardware().getBaseboard();
baseboard.setManufacturer(context.getComputerSystem().getBaseboard().getManufacturer());
baseboard.setModel(context.getComputerSystem().getBaseboard().getModel());
baseboard.setVersion(context.getComputerSystem().getBaseboard().getVersion());
baseboard.setSerialNumber(context.getComputerSystem().getBaseboard().getSerialNumber());
ServerStatus.Firmware firmware = context.getStatus().getHardware().getFirmware();
firmware.setManufacturer(context.getComputerSystem().getFirmware().getManufacturer());
firmware.setName(context.getComputerSystem().getFirmware().getName());
firmware.setDescription(context.getComputerSystem().getFirmware().getDescription());
firmware.setVersion(context.getComputerSystem().getFirmware().getVersion());
firmware.setReleaseDate(context.getComputerSystem().getFirmware().getReleaseDate());
}
@Override
public void collect(StatusCollectContext context) {
int[] fanSpeeds = context.getHardware().getSensors().getFanSpeeds();
ArrayList<Integer> values = new ArrayList<>(fanSpeeds.length);
for (int fanSpeed : fanSpeeds) {
values.add(fanSpeed);
}
context.getStatus().getHardware().setFanSpeeds(values);
}
}

View File

@@ -0,0 +1,100 @@
package com.imyeyu.api.modules.system.task.status.collector;
import com.imyeyu.api.modules.system.task.status.AbstractDequeStatusCollector;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import com.imyeyu.utils.Time;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.ManagementFactory;
import java.util.List;
/**
* JVM 状态采集器
*
* @author 夜雨
* @since 2026-04-07 11:17
*/
@Component
@Order(40)
public class JvmStatusCollector extends AbstractDequeStatusCollector {
private long lastHeapUsed = -1;
@Override
public void initialize(StatusCollectContext context) {
context.getStatus().getJvm().setBootAt(Time.now());
context.getStatus().getJvm().setName(System.getProperty("java.vm.name"));
context.getStatus().getJvm().setVersion(System.getProperty("java.version"));
context.getStatus().getJvm().setGcName(resolveGcName());
}
@Override
public void collect(StatusCollectContext context) {
long heapUsed = context.getJvmMemory().getHeapMemoryUsage().getUsed();
context.getStatus().getJvm().getMemory().setInit(context.getJvmMemory().getHeapMemoryUsage().getInit());
context.getStatus().getJvm().getMemory().setMax(context.getJvmMemory().getHeapMemoryUsage().getMax());
putDeque(context, context.getStatus().getJvm().getMemory().getUsed(), heapUsed);
putDeque(context, context.getStatus().getJvm().getMemory().getCommitted(), context.getJvmMemory().getHeapMemoryUsage().getCommitted());
long recoverySize = 0;
if (0 <= lastHeapUsed) {
recoverySize = Math.abs(heapUsed - lastHeapUsed);
}
lastHeapUsed = heapUsed;
long gcSyncCycles = 0;
long gcSyncCyclesTime = 0;
long gcPauses = 0;
long gcPausesTime = 0;
for (GarbageCollectorMXBean gc : ManagementFactory.getGarbageCollectorMXBeans()) {
switch (gc.getName()) {
case "ZGC Cycles" -> {
gcSyncCycles += gc.getCollectionCount();
gcSyncCyclesTime += gc.getCollectionTime();
}
case "ZGC Pauses" -> {
gcPauses += gc.getCollectionCount();
gcPausesTime += gc.getCollectionTime();
}
default -> {
}
}
}
if (context.getStatus().getJvm().getGc().getPauses() < gcPauses) {
context.getStatus().getJvm().getGc().setLastPauseAt(context.getCollectAt());
context.getStatus().getJvm().getGc().setLastRecoverySize(recoverySize);
}
putDeque(context, context.getStatus().getJvm().getGc().getSyncCyclesTime(), gcSyncCyclesTime - context.getStatus().getJvm().getGc().getSyncCyclesTimeTotal());
putDeque(context, context.getStatus().getJvm().getGc().getPausesTime(), gcPausesTime - context.getStatus().getJvm().getGc().getPausesTimeTotal());
context.getStatus().getJvm().getGc().setSyncCycles(gcSyncCycles);
context.getStatus().getJvm().getGc().setSyncCyclesTimeTotal(gcSyncCyclesTime);
context.getStatus().getJvm().getGc().setPauses(gcPauses);
context.getStatus().getJvm().getGc().setPausesTimeTotal(gcPausesTime);
}
/**
* 解析当前 JVM 的主要 GC 名称
*
* @return GC 名称
*/
private String resolveGcName() {
List<GarbageCollectorMXBean> collectors = ManagementFactory.getGarbageCollectorMXBeans();
if (collectors.isEmpty()) {
return null;
}
if (1 == collectors.size()) {
return collectors.get(0).getName();
}
StringBuilder gcName = new StringBuilder();
for (GarbageCollectorMXBean collector : collectors) {
if (!gcName.isEmpty()) {
gcName.append(", ");
}
gcName.append(collector.getName());
}
return gcName.toString();
}
}

View File

@@ -0,0 +1,29 @@
package com.imyeyu.api.modules.system.task.status.collector;
import com.imyeyu.api.modules.system.task.status.AbstractDequeStatusCollector;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 系统内存状态采集器
*
* @author 夜雨
* @since 2026-04-07 11:17
*/
@Component
@Order(30)
public class MemoryStatusCollector extends AbstractDequeStatusCollector {
@Override
public void initialize(StatusCollectContext context) {
context.getStatus().getMemory().setSize(context.getGlobalMemory().getTotal());
context.getStatus().getMemory().setSwapSize(context.getGlobalMemory().getVirtualMemory().getSwapTotal());
}
@Override
public void collect(StatusCollectContext context) {
putDeque(context, context.getStatus().getMemory().getUsed(), context.getGlobalMemory().getTotal() - context.getGlobalMemory().getAvailable());
putDeque(context, context.getStatus().getMemory().getSwapUsed(), context.getGlobalMemory().getVirtualMemory().getSwapUsed());
}
}

View File

@@ -0,0 +1,79 @@
package com.imyeyu.api.modules.system.task.status.collector;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.system.task.status.AbstractDequeStatusCollector;
import com.imyeyu.api.modules.system.task.status.StatusCollectContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import oshi.hardware.NetworkIF;
import java.util.List;
/**
* 网络状态采集器
*
* @author 夜雨
* @since 2026-04-07 11:15
*/
@Slf4j
@Component
@Order(50)
public class NetworkStatusCollector extends AbstractDequeStatusCollector {
@Override
public void initialize(StatusCollectContext context) {
List<NetworkIF> networkIFs = context.getHardware().getNetworkIFs();
String targetMac = context.getSettingService().getAsString(Setting.Module.System.STATUS_NETWORK_MAC);
for (NetworkIF networkIF : networkIFs) {
if (networkIF.getMacaddr().equals(targetMac)) {
networkIF.updateAttributes();
updateNetworkStatus(context, networkIF);
return;
}
}
log.error("not found setting networkIF MAC: {}", targetMac);
for (NetworkIF networkIF : networkIFs) {
log.info("Network Interface: {} -> {}", networkIF.getMacaddr(), networkIF.getDisplayName());
}
}
@Override
public void collect(StatusCollectContext context) {
String targetMac = context.getSettingService().getAsString(Setting.Module.System.STATUS_NETWORK_MAC);
int sampleRateMs = context.getSettingService().getAsInt(Setting.Module.System.STATUS_RATE);
for (NetworkIF networkIF : context.getHardware().getNetworkIFs()) {
if (networkIF.getMacaddr().equals(targetMac)) {
networkIF.updateAttributes();
long recv = networkIF.getBytesRecv() - context.getStatus().getNetwork().getRecvTotal();
long sent = networkIF.getBytesSent() - context.getStatus().getNetwork().getSentTotal();
context.getStatus().getNetwork().setRecvNow(recv * 1000 / sampleRateMs);
context.getStatus().getNetwork().setSentNow(sent * 1000 / sampleRateMs);
updateNetworkStatus(context, networkIF);
putDeque(context, context.getStatus().getNetwork().getRecv(), recv);
putDeque(context, context.getStatus().getNetwork().getSent(), sent);
return;
}
}
}
/**
* 更新网络状态缓存
*
* @param context 采集上下文
* @param networkIF 网卡
*/
private void updateNetworkStatus(StatusCollectContext context, NetworkIF networkIF) {
context.getStatus().getNetwork().setName(networkIF.getDisplayName());
context.getStatus().getNetwork().setMac(networkIF.getMacaddr());
context.getStatus().getNetwork().setRecvTotal(networkIF.getBytesRecv());
context.getStatus().getNetwork().setSentTotal(networkIF.getBytesSent());
context.getStatus().getNetwork().setRecvPacketsTotal(networkIF.getPacketsRecv());
context.getStatus().getNetwork().setSentPacketsTotal(networkIF.getPacketsSent());
context.getStatus().getNetwork().setInErrors(networkIF.getInErrors());
context.getStatus().getNetwork().setOutErrors(networkIF.getOutErrors());
context.getStatus().getNetwork().setInDrops(networkIF.getInDrops());
context.getStatus().getNetwork().setCollisions(networkIF.getCollisions());
}
}

Some files were not shown because too many files have changed in this diff Show More