机器管理

This commit is contained in:
唐潇凯 2025-05-30 14:08:16 +08:00
parent 0f8f6f837d
commit 6e317f9b8c
62 changed files with 4668 additions and 5 deletions

3
.idea/compiler.xml generated
View File

@ -7,6 +7,7 @@
<sourceOutputDir name="target/generated-sources/annotations" /> <sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" /> <sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" /> <outputRelativeToContentRoot value="true" />
<module name="machine-management-module" />
</profile> </profile>
<profile name="Annotation profile for ops-pro" enabled="true"> <profile name="Annotation profile for ops-pro" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" /> <sourceOutputDir name="target/generated-sources/annotations" />
@ -53,6 +54,7 @@
<module name="module-ci-dispatch-api" target="17" /> <module name="module-ci-dispatch-api" target="17" />
<module name="module-ci-environment" target="17" /> <module name="module-ci-environment" target="17" />
<module name="module-ci-event" target="17" /> <module name="module-ci-event" target="17" />
<module name="module-ci-execute" target="1.5" />
<module name="module-ci-log" target="17" /> <module name="module-ci-log" target="17" />
<module name="module-ci-market" target="17" /> <module name="module-ci-market" target="17" />
<module name="module-ci-project" target="17" /> <module name="module-ci-project" target="17" />
@ -69,6 +71,7 @@
<module name="app-plugins" options="-parameters" /> <module name="app-plugins" options="-parameters" />
<module name="commons" options="-parameters" /> <module name="commons" options="-parameters" />
<module name="framework" options="-parameters" /> <module name="framework" options="-parameters" />
<module name="machine-management-module" options="-parameters" />
<module name="module-ci-commons" options="-parameters" /> <module name="module-ci-commons" options="-parameters" />
<module name="module-ci-engine" options="-parameters" /> <module name="module-ci-engine" options="-parameters" />
<module name="module-ci-plugin" options="-parameters" /> <module name="module-ci-plugin" options="-parameters" />

1
.idea/encodings.xml generated
View File

@ -39,6 +39,7 @@
<file url="file://$PROJECT_DIR$/framework/spring-boot-starter-websocket/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/framework/spring-boot-starter-websocket/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/framework/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/framework/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/framework/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/framework/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/modules/ee/machine-management-module/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/modules/module-ci-common-pipeline/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/modules/module-ci-common-pipeline/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/modules/module-ci-common-pipeline/src/main/resources" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/modules/module-ci-common-pipeline/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/modules/module-ci-common/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/modules/module-ci-common/src/main/java" charset="UTF-8" />

View File

@ -1,16 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="RemoteRepositoriesConfiguration"> <component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="huaweicloud" />
<option name="name" value="huawei" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository> <remote-repository>
<option name="id" value="central" /> <option name="id" value="central" />
<option name="name" value="Central Repository" /> <option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" /> <option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository> </remote-repository>
<remote-repository>
<option name="id" value="huaweicloud" />
<option name="name" value="huawei" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository> <remote-repository>
<option name="id" value="aliyunmaven" /> <option name="id" value="aliyunmaven" />
<option name="name" value="aliyun" /> <option name="name" value="aliyun" />

1
.idea/misc.xml generated
View File

@ -52,6 +52,7 @@
<option value="$PROJECT_DIR$/modules/module-ci-store-api/pom.xml" /> <option value="$PROJECT_DIR$/modules/module-ci-store-api/pom.xml" />
<option value="$PROJECT_DIR$/modules/module-ci-process-biz/pom.xml" /> <option value="$PROJECT_DIR$/modules/module-ci-process-biz/pom.xml" />
<option value="$PROJECT_DIR$/modules/module-ci-process-api/pom.xml" /> <option value="$PROJECT_DIR$/modules/module-ci-process-api/pom.xml" />
<option value="$PROJECT_DIR$/modules/ee/machine-management-module/pom.xml" />
</list> </list>
</option> </option>
<option name="ignoredFiles"> <option name="ignoredFiles">

View File

@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf

View File

@ -0,0 +1,33 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

View File

@ -0,0 +1,259 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.2
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

View File

@ -0,0 +1,149 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.2
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
if ($env:MAVEN_USER_HOME) {
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
}
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.13-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<artifactId>machine-management-module</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>machine</name>
<description>machine</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.15.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- 机器连接-->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- SpringDoc OpenAPI 依赖 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,39 @@
package com.casic.machine.configuration;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.casic.machine.exception.ServiceException;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Getter
public class AliYunConfig {
private static final Logger logger = LoggerFactory.getLogger(AliYunConfig.class);
@Value("${aliyun.oss.endpoint}")
private String endpoint;
@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;
@Value("${aliyun.oss.accessKeySecret}")
private String accessKeySecret;
@Value("${aliyun.oss.bucketName}")
private String bucketName;
@Bean
public OSS ossClient() {
logger.info("OSS域名: {}", endpoint);
logger.info("AccessKeyId: {}", accessKeyId);
// 添加参数校验
if (endpoint == null || accessKeyId == null || accessKeySecret == null) {
throw new ServiceException(ServiceException.OSS_PARAM_NULL,"OSS参数为空");
}
return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
}
}

View File

@ -0,0 +1,5 @@
package com.casic.machine.contants;
public interface CommonConstants {
String DEFAULT_PACKAGE_NAME = "com.casic";
}

View File

@ -0,0 +1,69 @@
package com.casic.machine.controller;
import com.casic.machine.dto.MachineEnvDTO;
import com.casic.machine.pojo.ResponseData;
import com.casic.machine.service.MachineEnvService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 环境变量控制器
*/
@RestController
@RequestMapping("/api/machineEnv")
@Tag(name = "环境变量管理")
@RequiredArgsConstructor
public class MachineEnvController {
@Resource
private MachineEnvService machineEnvService;
@PostMapping("/add")
@Operation(summary = "新增环境变量")
public ResponseData add(@RequestBody MachineEnvDTO machineEnvDTO) {
return machineEnvService.add(machineEnvDTO) ? ResponseData.success() : ResponseData.error("新增机器的环境变量失败");
}
@PutMapping("/update")
@Operation(summary = "修改环境变量")
public ResponseData update(@RequestBody MachineEnvDTO machineEnvDTO) {
return machineEnvService.update(machineEnvDTO) ? ResponseData.success() : ResponseData.error("修改环境变量失败");
}
@DeleteMapping("/delete")
@Operation(summary = "删除机器的环境变量")
public ResponseData deleteByMachineId(
@RequestParam Long machineId) {
machineEnvService.deleteByMachineId(machineId);
return ResponseData.success();
}
@DeleteMapping("/deleteList")
@Operation(summary = "批量删除机器环境变量")
public ResponseData deleteList(@RequestParam List<Long> ids){
machineEnvService.deleteList(ids);
return ResponseData.success();
}
@GetMapping("/listByMachineId")
@Operation(summary = "获取机器的环境变量")
public ResponseData getByMachineId(
@RequestParam Long machineId) {
return ResponseData.success(machineEnvService.getByMachineId(machineId));
}
@PostMapping("/list")
@Operation(summary ="获取环境变量列表")
public ResponseData list(@RequestBody MachineEnvDTO machineEnvDTO) {
return ResponseData.success(machineEnvService.listEnv(machineEnvDTO));
}
}

View File

@ -0,0 +1,126 @@
package com.casic.machine.controller;
import com.casic.machine.dto.MachineInfoDto;
import com.casic.machine.entity.MachineInfo;
import com.casic.machine.pojo.ResponseData;
import com.casic.machine.pojo.SuccessResponseData;
import com.casic.machine.service.MachineInfoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@Tag(name = "机器信息管理")
@RequestMapping("/api/machineInfo")
public class MachineInfoController {
@Resource
private MachineInfoService machineInfoService;
@PostMapping("/add")
@Operation(summary = "新增机器信息")
public ResponseData add(@RequestBody MachineInfoDto machineInfoDto) {
return machineInfoService.addMachineInfo(machineInfoDto) ? ResponseData.success() : ResponseData.error("新增失败");
}
@GetMapping("/list")
@Operation(summary = "获取机器信息列表")
public ResponseData list(@RequestBody MachineInfoDto machineInfoDto) {
return SuccessResponseData.success(machineInfoService.listMachineInfo(machineInfoDto));
}
@PutMapping("/update")
@Operation(summary = "编辑机器信息")
public ResponseData update(@RequestBody MachineInfoDto machineInfoDto) {
return machineInfoService.updateMachineInfo(machineInfoDto) ? ResponseData.success() : ResponseData.error("更新失败");
}
@PutMapping("/updateStatus")
@Operation(summary = "机器启用/停用")
public ResponseData updateStatus(@RequestParam("machineInfoId") Long machineInfoId, @RequestParam("status") String status) {
return machineInfoService.updateStatus(machineInfoId, status) ? ResponseData.success() : ResponseData.error("更新状态失败");
}
@PutMapping("/bindingSecretKey")
@Operation(summary = "绑定密钥")
public ResponseData bindingSecretKey(@RequestParam("machineInfoId") Long machineInfoId, @RequestParam("secretKeyId") Long secretKeyId) {
return machineInfoService.bindingSecretKey(machineInfoId, secretKeyId) ? ResponseData.success() : ResponseData.error("绑定失败");
}
@DeleteMapping("/delete")
@Operation(summary = "机器信息删除")
public ResponseData delete(@RequestParam("machineInfoId") Long machineInfoId) {
machineInfoService.deleteMachineInfo(machineInfoId);
return ResponseData.success();
}
@DeleteMapping("/deleteList")
@Operation(summary = "批量删除机器信息")
public ResponseData deleteList(@RequestParam("machineInfoId") List<Long> machineInfoIds) {
machineInfoService.deleteList(machineInfoIds);
return ResponseData.success();
}
@PostMapping("/test")
@Operation(summary = "测试机器连接")
public ResponseData testConnection(@RequestBody MachineInfo machineInfo) {
boolean result = machineInfoService.testConnection(machineInfo);
return ResponseData.success();
}
@GetMapping("/status/{machineName}")
@Operation(summary = "获取机器连接状态")
public ResponseData getConnectionStatus(@PathVariable String machineName) {
return ResponseData.success(machineInfoService.getConnectionStatus(machineName));
}
@GetMapping("/status/all")
@Operation(summary = "获取所有机器连接状态")
public ResponseData getAllConnectionStatus() {
return ResponseData.success(machineInfoService.getAllConnectionStatus());
}
@PostMapping("/connect")
@Operation(summary = "建立机器连接")
public ResponseData connect(@RequestBody MachineInfo machineInfo) {
return ResponseData.success(machineInfoService.connect(machineInfo));
}
@PostMapping("/disconnect/{sessionId}")
@Operation(summary = "断开机器连接")
public ResponseData disconnect(@PathVariable String sessionId) {
return ResponseData.success(machineInfoService.disconnect(sessionId));
}
@PostMapping("/execute/{sessionId}")
@Operation(summary = "执行远程命令")
public ResponseData executeCommand(
@PathVariable String sessionId,
@RequestBody String command) {
return ResponseData.success(machineInfoService.executeCommand(sessionId, command));
}
@PostMapping("/upload/{sessionId}")
@Operation(summary = "上传文件到远程机器")
public ResponseData uploadFile(
@PathVariable String sessionId,
@RequestParam String localFilePath,
@RequestParam String remoteFilePath) {
return ResponseData.success(machineInfoService.uploadFile(sessionId, localFilePath, remoteFilePath));
}
@PostMapping("/download/{sessionId}")
@Operation(summary = "从远程机器下载文件")
public ResponseData downloadFile(
@PathVariable String sessionId,
@RequestParam String remoteFilePath,
@RequestParam String localFilePath) {
return ResponseData.success(machineInfoService.downloadFile(sessionId, remoteFilePath, localFilePath));
}
}

View File

@ -0,0 +1,71 @@
package com.casic.machine.controller;
import com.casic.machine.dto.MachineProxyDTO;
import com.casic.machine.pojo.ResponseData;
import com.casic.machine.pojo.SuccessResponseData;
import com.casic.machine.service.MachineProxyService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 机器代理控制器
*/
@RestController
@RequestMapping("/api/machineProxy")
@Tag(name = "机器代理管理")
@RequiredArgsConstructor
public class MachineProxyController {
@Resource
private MachineProxyService machineProxyService;
@PostMapping("/register")
@Operation(summary ="注册新的机器代理")
public ResponseData register(@RequestBody MachineProxyDTO machineProxyDTO) {
return machineProxyService.register(machineProxyDTO) ? ResponseData.success() :ResponseData.error("注册新的机器代理失败");
}
@GetMapping("/list")
@Operation(summary ="获取代理")
public ResponseData list(MachineProxyDTO machineProxyDTO) {
return ResponseData.success(machineProxyService.list(machineProxyDTO));
}
@PutMapping("/updateStatus")
@Operation(summary ="更新代理状态")
public ResponseData updateStatus(@RequestBody MachineProxyDTO machineProxyDTO) {
return machineProxyService.updateStatus(machineProxyDTO) ? ResponseData.success() : ResponseData.error("更新代理状态失败");
}
@PostMapping("/receiptHeartbeat")
@Operation(summary ="接收代理心跳")
public ResponseData heartbeat(@RequestBody MachineProxyDTO machineProxyDTO) {
return machineProxyService.heartbeat(machineProxyDTO) ? ResponseData.success() :ResponseData.error("接收代理心跳失败");
}
@GetMapping("/statistics/status")
@Operation(summary ="获取所有代理的状态统计")
public ResponseData getStatusStatistics() {
return ResponseData.success(machineProxyService.getStatusStatistics());
}
@PutMapping("/updateConfig")
@Operation(summary ="更新代理配置")
public ResponseData updateConfig(@RequestBody MachineProxyDTO machineProxyDTO) {
return machineProxyService.updateConfig(machineProxyDTO) ? ResponseData.success() : ResponseData.error("更新代理配置失败");
}
@DeleteMapping("/batch")
@Operation(summary ="批量删除代理")
public ResponseData deleteBatch(@RequestBody List<Long> ids) {
machineProxyService.delete(ids);
return new SuccessResponseData();
}
}

View File

@ -0,0 +1,75 @@
package com.casic.machine.controller;
import com.casic.machine.dto.SecretKeyDto;
import com.casic.machine.pojo.ResponseData;
import com.casic.machine.service.SecretKeyService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@RestController
@RequestMapping("/api/secretKey")
@Tag(name = "密钥管理")
public class SecretKeyController {
@Resource
private SecretKeyService secretKeyService;
@PostMapping(value = "/add", consumes = "multipart/form-data")
@Operation(summary ="新增密钥")
public ResponseData add(@RequestPart("secretKeyDto") SecretKeyDto secretKeyDto, @RequestPart("file") MultipartFile file) {
return secretKeyService.addSecretKey(secretKeyDto, file) ? ResponseData.success() : ResponseData.error("新增密钥失败");
}
@PutMapping("/bindingMachine")
@Operation(summary ="绑定机器")
public ResponseData bindingMachine(@RequestParam("secretKeyId") Long secretKeyId, @RequestParam("machineInfoIds") List<Long> machineInfoIds) {
secretKeyService.bindingMachine(secretKeyId, machineInfoIds);
return ResponseData.success();
}
@PutMapping("/update")
@Operation(summary ="编辑密钥信息")
public ResponseData update(@RequestPart("secretKeyDto") SecretKeyDto secretKeyDto, @RequestPart(value = "file", required = false) MultipartFile file) {
return secretKeyService.updateSecretKey(secretKeyDto, file) ? ResponseData.success() : ResponseData.error("编辑密钥信息失败");
}
@DeleteMapping("/delete")
@Operation(summary ="删除密钥")
public ResponseData delete(@RequestParam("secretKeyId") Long secretKeyId) {
return secretKeyService.deleteSecretKey(secretKeyId) ? ResponseData.success() : ResponseData.error("删除密钥");
}
@DeleteMapping("/deleteList")
@Operation(summary ="批量删除密钥")
public ResponseData deleteList(@RequestParam("secretKeyId") List<Long> secretKeyIds) {
return secretKeyService.deleteList(secretKeyIds) ? ResponseData.success() : ResponseData.error("批量删除密钥");
}
@GetMapping("/list")
@Operation(summary ="获取密钥信息列表")
public ResponseData list(@RequestBody SecretKeyDto secretKeyDto) {
return ResponseData.success(secretKeyService.listSecretKey(secretKeyDto));
}
@GetMapping("/downloadSecretKeyFile")
@Operation(summary ="下载密钥文件")
public ResponseEntity<InputStreamResource> downloadSecretKeyFile(@RequestParam("secretKeyId") Long secretKeyId) {
return secretKeyService.downloadSecretKeyFile(secretKeyId);
}
}

View File

@ -0,0 +1,45 @@
package com.casic.machine.dto;
import lombok.*;
import java.io.Serializable;
import java.util.Date;
/**
* 环境变量数据传输对象
*/
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MachineEnvDTO extends PageDto implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String envKey;
private String envValue;
private Boolean sensitive;
private String description;
private Long machineInfoId;
private Date createDate;
private Date updateDate;
private String sortField;
private String sortDirection;
/**
* 获取脱敏后的环境变量值
*/
public String getMaskedValue() {
if (!sensitive || envValue == null) {
return envValue;
}
int length = envValue.length();
if (length <= 4) {
return "****";
} else {
return envValue.substring(0, 2) + "****" + envValue.substring(length - 2);
}
}
}

View File

@ -0,0 +1,44 @@
package com.casic.machine.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.Date;
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MachineInfoDto extends PageDto {
private Long id;
private Date createDate;
private Date updateDate;
private String name;
private String tag;
private String hostIp;
private String description;
private String username;
private String status;
private String sshPort;
private String password;
private Long secretKeyId;
private Long machineProxyId;
private String authenticationType;
private String machineInfoTypeCode;
}

View File

@ -0,0 +1,43 @@
package com.casic.machine.dto;
import com.casic.machine.enums.MachineProxyStatus;
import lombok.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Objects;
/**
* 机器代理数据传输对象
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MachineProxyDTO extends PageDto implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String proxyType;
private String version;
private String status;
private Date lastHeartbeatTime;
private String config;
private String description;
private String hostIp;
private String sshPort;
/**
* 计算代理是否在线
*/
public boolean isOnline() {
if (status == null || lastHeartbeatTime == null) {
return false;
}
// 假设5分钟内有心跳为在线
long fiveMinutes = 5 * 60 * 1000;
return Objects.equals(MachineProxyStatus.ONLINE.getMessage(), status) &&
System.currentTimeMillis() - lastHeartbeatTime.getTime() < fiveMinutes;
}
}

View File

@ -0,0 +1,10 @@
package com.casic.machine.dto;
import lombok.Data;
@Data
public class PageDto{
private int pageIndex = 1;
private int pageSize = 10;
}

View File

@ -0,0 +1,36 @@
package com.casic.machine.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;
import java.util.Date;
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SecretKeyDto extends PageDto {
private Long id;
private String name;
private String description;
//存储路径
private String path;
//密钥文件
private MultipartFile file;
private String fileName;
//密钥密码
private String password;
private Date createDate;
private Date updateDate;
}

View File

@ -0,0 +1,28 @@
package com.casic.machine.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class BaseEntity {
@TableId
private Long id;
@TableField(value = "create_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createDate;
@TableField(value = "update_date")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateDate;
}

View File

@ -0,0 +1,54 @@
package com.casic.machine.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.*;
import java.io.Serializable;
import java.util.Date;
/**
* 环境变量实体类
*/
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("machine_env")
public class MachineEnv extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 机器ID唯一关联
*/
private Long machineId;
/**
* 环境变量键
*/
private String envKey;
/**
* 环境变量值
*/
private String envValue;
/**
* 是否敏感
*/
private Boolean sensitive;
/**
* 描述信息
*/
private String description;
/**
* 逻辑删除标志
*/
@TableLogic
private Boolean deleted;
}

View File

@ -0,0 +1,66 @@
package com.casic.machine.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.casic.machine.enums.AuthenticationType;
import com.casic.machine.enums.MachineInfoStatus;
import com.casic.machine.enums.MachineInfoType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "machine_info")
public class MachineInfo extends BaseEntity{
@TableField(value = "name")
private String name;
@TableField(value = "tag")
private String tag;
@TableField(value = "host_ip")
private String hostIp;
@TableField(value = "description")
private String description;
@TableField(exist = false)
private MachineInfoType machineInfoType;
@TableField(value = "machine_info_type_code")
private int machineInfoTypeCode;
@TableField(exist = false)
private MachineInfoStatus status;
@TableField(value = "status_code")
private int statusCode;
//用户名
@TableField(value = "username")
private String username;
//SSH端口号
@TableField(value = "SSH_port")
private Integer sshPort;
@TableField(value = "password")
private String password;
@TableField(value = "secret_key_id")
private Long secretKeyId;
@TableField(value = "machine_proxy_id")
private Long machineProxyId;
@TableField(value = "authentication_type_code")
private int authenticationTypeCode;
private AuthenticationType authenticationType;
}

View File

@ -0,0 +1,69 @@
package com.casic.machine.entity;
import com.baomidou.mybatisplus.annotation.*;
import com.casic.machine.enums.MachineInfoStatus;
import com.casic.machine.enums.MachineProxyType;
import lombok.*;
import java.io.Serializable;
import java.util.Date;
/**
* 机器代理实体类
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("machine_proxy")
public class MachineProxy extends BaseEntity implements Serializable {
private static final long serialVersionUID = 1L;
private String hostIp;
private String sshPort;
/**
* 代理类型
*/
@TableField(exist = false)
private MachineProxyType proxyType;
private int proxyTypeCode;
/**
* 代理版本
*/
private String version;
/**
* 代理状态 (online, offline, installing, updating, error)
*/
@TableField(exist = false)
private MachineInfoStatus status;
private int statusCode;
/**
* 最后心跳时间
*/
private Date lastHeartbeatTime;
/**
* 代理配置 (JSON格式)
*/
private String config;
/**
* 描述信息
*/
private String description;
/**
* 逻辑删除标志
*/
@TableLogic
private Boolean deleted;
}

View File

@ -0,0 +1,32 @@
package com.casic.machine.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "machine_secret_key")
public class SecretKey extends BaseEntity{
@TableField(value = "name")
private String name;
@TableField(value = "description")
private String description;
//存储路径
@TableField(value = "path")
private String path;
@TableField
private String fileName;
//密钥密码
@TableField(value = "password")
private String password;
}

View File

@ -0,0 +1,16 @@
package com.casic.machine.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum AuthenticationType implements CodeEnum {
PASSWORD(1,"密码认证"),
SECRET_KEY(2,"密钥认证");
private final int code;
private final String message;
}

View File

@ -0,0 +1,7 @@
package com.casic.machine.enums;
public interface CodeEnum {
int getCode();
String getMessage();
}

View File

@ -0,0 +1,22 @@
package com.casic.machine.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 连接状态枚举
*/
@Getter
@AllArgsConstructor
public enum ConnectionStatus {
DISCONNECTED("断开连接"),
CONNECTING("正在连接"),
CONNECTED("已连接"),
AUTH_FAILED("认证失败"),
CONNECTION_TIMEOUT("连接超时"),
CONNECTION_ERROR("连接错误"),
CLOSED("已关闭");
private final String description;
}

View File

@ -0,0 +1,16 @@
package com.casic.machine.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum MachineInfoStatus implements CodeEnum {
ENABLE(1,"启用"),
UN_ENABLE(0,"停用");
private final int code;
private final String message;
}

View File

@ -0,0 +1,16 @@
package com.casic.machine.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum MachineInfoType implements CodeEnum {
Linux(1,"Linux"),
WINDOWS(2,"Windows");
private final int code;
private final String message;
}

View File

@ -0,0 +1,22 @@
package com.casic.machine.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum MachineProxyStatus implements CodeEnum {
/**
* 代理状态 (online, offline, installing, updating, error)
*/
ONLINE(1,"online"),
OFFLINE(2,"offline"),
INSTALLING(3,"installing"),
UPDATING(4,"updating"),
ERROR(5,"error");
private final int code;
private final String message;
}

View File

@ -0,0 +1,17 @@
package com.casic.machine.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum MachineProxyType implements CodeEnum {
HTTP(1,"http"),
SOCKS4(2,"socks4"),
SOCKS5(3,"socks5");
private final int code;
private final String message;
}

View File

@ -0,0 +1,19 @@
package com.casic.machine.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum PermissionExceptionEnum implements CodeEnum {
URL_NOT_EXIST(1, "资源路径不存在,请检查请求地址"),
NO_PERMISSION(2, "没有权限访问资源,请联系管理员"),
NO_PERMISSION_OPERATE(3, "没有权限操作该数据,请联系管理员");
private final int code;
private final String message;
}

View File

@ -0,0 +1,19 @@
package com.casic.machine.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum RequestExceptionEnum implements CodeEnum {
REQUEST_TYPE_NOT_JSON(1,"传递参数格式错误请使用json格式"),
REQUEST_JSON_ERROR(2,"json格式错误"),
REQUEST_METHOD_NOT_POST(3, "不支持该请求方法请求方法应为POST"),
REQUEST_METHOD_NOT_GET(4, "不支持该请求方法请求方法应为GET"),
PARAM_ERROR(5, "参数错误");
private final int code;
private final String message;
}

View File

@ -0,0 +1,52 @@
package com.casic.machine.exception;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 业务逻辑异常 Exception
*/
@EqualsAndHashCode(callSuper = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ServiceException extends RuntimeException {
//机器信息为空
public static final int MACHINE_INFO_NULL = 555;
//上传文件失败
public static final int UPLOADING_FILE_FAIL = 556;
//下载失败
public static final int DOWNLOAD_FILE_FAIL = 557;
//文件名为空
public static final int FILENAME_NULL = 558;
//读取文件失败
public static final int READ_FILE_FAIL = 559;
//删除文件失败
public static final int DELETE_FILE_FAIL = 560;
//MachineProxyDTO对象为空
public static final int MACHINE_PROXY_DTO_NULL = 561;
//MachineProxy代理不存在
public static final int MACHINE_PROXY_NULL = 562;
//参数错误
public static final int PARAMETER_ERROR = 563;
//机器环境变量为空
public static final int MACHINE_ENV_NULL = 564;
//机器环境变量键不合法
public static final int MACHINE_ENV_KEY_ILLEGAL = 565;
//oss参数无法读取
public static final int OSS_PARAM_NULL = 1001;
/**
* 业务错误码
* 区间
* machine-management-module模块(555-1000)
* common-module模块(1001-2000)
*/
private Integer code;
/**
* 错误提示
*/
private String message;
}

View File

@ -0,0 +1,615 @@
package com.casic.machine.handler;
import com.casic.machine.entity.MachineInfo;
import com.casic.machine.entity.SecretKey;
import com.casic.machine.enums.AuthenticationType;
import com.casic.machine.enums.ConnectionStatus;
import com.casic.machine.service.SecretKeyService;
import com.casic.machine.utils.AliOssUtil;
import com.jcraft.jsch.*;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 优化后的SSH连接会话类
*/
@Slf4j
public class ConnectionSession implements AutoCloseable {
@Resource
SecretKeyService secretKeyService;
@Resource
AliOssUtil aliOssUtil;
private final MachineInfo machineInfo;
private Session sshSession;
private ConnectionStatus status = ConnectionStatus.DISCONNECTED;
private final AtomicBoolean isExecuting = new AtomicBoolean(false);
// 连接配置常量
private static final int CONNECTION_TIMEOUT = 5000; // 连接超时时间(毫秒)
private static final int COMMAND_TIMEOUT = 30000; // 命令执行超时时间(毫秒)
private static final int RETRY_COUNT = 3; // 重试次数
private static final int RETRY_DELAY = 1000; // 重试间隔(毫秒)
public ConnectionSession(MachineInfo machineInfo) {
this.machineInfo = Objects.requireNonNull(machineInfo, "MachineInfo cannot be null");
}
/**
* 建立SSH连接支持重试机制
*/
public synchronized void connect() throws JSchException {
if (status == ConnectionStatus.CONNECTED) {
log.debug("Already connected to {}", machineInfo.getHostIp());
return;
}
status = ConnectionStatus.CONNECTING;
JSchException lastException = null;
for (int attempt = 1; attempt <= RETRY_COUNT; attempt++) {
try {
doConnect();
status = ConnectionStatus.CONNECTED;
log.info("SSH connection established successfully to {} (attempt {}/{})",
machineInfo.getHostIp(), attempt, RETRY_COUNT);
return;
} catch (JSchException e) {
lastException = e;
status = ConnectionStatus.CONNECTION_ERROR;
log.error("SSH connection attempt {}/{} failed: {}",
attempt, RETRY_COUNT, e.getMessage());
// 认证失败直接退出无需重试
if (e.getMessage().contains("Auth fail")) {
status = ConnectionStatus.AUTH_FAILED;
throw e;
}
// 重试前等待
if (attempt < RETRY_COUNT) {
try {
Thread.sleep(RETRY_DELAY);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new JSchException("Connection attempt interrupted", ie);
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 所有重试都失败
throw new JSchException("Failed to connect after " + RETRY_COUNT + " attempts", lastException);
}
/**
* 实际执行连接逻辑
*/
private void doConnect() throws JSchException, IOException {
JSch jsch = new JSch();
// 配置认证方式
configureAuthentication(jsch);
// 创建SSH会话
sshSession = jsch.getSession(
machineInfo.getUsername(),
machineInfo.getHostIp(),
machineInfo.getSshPort() != null ? machineInfo.getSshPort() : 22
);
// 配置连接参数
configureSession(sshSession);
// 建立连接
sshSession.connect(CONNECTION_TIMEOUT);
}
/**
* 配置认证方式密码或密钥
*/
private void configureAuthentication(JSch jsch) throws JSchException{
if (machineInfo.getAuthenticationType() == AuthenticationType.SECRET_KEY) {
// 密钥认证
if (machineInfo.getSecretKeyId() == null) {
throw new JSchException("Secret key ID is required for key-based authentication");
}
String privateKeyContent = getPrivateKeyContent(machineInfo.getSecretKeyId());
if (StringUtils.isEmpty(privateKeyContent)) {
throw new JSchException("Private key content is empty or null for ID: " + machineInfo.getSecretKeyId());
}
// 加载私钥支持密码短语可从配置中获取
jsch.addIdentity(
machineInfo.getName(),
privateKeyContent.getBytes(StandardCharsets.UTF_8),
null,
null // 密码短语可为null
);
} else if (machineInfo.getAuthenticationType() == AuthenticationType.PASSWORD) {
// 密码认证
if (StringUtils.isEmpty(machineInfo.getPassword())) {
throw new JSchException("Password is required for password-based authentication");
}
} else {
throw new JSchException("Unsupported authentication type: " + machineInfo.getAuthenticationType());
}
}
/**
* 配置SSH会话参数安全增强
*/
private void configureSession(Session session) {
Properties config = new Properties();
// 安全增强默认验证主机密钥
if (isTrustedEnvironment()) {
log.warn("Running in trusted environment - disabling strict host key checking for {}",
machineInfo.getHostIp());
config.put("StrictHostKeyChecking", "no");
} else {
config.put("StrictHostKeyChecking", "yes");
// 可选配置已知主机文件路径
//直接配置阿里云密钥地址
config.put("UserKnownHostsFile", secretKeyService.getById(machineInfo.getSecretKeyId()).getPath());
}
// 其他安全配置
config.put("PreferredAuthentications", "publicKey,password,keyboard-interactive");
config.put("ServerAliveInterval", "30"); // 每30秒发送一次心跳
config.put("ServerAliveCountMax", "3"); // 允许3次心跳失败
session.setConfig(config);
}
/**
* 判断是否为可信环境生产环境应返回false
*/
private boolean isTrustedEnvironment() {
// todo实际项目中应基于配置或环境变量判断
return System.getProperty("environment", "production").equalsIgnoreCase("development");
}
@Override
public synchronized void close() {
disconnect();
}
public synchronized void disconnect() {
if (sshSession != null && sshSession.isConnected()) {
try {
sshSession.disconnect();
log.info("SSH connection closed: {}", machineInfo.getHostIp());
} catch (Exception e) {
log.error("Error closing SSH session: {}", e.getMessage());
}
}
status = ConnectionStatus.DISCONNECTED;
}
/**
* 执行远程命令支持超时和中断处理
*/
public String executeCommand(String command) throws JSchException, IOException {
if (!isConnected()) {
throw new IllegalStateException("Session is not connected");
}
if (!isExecuting.compareAndSet(false, true)) {
throw new IllegalStateException("Another command is already executing");
}
Channel channel = null;
InputStream inputStream = null;
ByteArrayOutputStream outputStream = null;
try {
channel = createExecChannel(command);
inputStream = channel.getInputStream();
outputStream = new ByteArrayOutputStream();
// 连接通道并设置超时
channel.connect(COMMAND_TIMEOUT);
// 读取命令输出
return readCommandOutput(inputStream, outputStream, channel);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Command execution interrupted", e);
} finally {
// 释放资源
closeResources(channel, inputStream, outputStream);
isExecuting.set(false);
}
}
/**
* 创建并配置命令执行通道
*/
private Channel createExecChannel(String command) throws JSchException {
Channel channel = sshSession.openChannel("exec");
((ChannelExec) channel).setCommand(command);
// 配置通道
channel.setInputStream(null);
((ChannelExec) channel).setErrStream(new ByteArrayOutputStream()); // 捕获错误输出
return channel;
}
/**
* 读取命令输出
*/
private String readCommandOutput(InputStream inputStream,
ByteArrayOutputStream outputStream,
Channel channel) throws IOException, InterruptedException {
byte[] buffer = new byte[1024];
// 使用线程中断机制实现超时控制
Thread readerThread = new Thread(() -> {
int bytesRead ;
try {
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
} catch (IOException e) {
// 通道关闭或读取异常
if (channel.isConnected()) {
log.warn("Error reading command output: {}", e.getMessage());
}
}
});
readerThread.start();
// 等待命令执行完成或超时
readerThread.join(COMMAND_TIMEOUT);
// 如果线程仍在运行中断并关闭通道
if (readerThread.isAlive()) {
readerThread.interrupt();
channel.disconnect();
throw new IOException("Command execution timed out after " + COMMAND_TIMEOUT + "ms");
}
// 等待通道完全关闭
while (channel.isConnected()) {
Thread.sleep(100);
}
return outputStream.toString(StandardCharsets.UTF_8);
}
/**
* 关闭资源
*/
private void closeResources(Channel channel, InputStream inputStream, OutputStream outputStream) {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
log.warn("Error closing output stream: {}", e.getMessage());
}
}
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
log.warn("Error closing input stream: {}", e.getMessage());
}
}
if (channel != null && channel.isConnected()) {
channel.disconnect();
}
}
/**
* 上传文件到远程服务器
*/
public boolean uploadFile(String localFilePath, String remoteFilePath) throws IOException {
if (!isConnected()) {
throw new IllegalStateException("Cannot upload file: SSH session is not connected");
}
// 检查本地文件是否存在且可读
File localFile = new File(localFilePath);
if (!localFile.exists()) {
throw new FileNotFoundException("Local file not found: " + localFilePath);
}
if (!localFile.canRead()) {
throw new IOException("Cannot read local file: " + localFilePath);
}
ChannelSftp channel = null;
boolean uploadSuccess = false;
try {
// 创建并连接SFTP通道设置超时
channel = (ChannelSftp) sshSession.openChannel("sftp");
channel.connect(CONNECTION_TIMEOUT);
// 确保目标目录存在
createRemoteDirectoryIfNotExists(channel, getParentDirectory(remoteFilePath));
// 使用更健壮的上传方式
channel.put(
new FileInputStream(localFile),
remoteFilePath,
new ProgressMonitorAdapter(localFile.length()),
ChannelSftp.OVERWRITE
);
uploadSuccess = true;
log.info("File uploaded successfully: {} -> {}", localFilePath, remoteFilePath);
return true;
} catch (SftpException e) {
log.error("SFTP error during file upload ({} -> {}): {}",
localFilePath, remoteFilePath, e.getMessage(), e);
throw new IOException("SFTP error: " + e.getMessage(), e);
} catch (FileNotFoundException e) {
log.error("Local file not found during upload: {}", localFilePath, e);
throw e;
} catch (IOException e) {
log.error("IO error during file upload: {}", e.getMessage(), e);
throw e;
} catch (Exception e) {
log.error("Unexpected error during file upload: {}", e.getMessage(), e);
throw new IOException("Unexpected error: " + e.getMessage(), e);
} finally {
// 确保通道始终被关闭
disconnectChannel(channel);
// // 如果上传失败尝试删除不完整的文件
// if (!uploadSuccess && remoteFilePath != null && !remoteFilePath.isEmpty()) {
// tryDeleteIncompleteFile(remoteFilePath);
// }
}
}
public boolean downloadFile(String remoteFilePath, String localFilePath) throws IOException {
if (!isConnected()) {
throw new IllegalStateException("Cannot download file: SSH session is not connected");
}
// 检查本地目录是否存在且可写
File localFile = new File(localFilePath);
File parentDir = localFile.getParentFile();
if (parentDir != null && !parentDir.exists()) {
if (!parentDir.mkdirs()) {
throw new IOException("Failed to create local directory: " + parentDir.getAbsolutePath());
}
}
if (parentDir != null && !parentDir.canWrite()) {
throw new IOException("Cannot write to local directory: " + parentDir.getAbsolutePath());
}
ChannelSftp channel = null;
boolean downloadSuccess = false;
File tempFile = null;
try {
// 创建并连接SFTP通道设置超时
channel = (ChannelSftp) sshSession.openChannel("sftp");
channel.connect(CONNECTION_TIMEOUT);
// 检查远程文件是否存在
SftpATTRS attrs = channel.stat(remoteFilePath);
long fileSize = attrs.getSize();
// 使用临时文件避免部分下载覆盖完整文件
tempFile = new File(localFilePath + ".part");
// 执行下载并监控进度
channel.get(
remoteFilePath,
new FileOutputStream(tempFile).toString(),
new ProgressMonitorAdapter(fileSize),
ChannelSftp.OVERWRITE
);
// 验证下载完整性
if (tempFile.length() != fileSize) {
throw new IOException("Download incomplete: expected " + fileSize +
" bytes, but got " + tempFile.length() + " bytes");
}
// 重命名临时文件为目标文件原子操作
if (!tempFile.renameTo(localFile)) {
throw new IOException("Failed to rename temporary file to: " + localFilePath);
}
downloadSuccess = true;
log.info("File downloaded successfully: {} -> {}", remoteFilePath, localFilePath);
return true;
} catch (SftpException e) {
log.error("SFTP error during file download ({} -> {}): {}",
remoteFilePath, localFilePath, e.getMessage(), e);
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE) {
throw new FileNotFoundException("Remote file not found: " + remoteFilePath);
}
throw new IOException("SFTP error: " + e.getMessage(), e);
} catch (IOException e) {
log.error("IO error during file download: {}", e.getMessage(), e);
throw e;
} catch (Exception e) {
log.error("Unexpected error during file download: {}", e.getMessage(), e);
throw new IOException("Unexpected error: " + e.getMessage(), e);
} finally {
// 确保通道始终被关闭
disconnectChannel(channel);
// 如果下载失败清理临时文件
if (!downloadSuccess && tempFile != null && tempFile.exists()) {
if (tempFile.delete()) {
log.debug("Deleted incomplete temporary file: {}", tempFile.getAbsolutePath());
} else {
log.warn("Failed to delete incomplete temporary file: {}", tempFile.getAbsolutePath());
}
}
}
}
// 创建远程目录如果不存在
private void createRemoteDirectoryIfNotExists(ChannelSftp channel, String directory) throws SftpException {
if (directory == null || directory.isEmpty() || directory.equals("/")) {
return;
}
try {
channel.stat(directory);
} catch (SftpException e) {
// 目录不存在尝试创建
createRemoteDirectoryIfNotExists(channel, getParentDirectory(directory));
channel.mkdir(directory);
log.debug("Created remote directory: {}", directory);
}
}
// 获取路径的父目录
private String getParentDirectory(String path) {
int lastSlash = path.lastIndexOf('/');
return lastSlash > 0 ? path.substring(0, lastSlash) : "";
}
// 断开SFTP通道
private void disconnectChannel(Channel channel) {
if (channel != null && channel.isConnected()) {
try {
channel.disconnect();
log.debug("SFTP channel disconnected");
} catch (Exception e) {
log.warn("Error disconnecting SFTP channel: {}", e.getMessage());
}
}
}
// 尝试删除不完整的文件
private void tryDeleteIncompleteFile(String remoteFilePath) {
ChannelSftp channel = null;
try {
channel = (ChannelSftp) sshSession.openChannel("sftp");
channel.connect(CONNECTION_TIMEOUT);
channel.rm(remoteFilePath);
log.info("Deleted incomplete file: {}", remoteFilePath);
} catch (Exception e) {
log.warn("Failed to delete incomplete file {}: {}", remoteFilePath, e.getMessage());
} finally {
disconnectChannel(channel);
}
}
// 增强的进度监控器
private static class ProgressMonitorAdapter implements SftpProgressMonitor {
private final long totalBytes;
private long bytesWritten = 0;
private int lastProgress = 0;
private final long startTime = System.currentTimeMillis();
public ProgressMonitorAdapter(long totalBytes) {
this.totalBytes = totalBytes;
}
@Override
public boolean count(long count) {
bytesWritten += count;
// 计算进度百分比
int progress = (int) ((bytesWritten * 100) / totalBytes);
// 每10%或每秒更新一次日志
long elapsedTime = System.currentTimeMillis() - startTime;
if (progress - lastProgress >= 10 || elapsedTime >= 1000) {
double speed = bytesWritten / (elapsedTime / 1000.0);
String speedStr = formatTransferSpeed(speed);
log.debug("Upload progress: {}% ({}/{} bytes, {})",
progress, bytesWritten, totalBytes, speedStr);
lastProgress = progress;
}
return true; // 返回true继续传输返回false中断传输
}
@Override
public void end() {
long elapsedTime = System.currentTimeMillis() - startTime;
double speed = totalBytes / (elapsedTime / 1000.0);
String speedStr = formatTransferSpeed(speed);
log.info("Upload completed: {} bytes in {} ms (avg speed: {})",
totalBytes, elapsedTime, speedStr);
}
@Override
public void init(int op, String src, String dest, long max) {
log.info("Starting upload: {} -> {} ({} bytes)", src, dest, max);
}
// 格式化传输速度
private String formatTransferSpeed(double bytesPerSecond) {
String[] units = {"B/s", "KB/s", "MB/s", "GB/s"};
int unitIndex = 0;
while (bytesPerSecond >= 1024 && unitIndex < units.length - 1) {
bytesPerSecond /= 1024;
unitIndex++;
}
return String.format("%.2f %s", bytesPerSecond, units[unitIndex]);
}
}
/**
* 检查连接状态
*/
public ConnectionStatus getStatus() {
if (status == ConnectionStatus.CONNECTED &&
(sshSession == null || !sshSession.isConnected())) {
status = ConnectionStatus.DISCONNECTED;
}
return status;
}
/**
* 检查是否已连接
*/
public boolean isConnected() {
return status == ConnectionStatus.CONNECTED &&
sshSession != null &&
sshSession.isConnected();
}
private String getPrivateKeyContent(Long secretKeyId) {
if (secretKeyId == null) {
return null;
}
SecretKey secretKey = secretKeyService.getById(secretKeyId);
InputStream read = aliOssUtil.read(secretKey.getFileName());
try {
return StreamUtils.copyToString(read, StandardCharsets.UTF_8);
} catch (IOException e) {
log.error("读取私钥文件失败", e);
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,183 @@
package com.casic.machine.handler;
import com.casic.machine.contants.CommonConstants;
import com.casic.machine.enums.PermissionExceptionEnum;
import com.casic.machine.enums.RequestExceptionEnum;
import com.casic.machine.pojo.ErrorResponseData;
import com.casic.machine.pojo.ResponseData;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.naming.AuthenticationException;
import java.util.stream.Collectors;
@Order(-200)
@ControllerAdvice
public class GlobalExceptionHandler {
Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 请求参数异常
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
@ResponseBody
public ResponseData handleMissingParameterException(MissingServletRequestParameterException e) {
logger.error("请求参数异常{}", e.getMessage());
return renderErrorJson(
500,
String.format("缺少请求参数{%s},类型为{%s}", e.getParameterName(), e.getParameterType()),
e
);
}
/**
* 拦截参数格式传递异常
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseBody
public ResponseData handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
logger.error("参数格式传递异常{}",e.getMessage());
return renderErrorJson(
RequestExceptionEnum.REQUEST_JSON_ERROR.getCode(),
RequestExceptionEnum.REQUEST_JSON_ERROR.getMessage(),
e
);
}
/**
* 拦截不支持媒体类型异常
*/
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
@ResponseBody
public ResponseData handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) {
logger.error("不支持媒体类型异常{}",e.getMessage());
return renderErrorJson(
RequestExceptionEnum.REQUEST_TYPE_NOT_JSON.getCode(),
RequestExceptionEnum.REQUEST_TYPE_NOT_JSON.getMessage(),
e
);
}
/**
* 拦截请求方法异常
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@ResponseBody
public ResponseData handleHttpRequestMethodNotSupportedException(HttpServletRequest httpServletRequest) {
if ("GET".equalsIgnoreCase(httpServletRequest.getMethod())) {
logger.error("请求方法异常{}",RequestExceptionEnum.REQUEST_METHOD_NOT_POST.getMessage());
return renderErrorJson(
RequestExceptionEnum.REQUEST_METHOD_NOT_POST.getCode(),
RequestExceptionEnum.REQUEST_METHOD_NOT_POST.getMessage(),
null
);
}
if("POST".equalsIgnoreCase(httpServletRequest.getMethod())) {
logger.error("请求方法异常{}",RequestExceptionEnum.REQUEST_METHOD_NOT_GET.getMessage());
return renderErrorJson(
RequestExceptionEnum.REQUEST_METHOD_NOT_GET.getCode(),
RequestExceptionEnum.REQUEST_METHOD_NOT_GET.getMessage(),
null
);
}
return null;
}
/**
* 拦截资源找不到的运行时异常
*/
@ExceptionHandler(NoHandlerFoundException.class)
@ResponseBody
public ResponseData handleNoHandlerFoundException(NoHandlerFoundException e) {
logger.error("资源不存在异常{}",e.getMessage());
return renderErrorJson(
PermissionExceptionEnum.URL_NOT_EXIST.getCode(),
PermissionExceptionEnum.NO_PERMISSION.getMessage(),
e
);
}
/**
* 拦截参数校验错误异常,JSON传参
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ResponseData handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
logger.error("拦截参数校验错误异常,JSON传参{}",e.getMessage());
return renderErrorJson(
RequestExceptionEnum.PARAM_ERROR.getCode(),
getArgNotValidMessage(e.getBindingResult()),
e
);
}
/**
* 拦截认证失败异常
*/
@ExceptionHandler(AuthenticationException.class)
@ResponseBody
public ResponseData handleAuthenticationException(AuthenticationException e) {
logger.error("拦截认证失败异常{}",e.getMessage());
return renderErrorJson(
500,
e.getMessage(),
e
);
}
/*
* 渲染异常Json
*/
private ErrorResponseData renderErrorJson(int code, String msg,Throwable e) {
if (e != null) {
StackTraceElement[] stackTrace = e.getStackTrace();
//默认的异常类全路径为第一条异常堆栈信息的
String exceptionClassTotalName = stackTrace[0].toString();
//遍历所有堆栈信息找到cd.casic开头的第一条异常信息
for (StackTraceElement stackTraceElement : stackTrace) {
if (stackTraceElement.toString().contains(CommonConstants.DEFAULT_PACKAGE_NAME)) {
exceptionClassTotalName = stackTraceElement.toString();
break;
}
}
ErrorResponseData error = ResponseData.error(code, msg);
error.setExceptionClazz(exceptionClassTotalName);
return error;
}else {
return ResponseData.error(code, msg);
}
}
/**
* 获取请求参数不正确的提示信息
* 多个信息拼接成用逗号分隔的形式
*/
private String getArgNotValidMessage(BindingResult bindingResult) {
if (bindingResult == null) {
return "";
}
//多个错误用逗号分隔
return bindingResult.getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(","));
}
}

View File

@ -0,0 +1,14 @@
package com.casic.machine.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.casic.machine.entity.MachineEnv;
import org.apache.ibatis.annotations.Mapper;
/**
* 环境变量Mapper接口
*/
@Mapper
public interface MachineEnvMapper extends BaseMapper<MachineEnv> {
}

View File

@ -0,0 +1,9 @@
package com.casic.machine.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.casic.machine.entity.MachineInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface MachineInfoMapper extends BaseMapper<MachineInfo> {
}

View File

@ -0,0 +1,11 @@
package com.casic.machine.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.casic.machine.entity.MachineProxy;
import org.apache.ibatis.annotations.Mapper;
/**
* 机器代理Mapper接口
*/
@Mapper
public interface MachineProxyMapper extends BaseMapper<MachineProxy> {
}

View File

@ -0,0 +1,9 @@
package com.casic.machine.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.casic.machine.entity.SecretKey;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SecretServiceMapper extends BaseMapper<SecretKey> {
}

View File

@ -0,0 +1,26 @@
package com.casic.machine.pojo;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class ErrorResponseData extends ResponseData {
/**
* 异常的具体类名称
*/
private String exceptionClazz;
public ErrorResponseData(String message) {
super(false, DEFAULT_ERROR_CODE, message, null);
}
public ErrorResponseData(Integer code, String message) {
super(false, code, message, null);
}
public ErrorResponseData(Integer code, String message, Object object) {
super(false, code, message, object);
}
}

View File

@ -0,0 +1,72 @@
package com.casic.machine.pojo;
import lombok.Data;
/**
* 响应结果数据
*/
@Data
public class ResponseData {
public static final String DEFAULT_SUCCESS_MESSAGE = "请求成功";
public static final String DEFAULT_ERROR_MESSAGE = "网络异常";
public static final Integer DEFAULT_SUCCESS_CODE = 200;
public static final Integer DEFAULT_ERROR_CODE = 500;
/**
* 请求是否成功
*/
private Boolean success;
/**
* 响应状态码
*/
private Integer code;
/**
* 响应信息
*/
private String message;
/**
* 响应对象
*/
private Object data;
public ResponseData() {
}
public ResponseData(Boolean success, Integer code, String message, Object data) {
this.success = success;
this.code = code;
this.message = message;
this.data = data;
}
public static SuccessResponseData success() {
return new SuccessResponseData();
}
public static SuccessResponseData success(Object object) {
return new SuccessResponseData(object);
}
public static SuccessResponseData success(Integer code, String message, Object object) {
return new SuccessResponseData(code, message, object);
}
public static ErrorResponseData error(String message) {
return new ErrorResponseData(message);
}
public static ErrorResponseData error(Integer code, String message) {
return new ErrorResponseData(code, message);
}
public static ErrorResponseData error(Integer code, String message, Object object) {
return new ErrorResponseData(code, message, object);
}
}

View File

@ -0,0 +1,16 @@
package com.casic.machine.pojo;
public class SuccessResponseData extends ResponseData {
public SuccessResponseData() {
super(true, DEFAULT_SUCCESS_CODE, DEFAULT_SUCCESS_MESSAGE, null);
}
public SuccessResponseData(Object object) {
super(true, DEFAULT_SUCCESS_CODE, DEFAULT_SUCCESS_MESSAGE, object);
}
public SuccessResponseData(Integer code, String message, Object object) {
super(true, code, message, object);
}
}

View File

@ -0,0 +1,42 @@
package com.casic.machine.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.casic.machine.dto.MachineEnvDTO;
import com.casic.machine.entity.MachineEnv;
import com.casic.machine.utils.PageResult;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
/**
* 环境变量服务接口
*/
public interface MachineEnvService extends IService<MachineEnv> {
/**
* 创建或更新机器的环境变量一对一关系
*/
boolean add(MachineEnvDTO machineEnvDTO);
/**
* 删除机器的环境变量
* @param machineId 机器ID
*/
void deleteByMachineId(Long machineId);
/**
* 获取机器的环境变量
* @param machineId 机器ID
* @return 环境变量DTO
*/
MachineEnvDTO getByMachineId(Long machineId);
/**
* @return 环境变量列表
*/
PageResult<MachineEnvDTO> listEnv(MachineEnvDTO machineEnvDTO);
void deleteList(List<Long> ids);
boolean update(MachineEnvDTO machineEnvDTO);
}

View File

@ -0,0 +1,89 @@
package com.casic.machine.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.casic.machine.entity.MachineInfo;
import com.casic.machine.dto.MachineInfoDto;
import com.casic.machine.enums.ConnectionStatus;
import com.casic.machine.utils.PageResult;
import java.util.List;
import java.util.Map;
public interface MachineInfoService extends IService<MachineInfo> {
boolean addMachineInfo(MachineInfoDto MachineInfoDto);
PageResult<MachineInfoDto> listMachineInfo(MachineInfoDto MachineInfoDto);
boolean updateMachineInfo(MachineInfoDto machineInfoDto);
boolean updateStatus(Long machineInfoId, String status);
boolean bindingSecretKey( Long machineInfoId, Long secretKeyId);
void deleteList(List<Long> machineInfoIds);
void deleteMachineInfo(Long machineInfoId);
/**
* 测试机器连接
* @param machineInfo 机器信息
* @return 连接是否成功
*/
boolean testConnection(MachineInfo machineInfo);
/**
* 获取机器连接状态
* @param machineName 机器名称
* @return 连接状态
*/
ConnectionStatus getConnectionStatus(String machineName);
/**
* 获取所有连接状态
* @return 机器名称到连接状态的映射
*/
Map<String, ConnectionStatus> getAllConnectionStatus();
/**
* 建立机器连接
* @param machineInfo 机器信息
* @return 连接会话ID
*/
String connect(MachineInfo machineInfo);
/**
* 断开机器连接
* @param sessionId 会话ID
* @return 操作结果
*/
boolean disconnect(String sessionId);
/**
* 执行远程命令
* @param sessionId 会话ID
* @param command 命令
* @return 命令执行结果
*/
String executeCommand(String sessionId, String command);
/**
* 上传文件到远程机器
* @param sessionId 会话ID
* @param localFilePath 本地文件路径
* @param remoteFilePath 远程文件路径
* @return 操作结果
*/
boolean uploadFile(String sessionId, String localFilePath, String remoteFilePath);
/**
* 从远程机器下载文件
* @param sessionId 会话ID
* @param remoteFilePath 远程文件路径
* @param localFilePath 本地文件路径
* @return 操作结果
*/
boolean downloadFile(String sessionId, String remoteFilePath, String localFilePath);
}

View File

@ -0,0 +1,52 @@
package com.casic.machine.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.casic.machine.dto.MachineProxyDTO;
import com.casic.machine.entity.MachineProxy;
import com.casic.machine.utils.PageResult;
import java.util.List;
import java.util.Map;
/**
* 机器代理服务接口
*/
public interface MachineProxyService extends IService<MachineProxy> {
/**
* 注册新的机器代理
*/
boolean register(MachineProxyDTO machineProxyDTO);
/**
* 更新代理状态
*/
boolean updateStatus(MachineProxyDTO machineProxyDTO);
/**
* 接收代理心跳
*/
boolean heartbeat(MachineProxyDTO machineProxyDTO);
/**
* 获取所有代理的状态统计
*
* @return 状态统计Map
*/
Map<String, Long> getStatusStatistics();
/**
* 更新代理配置
*/
boolean updateConfig(MachineProxyDTO machineProxyDTO);
/**
* 批量删除代理
* @param proxyIds 代理ID列表
*/
void delete(List<Long> proxyIds);
PageResult<MachineProxyDTO> list(MachineProxyDTO machineProxyDTO);
}

View File

@ -0,0 +1,28 @@
package com.casic.machine.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.casic.machine.entity.SecretKey;
import com.casic.machine.dto.SecretKeyDto;
import com.casic.machine.utils.PageResult;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
public interface SecretKeyService extends IService<SecretKey> {
boolean addSecretKey(SecretKeyDto secretKeyDto, MultipartFile file);
void bindingMachine(Long secretKeyId, List<Long> machineInfoIds);
boolean updateSecretKey(SecretKeyDto secretKeyDto,MultipartFile file);
boolean deleteSecretKey(Long secretKeyId);
PageResult<SecretKey> listSecretKey(SecretKeyDto secretKeyDto);
ResponseEntity<InputStreamResource> downloadSecretKeyFile(Long secretKeyId);
boolean deleteList(List<Long> secretKeyIds);
}

View File

@ -0,0 +1,175 @@
package com.casic.machine.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.casic.machine.dto.MachineEnvDTO;
import com.casic.machine.entity.MachineEnv;
import com.casic.machine.exception.ServiceException;
import com.casic.machine.mapper.MachineEnvMapper;
import com.casic.machine.service.MachineEnvService;
import com.casic.machine.utils.PageResult;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
/**
* 环境变量服务实现类
*/
@Service
public class MachineEnvServiceImpl extends ServiceImpl<MachineEnvMapper, MachineEnv> implements MachineEnvService {
@Resource
private MachineEnvMapper machineEnvMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean add(MachineEnvDTO machineEnvDTO) {
// 参数校验
if (machineEnvDTO==null) {
throw new ServiceException(ServiceException.MACHINE_ENV_NULL,"环境变量不能为空");
}
// 检查键是否合法
if (!isValidKey(machineEnvDTO.getEnvKey())) {
throw new ServiceException(ServiceException.MACHINE_ENV_KEY_ILLEGAL,"环境变量键不合法");
}
// 判断是否敏感变量
boolean sensitive = isSensitive(machineEnvDTO.getEnvKey());
MachineEnv machineEnv = new MachineEnv();
BeanUtils.copyProperties(machineEnvDTO, machineEnv);
return save(machineEnv);
}
@Override
public void deleteByMachineId(Long machineId) {
this.removeById(machineId);
}
@Override
public MachineEnvDTO getByMachineId(Long machineId) {
if (machineId == null) {
return null;
}
MachineEnv machineEnv = getOne(
new LambdaQueryWrapper<MachineEnv>()
.eq(MachineEnv::getMachineId, machineId)
);
return machineEnv != null ? convertToDTO(machineEnv) : null;
}
@Override
public PageResult<MachineEnvDTO> listEnv(MachineEnvDTO machineEnvDTO) {
// 构建查询条件
LambdaQueryWrapper<MachineEnv> queryWrapper = new LambdaQueryWrapper<>();
// 环境变量键模糊查询
if (!StringUtils.isEmpty(machineEnvDTO.getEnvKey())) {
queryWrapper.like(MachineEnv::getEnvKey, machineEnvDTO.getEnvKey());
}
// 机器ID模糊查询
if (!StringUtils.isEmpty(machineEnvDTO.getMachineInfoId())) {
queryWrapper.like(MachineEnv::getMachineId, machineEnvDTO.getMachineInfoId());
}
// 是否敏感
if (machineEnvDTO.getSensitive() != null) {
queryWrapper.eq(MachineEnv::getSensitive, machineEnvDTO.getSensitive());
}
// 创建时间范围查询
if (!StringUtils.isEmpty(machineEnvDTO.getCreateDate())) {
queryWrapper.ge(MachineEnv::getCreateDate, machineEnvDTO.getCreateDate());
}
// 排序
if (!StringUtils.isEmpty(machineEnvDTO.getSortField())) {
boolean isAsc = "asc".equalsIgnoreCase(machineEnvDTO.getSortDirection());
switch (machineEnvDTO.getSortField()) {
case "envKey":
queryWrapper.orderBy(true, isAsc, MachineEnv::getEnvKey);
break;
case "machineId":
queryWrapper.orderBy(true, isAsc, MachineEnv::getMachineId);
break;
case "createTime":
default:
queryWrapper.orderBy(true, isAsc, MachineEnv::getCreateDate);
break;
}
}
// 分页查询
Page<MachineEnv> page = machineEnvMapper.selectPage(new Page<>(machineEnvDTO.getPageIndex(), machineEnvDTO.getPageSize()), queryWrapper);
// 转换结果
List<MachineEnvDTO> dtoList = page.getRecords().stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
// 构建分页结果
return PageResult.<MachineEnvDTO>builder()
.pageNum(page.getCurrent())
.pageSize(page.getSize())
.total(page.getTotal())
.pages(page.getPages())
.list(dtoList)
.build();
}
@Override
public void deleteList(List<Long> ids) {
this.machineEnvMapper.deleteBatchIds(ids);
}
@Override
public boolean update(MachineEnvDTO machineEnvDTO) {
MachineEnv machineEnv = new MachineEnv();
BeanUtils.copyProperties(machineEnvDTO,machineEnv);
return this.updateById(machineEnv);
}
// 转换实体为DTO
private MachineEnvDTO convertToDTO(MachineEnv machineEnv) {
MachineEnvDTO dto = new MachineEnvDTO();
dto.setId(machineEnv.getId());
dto.setEnvKey(machineEnv.getEnvKey());
dto.setEnvValue(machineEnv.getEnvValue());
dto.setSensitive(machineEnv.getSensitive());
dto.setDescription(machineEnv.getDescription());
dto.setCreateDate(machineEnv.getCreateDate());
dto.setUpdateDate(machineEnv.getUpdateDate());
return dto;
}
// 检查环境变量键是否合法
private boolean isValidKey(String key) {
return key.matches("^[a-zA-Z_][a-zA-Z0-9_]*$");
}
// 判断是否为敏感变量
private boolean isSensitive(String key) {
if (key == null) {
return false;
}
String upperKey = key.toUpperCase();
return upperKey.contains("PASSWORD") || upperKey.contains("SECRET") ||
upperKey.contains("TOKEN") || upperKey.contains("KEY");
}
}

View File

@ -0,0 +1,186 @@
package com.casic.machine.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.casic.machine.dto.MachineProxyDTO;
import com.casic.machine.entity.MachineProxy;
import com.casic.machine.enums.MachineProxyStatus;
import com.casic.machine.enums.MachineProxyType;
import com.casic.machine.exception.ServiceException;
import com.casic.machine.mapper.MachineProxyMapper;
import com.casic.machine.service.MachineProxyService;
import com.casic.machine.utils.EnumUtils;
import com.casic.machine.utils.PageResult;
import jakarta.annotation.Resource;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 机器代理服务实现类
*/
@Service
public class MachineProxyServiceImpl extends ServiceImpl<MachineProxyMapper, MachineProxy> implements MachineProxyService {
@Resource
private MachineProxyMapper machineProxyMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public boolean register(MachineProxyDTO machineProxyDTO) {
// 创建代理记录
MachineProxy proxy = new MachineProxy();
proxy.setProxyTypeCode(EnumUtils.getEnumByMessage(machineProxyDTO.getProxyType(), MachineProxyType.class).getCode());
proxy.setVersion("1.0.0");
proxy.setStatusCode(MachineProxyStatus.INSTALLING.getCode());
return save(proxy);
}
@Override
public boolean updateStatus(MachineProxyDTO machineProxyDTO) {
// 参数校验
if (machineProxyDTO == null) {
throw new ServiceException(ServiceException.MACHINE_PROXY_DTO_NULL,"MachineProxyDTO对象为空");
}
// 查询代理
MachineProxy proxy = this.getById(machineProxyDTO.getId());
if (proxy == null) {
throw new ServiceException(ServiceException.MACHINE_PROXY_NULL,"代理不存在");
}
// 更新状态
proxy.setStatusCode(EnumUtils.getEnumByMessage(machineProxyDTO.getStatus(),MachineProxyStatus.class).getCode());
proxy.setUpdateDate(new Date());
return updateById(proxy);
}
@Override
public boolean heartbeat(MachineProxyDTO machineProxyDTO) {
// 参数校验
if (machineProxyDTO == null) {
throw new IllegalArgumentException("MachineProxyDTO对象为空");
}
// 查询代理
MachineProxy proxy = this.getById(machineProxyDTO.getId());
if (proxy == null) {
throw new IllegalArgumentException("代理不存在");
}
// 更新心跳信息
proxy.setVersion(machineProxyDTO.getVersion());
proxy.setStatusCode(EnumUtils.getEnumByMessage(machineProxyDTO.getStatus(),MachineProxyStatus.class).getCode());
return updateById(proxy);
}
@Override
public Map<String, Long> getStatusStatistics() {
List<MachineProxy> proxyList = list();
if (CollectionUtils.isEmpty(proxyList)) {
return Collections.emptyMap();
}
return proxyList.stream()
.map(proxy -> EnumUtils.getEnumByCode(proxy.getStatusCode(), MachineProxyStatus.class).getMessage())
.collect(Collectors.groupingBy(
Function.identity(),
Collectors.counting() // 统计每个分组的元素数量
));
}
@Override
public boolean updateConfig(MachineProxyDTO machineProxyDTO) {
// 参数校验
if (machineProxyDTO == null) {
throw new ServiceException(ServiceException.MACHINE_PROXY_DTO_NULL,"MachineProxyDTO对象为空");
}
// 查询代理
MachineProxy proxy = getById(machineProxyDTO.getId());
if (proxy == null) {
throw new ServiceException(ServiceException.MACHINE_PROXY_NULL,"代理不存在");
}
// 更新配置
proxy.setConfig(machineProxyDTO.getConfig());
proxy.setUpdateDate(new Date());
return updateById(proxy);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(List<Long> ids) {
// 参数校验
if (CollectionUtils.isEmpty(ids)) {
throw new ServiceException(ServiceException.PARAMETER_ERROR,"参数错误");
}
// 查询在线代理
List<MachineProxy> onlineProxies = list(new LambdaQueryWrapper<MachineProxy>()
.in(MachineProxy::getId, ids)
.eq(MachineProxy::getStatus, MachineProxyStatus.ONLINE.getCode()));
if (!CollectionUtils.isEmpty(onlineProxies)) {
List<Long> onlineIds = onlineProxies.stream()
.map(MachineProxy::getId)
.collect(Collectors.toList());
throw new IllegalArgumentException("以下代理处于在线状态,无法删除: " + String.join( ",", (CharSequence) onlineIds));
}
// 批量逻辑删除
remove(new LambdaQueryWrapper<MachineProxy>()
.in(MachineProxy::getId, ids));
}
@Override
public PageResult<MachineProxyDTO> list(MachineProxyDTO machineProxyDTO) {
QueryWrapper<MachineProxy> queryWrapper = getMachineProxyQueryWrapper(machineProxyDTO);
Page<MachineProxy> page = machineProxyMapper.selectPage(new Page<>(machineProxyDTO.getPageIndex(), machineProxyDTO.getPageSize()), queryWrapper);
List<MachineProxyDTO> machineProxyDtos = page.getRecords().stream().map(machineProxy -> {
MachineProxyDTO dto = new MachineProxyDTO();
BeanUtils.copyProperties(machineProxy, dto);
dto.setProxyType(EnumUtils.getEnumByCode(machineProxy.getStatusCode(), MachineProxyType.class).getMessage());
dto.setStatus(EnumUtils.getEnumByCode(machineProxy.getStatusCode(), MachineProxyStatus.class).getMessage());
return dto;
}).toList();
return new PageResult<>(
page.getCurrent(),
page.getSize(),
page.getTotal(),
page.getPages(),
machineProxyDtos
);
}
private QueryWrapper<MachineProxy> getMachineProxyQueryWrapper(MachineProxyDTO machineProxyDTO){
QueryWrapper<MachineProxy> queryWrapper = new QueryWrapper<>();
if (machineProxyDTO.getHostIp() != null && !machineProxyDTO.getHostIp().isEmpty()) {
queryWrapper.eq("host_ip", machineProxyDTO.getHostIp());
}
if (machineProxyDTO.getSshPort() != null && !machineProxyDTO.getSshPort().isEmpty()) {
queryWrapper.eq("ssh_port", machineProxyDTO.getSshPort());
}
if (machineProxyDTO.getUsername() != null && !machineProxyDTO.getUsername().isEmpty()) {
queryWrapper.eq("username", machineProxyDTO.getUsername());
}
if (machineProxyDTO.getDescription() != null && !machineProxyDTO.getDescription().isEmpty()) {
queryWrapper.eq("description", machineProxyDTO.getDescription());
}
if (machineProxyDTO.getStatus() != null && !machineProxyDTO.getStatus().isEmpty()) {
queryWrapper.eq("status_code", EnumUtils.getEnumByMessage(machineProxyDTO.getStatus(),MachineProxyStatus.class).getCode());
}
return queryWrapper;
}
}

View File

@ -0,0 +1,341 @@
package com.casic.machine.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.casic.machine.entity.MachineInfo;
import com.casic.machine.dto.MachineInfoDto;
import com.casic.machine.enums.AuthenticationType;
import com.casic.machine.enums.ConnectionStatus;
import com.casic.machine.enums.MachineInfoStatus;
import com.casic.machine.exception.ServiceException;
import com.casic.machine.handler.ConnectionSession;
import com.casic.machine.mapper.MachineInfoMapper;
import com.casic.machine.service.MachineInfoService;
import com.casic.machine.utils.EnumUtils;
import com.casic.machine.utils.PageResult;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Service
public class MachineinfoServiceImpl extends ServiceImpl<MachineInfoMapper, MachineInfo> implements MachineInfoService {
int ENABLE=1;
int UN_ENABLE=0;
@Resource
private MachineInfoMapper machineInfoMapper;
/**
* 会话ID生成器
*/
private final AtomicInteger sessionIdGenerator = new AtomicInteger(1000);
/**
* 会话管理会话ID -> 连接会话
*/
private final Map<String, ConnectionSession> sessions = new ConcurrentHashMap<>();
/**
* 机器名称 -> 会话ID
*/
private final Map<String, String> machineSessionMapping = new ConcurrentHashMap<>();
@Override
public boolean addMachineInfo(MachineInfoDto machineInfoDto) {
if (machineInfoDto == null) {
throw new ServiceException(ServiceException.MACHINE_INFO_NULL, "机器信息为空");
}
MachineInfo machineInfo = new MachineInfo();
BeanUtils.copyProperties(machineInfoDto, machineInfo);
machineInfo.setStatusCode(1);
machineInfo.setAuthenticationTypeCode(
"密码认证".equals(machineInfoDto.getAuthenticationType())
? AuthenticationType.PASSWORD.getCode()
: AuthenticationType.SECRET_KEY.getCode()
);
return this.save(machineInfo);
}
@Override
public PageResult<MachineInfoDto> listMachineInfo(MachineInfoDto machineInfoDto) {
QueryWrapper<MachineInfo> queryWrapper = getMachineInfoQueryWrapper(machineInfoDto);
Page<MachineInfo> page = machineInfoMapper.selectPage(
new Page<>(machineInfoDto.getPageIndex(), machineInfoDto.getPageSize()),
queryWrapper
);
List<MachineInfoDto> machineInfoDtos = page.getRecords().stream()
.map(machineInfo -> {
MachineInfoDto dto = new MachineInfoDto();
BeanUtils.copyProperties(machineInfo, dto);
// 直接调用原有枚举转换方法
dto.setStatus(EnumUtils.getEnumByCode(machineInfo.getStatus(), MachineInfoStatus.class).getMessage());
dto.setAuthenticationType(EnumUtils.getEnumByCode(machineInfo.getAuthenticationType(), AuthenticationType.class).getMessage());
return dto;
})
.toList();
return new PageResult<>(
page.getCurrent(),
page.getSize(),
page.getTotal(),
page.getPages(),
machineInfoDtos
);
}
@Override
public boolean updateMachineInfo(MachineInfoDto machineInfoDto) {
MachineInfo machineInfo = new MachineInfo();
BeanUtils.copyProperties(machineInfoDto, machineInfo);
machineInfo.setAuthenticationType(
"密码认证".equals(machineInfoDto.getAuthenticationType())
? AuthenticationType.PASSWORD
: AuthenticationType.SECRET_KEY
);
return this.updateById(machineInfo);
}
@Override
public boolean updateStatus(Long machineInfoId, String status) {
UpdateWrapper<MachineInfo> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", machineInfoId).set("status", status);
return this.update(updateWrapper);
}
@Override
public boolean bindingSecretKey(Long machineInfoId, Long secretKeyId) {
UpdateWrapper<MachineInfo> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", machineInfoId).set("secretKeyId", secretKeyId);
return this.update(updateWrapper);
}
@Override
public void deleteList(List<Long> machineInfoIds) {
machineInfoMapper.selectBatchIds(machineInfoIds).forEach(machineInfo -> {
if (machineInfo.getStatusCode() == 1){
this.removeById(machineInfo.getId());
}
});
}
@Override
public void deleteMachineInfo(Long machineInfoId) {
MachineInfo machineInfo = this.getById(machineInfoId);
if (machineInfo.getStatusCode() == 1){
this.removeById(machineInfoId);
}
}
private QueryWrapper<MachineInfo> getMachineInfoQueryWrapper(MachineInfoDto machineInfoDto) {
QueryWrapper<MachineInfo> queryWrapper = new QueryWrapper<>();
if (machineInfoDto.getStatus() != null && !machineInfoDto.getStatus().isEmpty()){
queryWrapper.eq("status_code", EnumUtils.getEnumByMessage(machineInfoDto.getStatus(), MachineInfoStatus.class).getCode());
}
if (machineInfoDto.getName() != null && !machineInfoDto.getName().isEmpty()) {
queryWrapper.like("name", machineInfoDto.getName());
}
if (machineInfoDto.getTag() != null && !machineInfoDto.getTag().isEmpty()) {
queryWrapper.like("tag", machineInfoDto.getTag());
}
if (machineInfoDto.getHostIp() != null && !machineInfoDto.getHostIp().isEmpty()) {
queryWrapper.like("host_ip", machineInfoDto.getHostIp());
}
if (machineInfoDto.getDescription() != null && !machineInfoDto.getDescription().isEmpty()) {
queryWrapper.like("description", machineInfoDto.getDescription());
}
return queryWrapper;
}
@Override
public boolean testConnection(MachineInfo machineInfo) {
if (machineInfo.getStatus().getCode()==UN_ENABLE) {
throw new RuntimeException("机器不可用");
}
log.info("测试机器连接: {}", machineInfo.getHostIp());
try (ConnectionSession session = createSession(machineInfo)) {
session.connect();
return true;
} catch (Exception e) {
log.error("机器连接测试失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public ConnectionStatus getConnectionStatus(String machineName) {
String sessionId = machineSessionMapping.get(machineName);
if (sessionId == null) {
return ConnectionStatus.DISCONNECTED;
}
ConnectionSession session = sessions.get(sessionId);
return session != null ? session.getStatus() : ConnectionStatus.DISCONNECTED;
}
@Override
public Map<String, ConnectionStatus> getAllConnectionStatus() {
Map<String, ConnectionStatus> result = new HashMap<>();
machineSessionMapping.forEach((machineName, sessionId) -> {
ConnectionSession session = sessions.get(sessionId);
result.put(machineName, session != null ? session.getStatus() : ConnectionStatus.DISCONNECTED);
});
return result;
}
@Override
public String connect(MachineInfo machineInfo) {
if (machineInfo.getStatus().getCode()==UN_ENABLE) {
throw new RuntimeException("机器不可用");
}
log.info("建立机器连接: {}", machineInfo.getHostIp());
// 检查是否已连接
String existingSessionId = machineSessionMapping.get(machineInfo.getName());
if (existingSessionId != null) {
ConnectionSession existingSession = sessions.get(existingSessionId);
if (existingSession != null && existingSession.getStatus() == ConnectionStatus.CONNECTED) {
log.info("机器已连接,返回现有会话: {}", machineInfo.getHostIp());
return existingSessionId;
}
}
try {
ConnectionSession session = createSession(machineInfo);
session.connect();
// 生成会话ID
String sessionId = generateSessionId();
// 保存会话
sessions.put(sessionId, session);
machineSessionMapping.put(machineInfo.getName(), sessionId);
log.info("机器连接成功: {}, 会话ID: {}", machineInfo.getHostIp(), sessionId);
return sessionId;
} catch (Exception e) {
log.error("机器连接失败: {}", e.getMessage(), e);
throw new RuntimeException("机器连接失败: " + e.getMessage(), e);
}
}
@Override
public boolean disconnect(String sessionId) {
log.info("断开机器连接: {}", sessionId);
ConnectionSession session = sessions.get(sessionId);
if (session == null) {
log.warn("会话不存在: {}", sessionId);
return false;
}
try {
session.disconnect();
// 清理会话
sessions.remove(sessionId);
machineSessionMapping.entrySet().removeIf(entry -> entry.getValue().equals(sessionId));
log.info("机器连接已断开: {}", sessionId);
return true;
} catch (Exception e) {
log.error("断开连接失败: {}", e.getMessage(), e);
return false;
}
}
@Override
public String executeCommand(String sessionId, String command) {
log.info("执行命令: {}, 会话ID: {}", command, sessionId);
ConnectionSession session = sessions.get(sessionId);
if (session == null) {
throw new RuntimeException("会话不存在: " + sessionId);
}
if (session.getStatus() != ConnectionStatus.CONNECTED) {
throw new RuntimeException("会话未连接: " + sessionId);
}
try {
return session.executeCommand(command);
} catch (Exception e) {
log.error("命令执行失败: {}", e.getMessage(), e);
throw new RuntimeException("命令执行失败: " + e.getMessage(), e);
}
}
@Override
public boolean uploadFile(String sessionId, String localFilePath, String remoteFilePath) {
log.info("上传文件: {} -> {}, 会话ID: {}", localFilePath, remoteFilePath, sessionId);
ConnectionSession session = sessions.get(sessionId);
if (session == null) {
throw new RuntimeException("会话不存在: " + sessionId);
}
if (session.getStatus() != ConnectionStatus.CONNECTED) {
throw new RuntimeException("会话未连接: " + sessionId);
}
try {
return session.uploadFile(localFilePath, remoteFilePath);
} catch (Exception e) {
log.error("文件上传失败: {}", e.getMessage(), e);
throw new RuntimeException("文件上传失败: " + e.getMessage(), e);
}
}
@Override
public boolean downloadFile(String sessionId, String remoteFilePath, String localFilePath) {
log.info("下载文件: {} -> {}, 会话ID: {}", remoteFilePath, localFilePath, sessionId);
ConnectionSession session = sessions.get(sessionId);
if (session == null) {
throw new RuntimeException("会话不存在: " + sessionId);
}
if (session.getStatus() != ConnectionStatus.CONNECTED) {
throw new RuntimeException("会话未连接: " + sessionId);
}
try {
return session.downloadFile(remoteFilePath, localFilePath);
} catch (Exception e) {
log.error("文件下载失败: {}", e.getMessage(), e);
throw new RuntimeException("文件下载失败: " + e.getMessage(), e);
}
}
/**
* 创建连接会话
*/
private ConnectionSession createSession(MachineInfo machineInfo) {
return new ConnectionSession(machineInfo);
}
/**
* 生成会话ID
*/
private String generateSessionId() {
return "session-" + sessionIdGenerator.incrementAndGet();
}
}

View File

@ -0,0 +1,133 @@
package com.casic.machine.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.casic.machine.configuration.AliYunConfig;
import com.casic.machine.entity.MachineInfo;
import com.casic.machine.entity.SecretKey;
import com.casic.machine.dto.SecretKeyDto;
import com.casic.machine.exception.ServiceException;
import com.casic.machine.mapper.SecretServiceMapper;
import com.casic.machine.service.MachineInfoService;
import com.casic.machine.service.SecretKeyService;
import com.casic.machine.utils.AliOssUtil;
import com.casic.machine.utils.PageResult;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Service
public class SecretKeyServiceImpl extends ServiceImpl<SecretServiceMapper, SecretKey> implements SecretKeyService {
@Resource
private AliOssUtil aliOssUtil;
@Resource
private MachineInfoService machineInfoService;
@Resource
private AliYunConfig aliYunConfig;
@Resource
private SecretServiceMapper secretServiceMapper;
//todo public方便测试后面改为private
public static final ExecutorService FILE_DELETE_EXECUTOR = Executors.newFixedThreadPool(10);
Logger logger = LoggerFactory.getLogger(SecretKeyServiceImpl.class);
@Override
public boolean addSecretKey(SecretKeyDto secretKeyDto, MultipartFile file) {
secretKeyDto.setFile(file);
String fileName = aliOssUtil.save(file);
String path = "https://" + aliYunConfig.getBucketName() + "." + aliYunConfig.getEndpoint() + "/" + fileName;
secretKeyDto.setPath(path);
secretKeyDto.setFileName(fileName);
SecretKey secretKey = new SecretKey();
BeanUtils.copyProperties(secretKeyDto,secretKey);
return this.save(secretKey);
}
@Override
public void bindingMachine(Long secretKeyId, List<Long> machineInfoIds) {
List<MachineInfo> machineInfos = machineInfoService.listByIds(machineInfoIds);
machineInfos.forEach(machineInfo -> machineInfo.setSecretKeyId(secretKeyId));
}
@Override
public boolean updateSecretKey(SecretKeyDto secretKeyDto, MultipartFile file) {
if (file != null){
secretKeyDto.setFile(file);
String fileName = aliOssUtil.save(file);
String path = "https://" + aliYunConfig.getBucketName() + "." + aliYunConfig.getEndpoint() + "/" + fileName;
secretKeyDto.setPath(path);
secretKeyDto.setFileName(fileName);
}
SecretKey secretKey = new SecretKey();
BeanUtils.copyProperties(secretKeyDto,secretKey);
return this.updateById(secretKey);
}
@Override
public boolean deleteSecretKey(Long secretKeyId) {
String fileName = this.getById(secretKeyId).getFileName();
aliOssUtil.deleteFile(fileName);
return this.removeById(secretKeyId);
}
@Override
public PageResult<SecretKey> listSecretKey(SecretKeyDto secretKeyDto) {
QueryWrapper<SecretKey> queryWrapper = new QueryWrapper<>();
if (secretKeyDto.getName() != null && !secretKeyDto.getName().isEmpty()){
queryWrapper.like("name", secretKeyDto.getName());
}
if (secretKeyDto.getDescription() != null && !secretKeyDto.getDescription().isEmpty()){
queryWrapper.like("description", secretKeyDto.getDescription());
}
Page<SecretKey> page = secretServiceMapper.selectPage(new Page<>(secretKeyDto.getPageIndex(), secretKeyDto.getPageSize()), queryWrapper);
return new PageResult<>(
page.getCurrent(),
page.getSize(),
page.getTotal(),
page.getPages(),
page.getRecords()
);
}
@Override
public ResponseEntity<InputStreamResource> downloadSecretKeyFile(Long secretKeyId) {
String fileName = this.getById(secretKeyId).getFileName();
return aliOssUtil.downloadFile(fileName);
}
@Override
public boolean deleteList(List<Long> secretKeyIds) {
List<SecretKey> secretKeys = this.listByIds(secretKeyIds);
// 提交异步任务到线程池
FILE_DELETE_EXECUTOR.execute(() -> {
try {
for (SecretKey secretKey : secretKeys) {
if (secretKey.getFileName() != null && !secretKey.getFileName().isEmpty()){
aliOssUtil.deleteFile(secretKey.getFileName());
}
}
} catch (Exception e) {
logger.error("异步删除文件失败:{}", e.getMessage());
throw new ServiceException(ServiceException.DELETE_FILE_FAIL,"异步删除文件失败:"+e.getMessage());
}
});
return secretServiceMapper.deleteBatchIds(secretKeyIds) > 0 ;
}
}

View File

@ -0,0 +1,113 @@
package com.casic.machine.utils;
import com.aliyun.oss.OSS;
import com.aliyun.oss.model.OSSObject;
import com.casic.machine.configuration.AliYunConfig;
import com.casic.machine.exception.ServiceException;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@Component
public class AliOssUtil{
@Autowired
private OSS ossClient;
@Resource
private AliYunConfig aliyunConfig;
private static final Logger logger = LoggerFactory.getLogger(AliOssUtil.class);
public String save(MultipartFile file) {
try {
String fileName = generateUniqueFileName(file.getName());
ossClient.putObject(
// 存储桶名称
aliyunConfig.getBucketName(),
//对象键Object Key即文件在 OSS 中的完整路径和名称
fileName,
//文件内容的输入流用于读取待上传的文件数据
file.getInputStream(),
//文件的元数据信息如内容类型Content-Type缓存策略文件大小等
null
);
return fileName;
} catch (Exception e) {
logger.info("文件上传失败:{}", e.getMessage());
throw new ServiceException(ServiceException.UPLOADING_FILE_FAIL, "上传文件失败:" + e.getMessage());
}
}
public ResponseEntity<InputStreamResource> downloadFile(String fileName) {
try {
// 读取文件流
InputStream inputStream = read(fileName);
// 处理文件名编码防止中文乱码
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)
.replaceAll("\\+", "%20");
// 设置响应头
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + encodedFileName);
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setCacheControl("no-cache, no-store, must-revalidate");
return ResponseEntity.ok()
.headers(headers)
.body(new InputStreamResource(inputStream));
} catch (Exception e) {
logger.error("下载失败:{}", e.getMessage());
throw new ServiceException(ServiceException.DOWNLOAD_FILE_FAIL, "下载失败: " + e.getMessage());
}
}
public void deleteFile(String fileName) {
try {
if (fileName == null || fileName.isEmpty()) {
throw new ServiceException(ServiceException.FILENAME_NULL, "文件名不能为空");
}
// 调用 OSS 客户端删除文件
ossClient.deleteObject(aliyunConfig.getBucketName(), fileName);
logger.info("文件删除成功: {}", fileName);
} catch (Exception e) {
logger.error("删除文件失败: {}", e.getMessage());
throw new ServiceException(ServiceException.DELETE_FILE_FAIL, "删除文件失败: " + e.getMessage());
}
}
public InputStream read(String fileName) {
try {
if (fileName == null || fileName.isEmpty()) {
throw new ServiceException(ServiceException.FILENAME_NULL, "文件名不能为空");
}
OSSObject ossObject = ossClient.getObject(aliyunConfig.getBucketName(), fileName);
return ossObject.getObjectContent();
} catch (Exception e) {
logger.error("读取文件失败:{}", e.getMessage());
throw new ServiceException(ServiceException.READ_FILE_FAIL, "读取文件失败: " + e.getMessage());
}
}
// 生成带时间戳的唯一文件名
private String generateUniqueFileName(String originalFileName) {
String timestamp = String.valueOf(System.currentTimeMillis());
String ext = originalFileName.substring(originalFileName.lastIndexOf("."));
return timestamp + ext;
}
}

View File

@ -0,0 +1,50 @@
package com.casic.machine.utils;
import com.casic.machine.enums.CodeEnum;
public class EnumUtils {
/**
* 根据code和枚举类型获取对应的枚举值
*
* @param code 枚举的code值
* @param enumClass 实现了CodeEnum接口的枚举类
* @param <T> 枚举类型
* @return 对应的枚举值若未找到则返回null
*/
public static <T extends CodeEnum> T getEnumByCode(Object code, Class<T> enumClass) {
if (code == null || enumClass == null) {
return null;
}
// 遍历枚举值查找匹配的code
for (T enumConstant : enumClass.getEnumConstants()) {
if (code.equals(enumConstant.getCode())) {
return enumConstant;
}
}
return null;
}
/**
* 根据message和枚举类型获取对应的枚举值
*
* @param message 枚举的message值
* @param enumClass 实现了CodeEnum接口的枚举类
* @param <T> 枚举类型
* @return 对应的枚举值若未找到则返回null
*/
public static <T extends CodeEnum> T getEnumByMessage(String message, Class<T> enumClass) {
if (message == null || enumClass == null) {
return null;
}
// 遍历枚举值查找匹配的message
for (T enumConstant : enumClass.getEnumConstants()) {
if (message.equals(enumConstant.getMessage())) {
return enumConstant;
}
}
return null;
}
}

View File

@ -0,0 +1,45 @@
package com.casic.machine.utils;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 分页结果通用类
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageResult<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 当前页码
*/
private Long pageNum;
/**
* 每页数量
*/
private Long pageSize;
/**
* 总记录数
*/
private Long total;
/**
* 总页数
*/
private Long pages;
/**
* 数据列表
*/
private List<T> list;
}

View File

@ -0,0 +1,35 @@
package com.casic.machine.utils;
import io.micrometer.common.util.StringUtils;
import java.util.function.Consumer;
/**
* 属性操作工具类
*/
public class PropertyUtils {
/**
* 如果不为空则设置值
*
* @param value
* @param setter 具体set操作
*/
public static void setIfNotBlank(String value, Consumer<String> setter) {
if (StringUtils.isNotBlank(value)) {
setter.accept(value);
}
}
/**
* 如果不为空则设置值
*
* @param value
* @param setter 具体set操作
*/
public static <T> void setIfNotNull(T value, Consumer<T> setter) {
if (value != null) {
setter.accept(value);
}
}
}

View File

@ -0,0 +1,14 @@
spring:
application:
name: machine-management-module
# datasource:
# driver-class-name: com.mysql.cj.jdbc.Driver
# url: jdbc:mysql://localhost:3306/resource_management?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
# username:
# password:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/resource_management?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false
username: root
password: 031219

View File

@ -0,0 +1,37 @@
spring:
application:
name: machine-management-module
profiles:
active: dev
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.casic.entity
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
logging:
pattern:
console: '%d{HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n'
server:
port: 10111
#SpringDoc访问路径
springdoc.swagger-ui.path: /api-docs.html
aliyun:
oss:
# OSS 服务的访问域名
endpoint: "zyj031219.xin"
# 访问密钥 ID
accessKeyId: "LTAI5tKnFb95ytubCbYfmf5g"
# 访问密钥 Secret
accessKeySecret: "EDtyMKuu5MtViiMoDgAYkWL0JV34AF"
# 存储空间名称
bucketName: "zyjtest031219"

View File

@ -0,0 +1,184 @@
package com.casic.machine.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.casic.commons.exception.ServiceException;
import com.casic.commons.utils.PageResult;
import com.casic.machine.dto.MachineEnvDTO;
import com.casic.machine.entity.MachineEnv;
import com.casic.machine.mapper.MachineEnvMapper;
import com.casic.machine.service.MachineEnvService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
@Rollback(value = true) // 测试后回滚数据
@Sql(scripts = {"classpath:sql/machine_env_test_data.sql"}) // 初始化测试数据可选
public class MachineEnvServiceImplTest {
@Autowired
private MachineEnvService machineEnvService;
@Autowired
private MachineEnvMapper machineEnvMapper;
private MachineEnvDTO validDto;
private MachineEnvDTO invalidKeyDto;
private Long existingMachineId;
private Long nonExistingMachineId;
@BeforeEach
public void setUp() {
// 准备测试数据
existingMachineId = 1L; // 假设数据库中存在ID为1的机器环境变量
nonExistingMachineId = 999L; // 不存在的机器ID
// 有效测试数据
validDto = new MachineEnvDTO();
validDto.setMachineInfoId(existingMachineId);
validDto.setEnvKey("TEST_ENV_KEY");
validDto.setEnvValue("test-value");
validDto.setSensitive(true);
// 无效Key测试数据包含非法字符
invalidKeyDto = new MachineEnvDTO();
invalidKeyDto.setMachineInfoId(existingMachineId);
invalidKeyDto.setEnvKey("test-env-key"); // 包含'-'不符合正则
invalidKeyDto.setEnvValue("test-value");
}
// ==================== 更新环境变量测试 ====================
@Test
void testUpdateEnv_ValidData_ShouldSucceed() {
// 执行更新假设数据库中已存在machineId=1的记录
boolean result = machineEnvService.update(validDto);
// 验证结果
assertTrue(result);
// 检查数据库数据是否更新
MachineEnv updatedEnv = machineEnvMapper.selectOne(
new LambdaQueryWrapper<MachineEnv>().eq(MachineEnv::getMachineId, existingMachineId)
);
assertNotNull(updatedEnv);
assertEquals(validDto.getEnvKey(), updatedEnv.getEnvKey());
assertEquals(validDto.getEnvValue(), updatedEnv.getEnvValue());
assertEquals(validDto.getSensitive(), updatedEnv.getSensitive());
}
@Test
void testUpdateEnv_NullDto_ShouldThrowException() {
MachineEnvDTO machineEnvDTO = new MachineEnvDTO();
assertThrows(ServiceException.class, () -> {
machineEnvService.update(machineEnvDTO);
}, "环境变量不能为空");
}
@Test
void testUpdateEnv_InvalidKey_ShouldThrowException() {
assertThrows(ServiceException.class, () -> {
machineEnvService.update(invalidKeyDto);
}, "环境变量键不合法");
}
@Test
void testUpdateEnv_SensitiveKey_ShouldMarkAsSensitive() {
MachineEnvDTO sensitiveDto = new MachineEnvDTO();
sensitiveDto.setMachineInfoId(existingMachineId);
sensitiveDto.setEnvKey("DB_PASSWORD"); // 包含敏感词
sensitiveDto.setEnvValue("secret");
machineEnvService.update(sensitiveDto);
MachineEnv env = machineEnvMapper.selectOne(
new LambdaQueryWrapper<MachineEnv>().eq(MachineEnv::getMachineId, existingMachineId)
);
assertTrue(env.getSensitive());
}
// ==================== 删除环境变量测试 ====================
@Test
void testDeleteByMachineId_ExistingId_ShouldSucceed() {
machineEnvService.deleteByMachineId(existingMachineId);
MachineEnv env = machineEnvMapper.selectById(existingMachineId);
assertNull(env);
}
@Test
void testDeleteByMachineId_NonExistingId_ShouldDoNothing() {
machineEnvService.deleteByMachineId(nonExistingMachineId);
// 不抛出异常静默处理
}
// ==================== 根据机器ID查询测试 ====================
@Test
void testGetByMachineId_ExistingId_ShouldReturnDto() {
MachineEnvDTO dto = machineEnvService.getByMachineId(existingMachineId);
assertNotNull(dto);
assertEquals(existingMachineId, dto.getMachineInfoId());
}
@Test
void testGetByMachineId_NonExistingId_ShouldReturnNull() {
MachineEnvDTO dto = machineEnvService.getByMachineId(nonExistingMachineId);
assertNull(dto);
}
// ==================== 列表查询测试 ====================
@Test
void testListEnv_Pagination_ShouldReturnValidPage() {
MachineEnvDTO queryDto = new MachineEnvDTO();
queryDto.setPageIndex(1);
queryDto.setPageSize(10);
queryDto.setEnvKey("TEST"); // 假设测试数据中存在包含"TEST"的键
PageResult<MachineEnvDTO> pageResult = machineEnvService.listEnv(queryDto);
assertNotNull(pageResult.getList());
assertTrue(pageResult.getTotal() >= 0);
assertEquals(1, pageResult.getPageNum());
}
@Test
void testListEnv_SortByCreateTime_ShouldBeOrdered() {
MachineEnvDTO queryDto = new MachineEnvDTO();
queryDto.setSortField("createTime");
queryDto.setSortDirection("desc");
PageResult<MachineEnvDTO> pageResult = machineEnvService.listEnv(queryDto);
List<MachineEnvDTO> list = pageResult.getList();
if (!list.isEmpty()) {
Date prevDate = list.get(0).getCreateDate();
for (int i = 1; i < list.size(); i++) {
Date currDate = list.get(i).getCreateDate();
assertTrue(currDate.before(prevDate) || currDate.equals(prevDate), "排序应为降序");
prevDate = currDate;
}
}
}
// ==================== 批量删除测试 ====================
@Test
void testDeleteList_ValidIds_ShouldDeleteBatch() {
// 假设测试数据中有ID为1和2的记录
List<Long> ids = List.of(1L, 2L);
machineEnvService.deleteList(ids);
long count = machineEnvMapper.selectCount(new LambdaQueryWrapper<MachineEnv>().in(MachineEnv::getId, ids));
assertEquals(0, count);
}
}

View File

@ -0,0 +1,222 @@
package com.casic.machine.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.casic.commons.exception.ServiceException;
import com.casic.commons.utils.EnumUtils;
import com.casic.commons.utils.PageResult;
import com.casic.machine.dto.MachineProxyDTO;
import com.casic.machine.entity.MachineProxy;
import com.casic.machine.enums.MachineProxyStatus;
import com.casic.machine.enums.MachineProxyType;
import com.casic.machine.mapper.MachineProxyMapper;
import com.casic.machine.service.MachineProxyService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
@Rollback(true)
@Sql(scripts = {"classpath:sql/machine_proxy_test_data.sql"})
public class MachineProxyServiceImplTest {
@Autowired
private MachineProxyService machineProxyService;
@Autowired
private MachineProxyMapper machineProxyMapper;
private MachineProxyDTO validProxyDTO;
private Long existingProxyId;
private Long nonExistingProxyId;
@BeforeEach
public void setUp() {
// 初始化测试数据假设 SQL 脚本已插入一条状态为 OFFLINE 的代理
existingProxyId = 1L;
nonExistingProxyId = 999L;
// 有效代理 DTO
validProxyDTO = new MachineProxyDTO();
validProxyDTO.setProxyType(MachineProxyType.SOCKS5.getMessage());
validProxyDTO.setHostIp("192.168.1.100");
validProxyDTO.setSshPort("22");
validProxyDTO.setUsername("test_user");
validProxyDTO.setStatus(MachineProxyStatus.ONLINE.getMessage());
}
// ============================== 注册代理测试 ==============================
@Test
void testRegister_ValidData_ShouldSucceed() {
// 执行注册
boolean result = machineProxyService.register(validProxyDTO);
assertTrue(result, "注册失败");
// 使用 Lambda 表达式查询推荐
MachineProxy proxy = machineProxyMapper.selectOne(
new LambdaQueryWrapper<MachineProxy>()
.eq(MachineProxy::getHostIp, validProxyDTO.getHostIp())
);
// 断言数据存在
assertNotNull(proxy, "代理记录未写入数据库");
assertEquals(MachineProxyType.SOCKS5.getCode(), proxy.getProxyTypeCode());
assertEquals(MachineProxyStatus.INSTALLING.getCode(), proxy.getStatusCode());
assertEquals(validProxyDTO.getHostIp(), proxy.getHostIp(), "IP 地址不一致");
}
// ============================== 更新状态测试 ==============================
@Test
void testUpdateStatus_ExistingProxy_ShouldUpdateStatus() {
// 准备数据查询现有代理状态为 OFFLINE
MachineProxy proxy = machineProxyMapper.selectById(existingProxyId);
assertEquals(MachineProxyStatus.OFFLINE.getCode(), proxy.getStatusCode());
// 执行状态更新为 ONLINE
validProxyDTO.setId(existingProxyId);
validProxyDTO.setStatus(MachineProxyStatus.ONLINE.getMessage());
boolean result = machineProxyService.updateStatus(validProxyDTO);
// 验证结果
assertTrue(result);
proxy = machineProxyMapper.selectById(existingProxyId);
assertEquals(MachineProxyStatus.ONLINE.getCode(), proxy.getStatusCode());
}
@Test
void testUpdateStatus_NullDto_ShouldThrowException() {
assertThrows(ServiceException.class, () -> {
machineProxyService.updateStatus(null);
}, "MachineProxyDTO对象为空");
}
// ============================== 心跳测试 ==============================
@Test
void testHeartbeat_ValidData_ShouldUpdateVersionAndStatus() {
// 准备数据现有代理状态为 OFFLINE版本为 1.0.0
MachineProxy proxy = machineProxyMapper.selectById(existingProxyId);
assertEquals("1.0.0", proxy.getVersion());
assertEquals(MachineProxyStatus.OFFLINE.getCode(), proxy.getStatusCode());
// 发送心跳更新版本和状态
validProxyDTO.setId(existingProxyId);
validProxyDTO.setVersion("2.0.0");
validProxyDTO.setStatus(MachineProxyStatus.ONLINE.getMessage());
machineProxyService.heartbeat(validProxyDTO);
// 验证结果
proxy = machineProxyMapper.selectById(existingProxyId);
assertEquals("2.0.0", proxy.getVersion());
assertEquals(MachineProxyStatus.ONLINE.getCode(), proxy.getStatusCode());
}
// ============================== 状态统计测试 ==============================
@Test
void testGetStatusStatistics_ShouldReturnValidCounts() {
// 假设测试数据中有 OFFLINE(1)INSTALLING(1)ONLINE(1) 三种状态
Map<String, Long> stats = machineProxyService.getStatusStatistics();
// 验证统计结果
assertEquals(3, stats.size());
assertTrue(stats.containsKey(MachineProxyStatus.OFFLINE.getMessage()));
assertTrue(stats.containsKey(MachineProxyStatus.INSTALLING.getMessage()));
assertTrue(stats.containsKey(MachineProxyStatus.ONLINE.getMessage()));
assertEquals(1L, stats.get(MachineProxyStatus.OFFLINE.getMessage()));
}
// ============================== 更新配置测试 ==============================
@Test
void testUpdateConfig_ValidConfig_ShouldSucceed() {
// 准备数据现有代理配置为空
MachineProxy proxy = machineProxyMapper.selectById(existingProxyId);
assertNull(proxy.getConfig());
// 更新配置
validProxyDTO.setId(existingProxyId);
validProxyDTO.setConfig("{\"port\": 8080}");
boolean result = machineProxyService.updateConfig(validProxyDTO);
// 验证结果
assertTrue(result);
proxy = machineProxyMapper.selectById(existingProxyId);
assertEquals("{\"port\": 8080}", proxy.getConfig());
}
// ============================== 删除代理测试 ==============================
@Test
void testDelete_OfflineProxy_ShouldDeleteSuccessfully() {
// 准备数据状态为 OFFLINE 的代理 ID
List<Long> ids = Collections.singletonList(existingProxyId);
machineProxyService.delete(ids);
// 验证删除
MachineProxy proxy = machineProxyMapper.selectById(existingProxyId);
assertNull(proxy);
}
@Test
void testDelete_OnlineProxy_ShouldThrowException() {
// 先将代理状态改为 ONLINE
MachineProxy onlineProxy = new MachineProxy();
onlineProxy.setId(existingProxyId);
onlineProxy.setStatusCode(MachineProxyStatus.ONLINE.getCode());
machineProxyMapper.updateById(onlineProxy);
// 执行删除
List<Long> ids = Collections.singletonList(existingProxyId);
assertThrows(IllegalArgumentException.class, () -> {
machineProxyService.delete(ids);
}, "以下代理处于在线状态,无法删除: 1");
}
// ============================== 列表查询测试 ==============================
@Test
void testList_WithStatusFilter_ShouldReturnMatchedRecords() {
// 查询状态为 OFFLINE 的代理
MachineProxyDTO queryDto = new MachineProxyDTO();
queryDto.setStatus(MachineProxyStatus.OFFLINE.getMessage());
PageResult<MachineProxyDTO> pageResult = machineProxyService.list(queryDto);
// 验证结果
assertFalse(pageResult.getList().isEmpty());
pageResult.getList().forEach(dto ->
assertEquals(MachineProxyStatus.OFFLINE.getMessage(), dto.getStatus())
);
}
@Test
void testList_WithHostIpFilter_ShouldReturnMatchedRecords() {
// 假设测试数据中存在 host_ip "192.168.1.1" 的代理
MachineProxyDTO queryDto = new MachineProxyDTO();
queryDto.setHostIp("192.168.1.1");
PageResult<MachineProxyDTO> pageResult = machineProxyService.list(queryDto);
// 验证结果
assertFalse(pageResult.getList().isEmpty());
pageResult.getList().forEach(dto ->
assertEquals("192.168.1.1", dto.getHostIp())
);
}
// ============================== 辅助方法测试 ==============================
@Test
void testEnumUtils_ConvertCodeToMessage() {
// 验证代理类型枚举转换
String typeMessage = EnumUtils.getEnumByCode(MachineProxyType.HTTP.getCode(), MachineProxyType.class).getMessage();
assertEquals("HTTP", typeMessage);
// 验证状态枚举转换
String statusMessage = EnumUtils.getEnumByCode(MachineProxyStatus.INSTALLING.getCode(), MachineProxyStatus.class).getMessage();
assertEquals("安装中", statusMessage);
}
}

View File

@ -0,0 +1,177 @@
package com.casic.machine.service.impl;
import com.casic.commons.exception.ServiceException;
import com.casic.machine.dto.MachineInfoDto;
import com.casic.machine.entity.MachineInfo;
import com.casic.machine.enums.AuthenticationType;
import com.casic.machine.enums.ConnectionStatus;
import com.casic.machine.enums.MachineInfoStatus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
@Rollback(true)
@Sql(scripts = {"classpath:sql/machine_info_test_data.sql"})
public class MachineinfoServiceImplTest {
@Autowired
private MachineinfoServiceImpl machineInfoService;
private MachineInfoDto validMachineInfoDto;
private Long existingMachineId;
private Long nonExistingMachineId;
@BeforeEach
public void setUp() {
// 初始化测试数据假设 SQL 脚本已插入一条状态为 ENABLE 的机器
existingMachineId = 1L;
nonExistingMachineId = 999L;
// 有效机器信息 DTO
validMachineInfoDto = new MachineInfoDto();
validMachineInfoDto.setName("Test Machine");
validMachineInfoDto.setHostIp("192.168.1.101");
validMachineInfoDto.setSshPort("22");
validMachineInfoDto.setUsername("testuser");
validMachineInfoDto.setAuthenticationType(AuthenticationType.PASSWORD.getMessage());
validMachineInfoDto.setStatus(MachineInfoStatus.ENABLE.getMessage());
}
// ======================= 新增机器测试 =======================
@Test
void testAddMachineInfo_ValidData_ShouldSucceed() {
boolean result = machineInfoService.addMachineInfo(validMachineInfoDto);
assertTrue(result);
// 验证数据库存在记录
MachineInfo machineInfo = machineInfoService.getById(validMachineInfoDto.getId());
assertNotNull(machineInfo);
assertEquals(validMachineInfoDto.getHostIp(), machineInfo.getHostIp());
assertEquals(AuthenticationType.PASSWORD.getCode(), machineInfo.getAuthenticationTypeCode());
}
@Test
void testAddMachineInfo_NullDto_ShouldThrowException() {
assertThrows(ServiceException.class, () -> {
machineInfoService.addMachineInfo(null);
}, "机器信息为空");
}
// ======================= 列表查询测试 =======================
@Test
void testListMachineInfo_WithStatusFilter_ShouldReturnValidRecords() {
MachineInfoDto queryDto = new MachineInfoDto();
queryDto.setStatus(MachineInfoStatus.ENABLE.getMessage());
var pageResult = machineInfoService.listMachineInfo(queryDto);
assertFalse(pageResult.getList().isEmpty());
pageResult.getList().forEach(dto ->
assertEquals(MachineInfoStatus.ENABLE.getMessage(), dto.getStatus())
);
}
@Test
void testListMachineInfo_WithHostIpFilter_ShouldFilterRecords() {
MachineInfoDto queryDto = new MachineInfoDto();
queryDto.setHostIp("192.168.1.100"); // 假设测试数据中的 IP
var pageResult = machineInfoService.listMachineInfo(queryDto);
assertFalse(pageResult.getList().isEmpty());
pageResult.getList().forEach(dto ->
assertTrue(dto.getHostIp().contains("192.168.1.100"))
);
}
// ======================= 更新机器状态测试 =======================
@Test
void testUpdateStatus_ValidId_ShouldUpdateStatus() {
boolean result = machineInfoService.updateStatus(existingMachineId, MachineInfoStatus.UN_ENABLE.getMessage());
assertTrue(result);
MachineInfo machineInfo = machineInfoService.getById(existingMachineId);
assertEquals(MachineInfoStatus.UN_ENABLE.getCode(), machineInfo.getStatus());
}
@Test
void testUpdateStatus_NonExistingId_ShouldFail() {
boolean result = machineInfoService.updateStatus(nonExistingMachineId, MachineInfoStatus.UN_ENABLE.getMessage());
assertFalse(result);
}
// ======================= 连接测试 =======================
@Test
void testTestConnection_EnabledMachine_ShouldSucceed() {
MachineInfo machineInfo = machineInfoService.getById(existingMachineId);
assertTrue(machineInfoService.testConnection(machineInfo));
}
@Test
void testTestConnection_DisabledMachine_ShouldThrowException() {
// 先禁用机器
machineInfoService.updateStatus(existingMachineId, MachineInfoStatus.UN_ENABLE.getMessage());
MachineInfo disabledMachine = machineInfoService.getById(existingMachineId);
assertThrows(RuntimeException.class, () -> {
machineInfoService.testConnection(disabledMachine);
}, "机器不可用");
}
// ======================= 会话管理测试 =======================
@Test
void testConnect_NewMachine_ShouldCreateSession() {
MachineInfo machineInfo = machineInfoService.getById(existingMachineId);
String sessionId = machineInfoService.connect(machineInfo);
assertNotNull(sessionId);
assertTrue(sessionId.startsWith("session-"));
assertEquals(ConnectionStatus.CONNECTING, machineInfoService.getConnectionStatus(machineInfo.getName()));
}
// ======================= 删除机器测试 =======================
@Test
void testDeleteMachineInfo_EnabledMachine_ShouldDelete() {
machineInfoService.deleteMachineInfo(existingMachineId);
assertNull(machineInfoService.getById(existingMachineId));
}
@Test
void testDeleteList_ValidIds_ShouldDeleteBatch() {
machineInfoService.deleteList(Collections.singletonList(existingMachineId));
var list = machineInfoService.list();
assertFalse(list.contains(existingMachineId));
}
// ======================= 辅助功能测试 =======================
@Test
void testAuthenticationTypeConversion() {
validMachineInfoDto.setAuthenticationType(AuthenticationType.SECRET_KEY.getMessage());
MachineInfo machineInfo = new MachineInfo();
BeanUtils.copyProperties(validMachineInfoDto, machineInfo);
assertEquals(AuthenticationType.SECRET_KEY.getCode(), machineInfo.getAuthenticationTypeCode());
}
@Test
void testGetAllConnectionStatus_ShouldReturnStatusMap() {
MachineInfo machineInfo = machineInfoService.getById(existingMachineId);
machineInfoService.connect(machineInfo);
var statusMap = machineInfoService.getAllConnectionStatus();
assertFalse(statusMap.isEmpty());
assertTrue(statusMap.containsValue(ConnectionStatus.CONNECTING));
}
}

View File

@ -0,0 +1,218 @@
package com.casic.machine.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.casic.commons.exception.ServiceException;
import com.casic.commons.utils.AliOssUtil;
import com.casic.machine.entity.MachineInfo;
import com.casic.machine.entity.SecretKey;
import com.casic.machine.dto.SecretKeyDto;
import com.casic.machine.mapper.SecretServiceMapper;
import com.casic.machine.service.MachineInfoService;
import com.jayway.jsonpath.internal.Utils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.BeanUtils;
import org.springframework.core.io.InputStreamResource;
import org.springframework.http.ResponseEntity;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.util.ReflectionTestUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;
@SuppressWarnings("unchecked")
public class SecretKeyServiceImplTest {
@InjectMocks
private SecretKeyServiceImpl secretKeyService;
@Mock
private AliOssUtil aliOssUtil;
@Mock
private SecretServiceMapper secretServiceMapper;
@Mock
private MachineInfoService machineInfoService;
private SecretKeyDto validSecretKeyDto;
private MockMultipartFile mockFile;
private final Long TEST_SECRET_KEY_ID = 1L;
private final String TEST_FILE_NAME = "test_key.pem";
private final String TEST_PATH = "https://bucket.endpoint/test_key.pem";
@BeforeEach
public void setUp() throws IOException {
MockitoAnnotations.openMocks(this);
validSecretKeyDto = new SecretKeyDto();
validSecretKeyDto.setName("Test Key");
validSecretKeyDto.setDescription("Test secret key");
// 创建模拟文件
byte[] fileContent = "test content".getBytes();
mockFile = new MockMultipartFile(
"file",
TEST_FILE_NAME,
"application/octet-stream",
new ByteArrayInputStream(fileContent)
);
// 模拟 OSS 工具返回值
when(aliOssUtil.save(any(MockMultipartFile.class))).thenReturn(TEST_FILE_NAME);
}
// ======================= 新增密钥测试 =======================
@Test
void testAddSecretKey_ValidData_ShouldSucceed() throws IOException {
// 执行新增
boolean result = secretKeyService.addSecretKey(validSecretKeyDto, mockFile);
// 验证 OSS 保存调用
verify(aliOssUtil, times(1)).save(mockFile);
// 验证实体属性
SecretKey savedKey = new SecretKey();
BeanUtils.copyProperties(validSecretKeyDto, savedKey);
savedKey.setFileName(TEST_FILE_NAME);
savedKey.setPath(TEST_PATH);
// 验证 Mapper 调用
verify(secretServiceMapper, times(1)).insert(savedKey);
assertTrue(result);
}
@Test
void testAddSecretKey_NullFile_ShouldThrowException() {
assertThrows(ServiceException.class, () -> {
secretKeyService.addSecretKey(validSecretKeyDto, null);
}, "文件为空");
}
// ======================= 绑定机器测试 =======================
@Test
void testBindingMachine_ValidIds_ShouldUpdateMachine() {
// 模拟机器列表
List<Long> machineIds = Collections.singletonList(1L);
when(machineInfoService.listByIds(machineIds)).thenReturn(Collections.singletonList(new MachineInfo()));
secretKeyService.bindingMachine(TEST_SECRET_KEY_ID, machineIds);
// 验证机器信息更新
verify(machineInfoService, times(1)).listByIds(machineIds);
machineIds.forEach(id -> {
verify(machineInfoService, times(1)).update(any());
});
}
// ======================= 更新密钥测试 =======================
@Test
void testUpdateSecretKey_WithNewFile_ShouldUpdatePath() throws IOException {
MockMultipartFile newFile = new MockMultipartFile(
"file",
"new_key.pem",
"application/octet-stream",
new ByteArrayInputStream("new content".getBytes())
);
when(aliOssUtil.save(newFile)).thenReturn("new_key.pem");
validSecretKeyDto.setId(TEST_SECRET_KEY_ID);
boolean result = secretKeyService.updateSecretKey(validSecretKeyDto, newFile);
// 验证 OSS 调用和路径更新
verify(aliOssUtil, times(1)).save(newFile);
SecretKey updatedKey = new SecretKey();
BeanUtils.copyProperties(validSecretKeyDto, updatedKey);
updatedKey.setFileName("new_key.pem");
updatedKey.setPath("https://bucket.endpoint/new_key.pem");
verify(secretServiceMapper, times(1)).updateById(updatedKey);
assertTrue(result);
}
// ======================= 删除密钥测试 =======================
@Test
void testDeleteSecretKey_ValidId_ShouldDeleteFileAndRecord() {
SecretKey secretKey = new SecretKey();
secretKey.setId(TEST_SECRET_KEY_ID);
secretKey.setFileName(TEST_FILE_NAME);
when(secretServiceMapper.selectById(TEST_SECRET_KEY_ID)).thenReturn(secretKey);
boolean result = secretKeyService.deleteSecretKey(TEST_SECRET_KEY_ID);
// 验证 OSS 删除和 Mapper 调用
verify(aliOssUtil, times(1)).deleteFile(TEST_FILE_NAME);
verify(secretServiceMapper, times(1)).deleteById(TEST_SECRET_KEY_ID);
assertTrue(result);
}
// ======================= 列表查询测试 =======================
@Test
void testListSecretKey_WithNameFilter_ShouldReturnMatchedRecords() {
SecretKeyDto queryDto = new SecretKeyDto();
queryDto.setName("Test");
QueryWrapper<SecretKey> wrapper = new QueryWrapper<>();
wrapper.like("name", "Test");
when(secretServiceMapper.selectPage(any(), any())).thenReturn(new Page<>());
secretKeyService.listSecretKey(queryDto);
verify(secretServiceMapper, times(1)).selectPage(any(), wrapper);
}
// ======================= 文件下载测试 =======================
@Test
void testDownloadSecretKeyFile_ValidId_ShouldReturnResponseEntity() throws IOException {
SecretKey secretKey = new SecretKey();
secretKey.setFileName(TEST_FILE_NAME);
when(secretServiceMapper.selectById(TEST_SECRET_KEY_ID)).thenReturn(secretKey);
InputStreamResource inputStreamResource = new InputStreamResource(
new ByteArrayInputStream("test content".getBytes())
);
when(aliOssUtil.downloadFile(TEST_FILE_NAME)).thenReturn(
ResponseEntity.ok(inputStreamResource)
);
ResponseEntity<InputStreamResource> response = secretKeyService.downloadSecretKeyFile(TEST_SECRET_KEY_ID);
assertNotNull(response);
assertEquals("test content", Utils.toString(response.getBody().getInputStream()));
}
// ======================= 批量删除测试 =======================
@Test
void testDeleteList_ValidIds_ShouldSubmitAsyncDelete() {
List<Long> ids = Collections.singletonList(TEST_SECRET_KEY_ID);
SecretKey secretKey = new SecretKey();
secretKey.setFileName(TEST_FILE_NAME);
when(secretServiceMapper.selectBatchIds(ids)).thenReturn(Collections.singletonList(secretKey));
secretKeyService.deleteList(ids);
// 验证异步任务提交
verify(secretKeyService.FILE_DELETE_EXECUTOR, times(1)).execute(any(Runnable.class));
verify(secretServiceMapper, times(1)).deleteBatchIds(ids);
}
// ======================= 异常处理测试 =======================
@Test
void testDeleteSecretKey_FileDeleteFailed_ShouldThrowException() {
aliOssUtil.deleteFile(TEST_FILE_NAME);
SecretKey secretKey = new SecretKey();
secretKey.setId(TEST_SECRET_KEY_ID);
secretKey.setFileName(TEST_FILE_NAME);
when(secretServiceMapper.selectById(TEST_SECRET_KEY_ID)).thenReturn(secretKey);
assertThrows(ServiceException.class, () -> {
secretKeyService.deleteSecretKey(TEST_SECRET_KEY_ID);
}, "删除文件失败");
}
}