소스 검색

feat:1.初始化

wangkun 6 일 전
커밋
0c21b85a87
40개의 변경된 파일과 3003개의 추가작업 그리고 0개의 파일을 삭제
  1. 3 0
      .mvn/wrapper/maven-wrapper.properties
  2. 295 0
      mvnw
  3. 189 0
      mvnw.cmd
  4. 128 0
      pom.xml
  5. 15 0
      src/main/java/com/ymwk/supervisionserver/SupervisionServerApplication.java
  6. 14 0
      src/main/java/com/ymwk/supervisionserver/code/IErrorCode.java
  7. 29 0
      src/main/java/com/ymwk/supervisionserver/component/EndpointValidator.java
  8. 53 0
      src/main/java/com/ymwk/supervisionserver/component/PageParam.java
  9. 35 0
      src/main/java/com/ymwk/supervisionserver/component/PageResult.java
  10. 103 0
      src/main/java/com/ymwk/supervisionserver/component/ResponseDTO.java
  11. 18 0
      src/main/java/com/ymwk/supervisionserver/component/RestTemplateConfig.java
  12. 89 0
      src/main/java/com/ymwk/supervisionserver/component/SmartBeanUtil.java
  13. 128 0
      src/main/java/com/ymwk/supervisionserver/component/SmartPageUtil.java
  14. 320 0
      src/main/java/com/ymwk/supervisionserver/component/SmartStringUtil.java
  15. 18 0
      src/main/java/com/ymwk/supervisionserver/config/MybatisPlusConfig.java
  16. 63 0
      src/main/java/com/ymwk/supervisionserver/controller/ApiController.java
  17. 42 0
      src/main/java/com/ymwk/supervisionserver/controller/RegulatedController.java
  18. 598 0
      src/main/java/com/ymwk/supervisionserver/controller/RemoteController.java
  19. 77 0
      src/main/java/com/ymwk/supervisionserver/entity/Api.java
  20. 32 0
      src/main/java/com/ymwk/supervisionserver/entity/Regulated.java
  21. 15 0
      src/main/java/com/ymwk/supervisionserver/mapper/ApiMapper.java
  22. 31 0
      src/main/java/com/ymwk/supervisionserver/mapper/RegulatedMapper.java
  23. 27 0
      src/main/java/com/ymwk/supervisionserver/model/ApiAddRequest.java
  24. 15 0
      src/main/java/com/ymwk/supervisionserver/model/ApiListRequest.java
  25. 32 0
      src/main/java/com/ymwk/supervisionserver/model/AppApiItem.java
  26. 16 0
      src/main/java/com/ymwk/supervisionserver/model/AppApiTestRequest.java
  27. 16 0
      src/main/java/com/ymwk/supervisionserver/model/AppApiToggleRequest.java
  28. 27 0
      src/main/java/com/ymwk/supervisionserver/model/ComputerInfo.java
  29. 25 0
      src/main/java/com/ymwk/supervisionserver/model/CryptoStatInfo.java
  30. 14 0
      src/main/java/com/ymwk/supervisionserver/model/CryptoStatItem.java
  31. 9 0
      src/main/java/com/ymwk/supervisionserver/model/DynamicRequest.java
  32. 17 0
      src/main/java/com/ymwk/supervisionserver/model/RegulatedAddRequest.java
  33. 13 0
      src/main/java/com/ymwk/supervisionserver/model/RegulatedListRequest.java
  34. 55 0
      src/main/java/com/ymwk/supervisionserver/model/SystemInfo.java
  35. 33 0
      src/main/java/com/ymwk/supervisionserver/service/ApiService.java
  36. 34 0
      src/main/java/com/ymwk/supervisionserver/service/RegulatedService.java
  37. 210 0
      src/main/java/com/ymwk/supervisionserver/service/impl/ApiServiceImpl.java
  38. 111 0
      src/main/java/com/ymwk/supervisionserver/service/impl/RegulatedServiceImpl.java
  39. 41 0
      src/main/resources/application.yaml
  40. 13 0
      src/test/java/com/ymwk/supervisionserver/SupervisionServerApplicationTests.java

+ 3 - 0
.mvn/wrapper/maven-wrapper.properties

@@ -0,0 +1,3 @@
+wrapperVersion=3.3.4
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip

+ 295 - 0
mvnw

@@ -0,0 +1,295 @@
+#!/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.4
+#
+# 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:]'
+}
+
+scriptDir="$(dirname "$0")"
+scriptName="$(basename "$0")"
+
+# 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 <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.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${scriptName#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
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+actualDistributionDir=""
+
+# First try the expected directory name (for regular distributions)
+if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
+  if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
+    actualDistributionDir="$distributionUrlNameMain"
+  fi
+fi
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if [ -z "$actualDistributionDir" ]; then
+  # enable globbing to iterate over items
+  set +f
+  for dir in "$TMP_DOWNLOAD_DIR"/*; do
+    if [ -d "$dir" ]; then
+      if [ -f "$dir/bin/$MVN_CMD" ]; then
+        actualDistributionDir="$(basename "$dir")"
+        break
+      fi
+    fi
+  done
+  set -f
+fi
+
+if [ -z "$actualDistributionDir" ]; then
+  verbose "Contents of $TMP_DOWNLOAD_DIR:"
+  verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
+  die "Could not find Maven distribution directory in extracted archive"
+fi
+
+verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"

+ 189 - 0
mvnw.cmd

@@ -0,0 +1,189 @@
+<# : 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.4
+@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 -eq $False) { "/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_M2_PATH = "$HOME/.m2"
+if ($env:MAVEN_USER_HOME) {
+  $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
+}
+
+if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
+    New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
+}
+
+$MAVEN_WRAPPER_DISTS = $null
+if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
+  $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
+} else {
+  $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
+}
+
+$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::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
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+$actualDistributionDir = ""
+
+# First try the expected directory name (for regular distributions)
+$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
+$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
+if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
+  $actualDistributionDir = $distributionUrlNameMain
+}
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if (!$actualDistributionDir) {
+  Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
+    $testPath = Join-Path $_.FullName "bin/$MVN_CMD"
+    if (Test-Path -Path $testPath -PathType Leaf) {
+      $actualDistributionDir = $_.Name
+    }
+  }
+}
+
+if (!$actualDistributionDir) {
+  Write-Error "Could not find Maven distribution directory in extracted archive"
+}
+
+Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -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"

+ 128 - 0
pom.xml

@@ -0,0 +1,128 @@
+<?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>4.1.1</version>
+        <relativePath/> <!-- lookup parent from repository -->
+    </parent>
+    <groupId>com.ymwk</groupId>
+    <artifactId>supervisionServer</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <name>supervisionServer</name>
+    <description>supervisionServer</description>
+    <url/>
+    <licenses>
+        <license/>
+    </licenses>
+    <developers>
+        <developer/>
+    </developers>
+    <scm>
+        <connection/>
+        <developerConnection/>
+        <tag/>
+        <url/>
+    </scm>
+    <properties>
+        <java.version>21</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-validation</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jdbc</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.validation</groupId>
+            <artifactId>validation-api</artifactId>
+            <version>2.0.1.Final</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.swagger.core.v3</groupId>
+            <artifactId>swagger-annotations</artifactId>
+            <version>2.2.54</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
+            <version>3.5.17</version>
+        </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-jsqlparser</artifactId>
+            <version>3.5.17</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-collections4</artifactId>
+            <version>4.6.0</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>5.8.47</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>31.1-jre</version> <!-- 或更新的版本 -->
+        </dependency>
+
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-devtools</artifactId>
+            <scope>runtime</scope>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 15 - 0
src/main/java/com/ymwk/supervisionserver/SupervisionServerApplication.java

@@ -0,0 +1,15 @@
+package com.ymwk.supervisionserver;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.mybatis.spring.annotation.MapperScan;
+
+@SpringBootApplication
+@MapperScan("com.ymwk.supervisionserver.mapper")
+public class SupervisionServerApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(SupervisionServerApplication.class, args);
+    }
+
+}

+ 14 - 0
src/main/java/com/ymwk/supervisionserver/code/IErrorCode.java

@@ -0,0 +1,14 @@
+package com.ymwk.supervisionserver.code;
+
+/**
+ * @author autwindx
+ */
+public interface IErrorCode {
+    int getCode();
+
+    /**
+     * 错误消息
+     *
+     */
+    String getMsg();
+}

+ 29 - 0
src/main/java/com/ymwk/supervisionserver/component/EndpointValidator.java

@@ -0,0 +1,29 @@
+package com.ymwk.supervisionserver.component;
+
+import java.net.URI;
+
+/**
+ * 采集接口地址校验
+ */
+public class EndpointValidator {
+
+    private EndpointValidator() {
+    }
+
+    /**
+     * 校验地址必须是完整的 http/https 地址(如 http://10.10.10.88:8800/api/xxx)
+     */
+    public static boolean isValid(String endpoint) {
+        if (endpoint == null || endpoint.isBlank()) {
+            return false;
+        }
+        try {
+            URI uri = URI.create(endpoint.trim());
+            String scheme = uri.getScheme();
+            return uri.isAbsolute()
+                    && ("http".equalsIgnoreCase(scheme) || "https".equalsIgnoreCase(scheme));
+        } catch (IllegalArgumentException e) {
+            return false;
+        }
+    }
+}

+ 53 - 0
src/main/java/com/ymwk/supervisionserver/component/PageParam.java

@@ -0,0 +1,53 @@
+package com.ymwk.supervisionserver.component;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.hibernate.validator.constraints.Length;
+
+import javax.validation.Valid;
+import javax.validation.constraints.Max;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+import java.util.List;
+
+/**
+ * @author autwindx
+ */
+@Data
+public class PageParam {
+
+    @Schema(description = "页码(不能为空)", example = "1")
+    @NotNull(message = "分页参数不能为空")
+    private Long pageNum;
+
+    @Schema(description = "每页数量(不能为空)", example = "10")
+    @NotNull(message = "每页数量不能为空")
+    @Max(value = 500, message = "每页最大为500")
+    private Long pageSize;
+
+    @Schema(description = "是否查询总条数")
+    protected Boolean searchCount;
+
+    @Schema(description = "排序字段集合")
+    @Size(max = 10, message = "排序字段最多10")
+    @Valid
+    private List<SortItem> sortItemList;
+
+    /**
+     * 排序DTO类
+     */
+    @Data
+    public static class SortItem {
+
+        @Schema(description = "true正序|false倒序")
+        @NotNull(message = "排序规则不能为空")
+        private Boolean isAsc;
+
+        @Schema(description = "排序字段")
+        @NotBlank(message = "排序字段不能为空")
+        @Length(max = 30, message = "排序字段最多30")
+        private String column;
+    }
+}

+ 35 - 0
src/main/java/com/ymwk/supervisionserver/component/PageResult.java

@@ -0,0 +1,35 @@
+package com.ymwk.supervisionserver.component;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @author autwindx
+ */
+@Data
+public class PageResult<T> {
+    /**
+     * 当前页
+     */
+    private Long pageNum;
+    /**
+     * 每页的数量
+     */
+    private Long pageSize;
+    /**
+     * 总记录数
+     */
+    private Long total;
+    /**
+     * 总页数
+     */
+    private Long pages;
+    /**
+     * 结果集
+     */
+    private List<T> list;
+
+    private Boolean emptyFlag;
+
+}

+ 103 - 0
src/main/java/com/ymwk/supervisionserver/component/ResponseDTO.java

@@ -0,0 +1,103 @@
+package com.ymwk.supervisionserver.component;
+
+import com.baomidou.mybatisplus.core.toolkit.StringUtils;
+import com.ymwk.supervisionserver.code.IErrorCode;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * @author autwindx
+ */
+@Data
+@Schema
+public class ResponseDTO<T> {
+    public static final int OK_CODE = 100000;
+    public static final int ERROR_CODE = 100001;
+
+    public static final String OK_MSG = "操作成功";
+
+    @Schema(description = "返回码")
+    private Integer code;
+
+    private String message;
+
+    @Schema(description = "返回数据")
+    private T data;
+
+
+    public ResponseDTO(Integer code, String message, T data) {
+        this.code = code;
+        this.message = message;
+        this.data = data;
+    }
+
+    public ResponseDTO(IErrorCode IErrorCode, String msg, T data) {
+        this.code = IErrorCode.getCode();
+        if (StringUtils.isNotBlank(msg)) {
+            this.message = msg;
+        } else {
+            this.message = IErrorCode.getMsg();
+        }
+        this.data = data;
+    }
+
+    public ResponseDTO(IErrorCode IErrorCode, boolean ok, String msg, T data) {
+        this.code = IErrorCode.getCode();
+
+        if (StringUtils.isNotBlank(msg)) {
+            this.message = msg;
+        } else {
+            this.message = IErrorCode.getMsg();
+        }
+        this.data = data;
+    }
+
+    public static <T> ResponseDTO<T> ok() {
+        return new ResponseDTO<>(OK_CODE, OK_MSG, null);
+    }
+
+    public static <T> ResponseDTO<T> ok(T data) {
+        return new ResponseDTO<>(OK_CODE, OK_MSG, data);
+    }
+
+    public static <T> ResponseDTO<T> ok(T data, String msg) {
+        return new ResponseDTO<>(OK_CODE, msg, data);
+    }
+
+    public static <T> ResponseDTO<T> okMsg(String msg) {
+        return new ResponseDTO<>(OK_CODE, msg, null);
+    }
+
+
+
+    // -------------------------------------------- 错误码 --------------------------------------------
+
+    public static <T> ResponseDTO<T> error(IErrorCode IErrorCode) {
+        return new ResponseDTO<>(IErrorCode, null, null);
+    }
+
+    public static <T> ResponseDTO<T> error(IErrorCode IErrorCode, boolean ok) {
+        return new ResponseDTO<>(IErrorCode, null, null);
+    }
+
+    public static <T>  ResponseDTO<T> error(ResponseDTO<?> responseDTO) {
+        return new ResponseDTO<>(responseDTO.getCode(), responseDTO.getMessage(), null);
+    }
+
+    public static <T> ResponseDTO<T> error(IErrorCode IErrorCode, String msg) {
+        return new ResponseDTO<>(IErrorCode, msg, null);
+    }
+
+    public static <T> ResponseDTO<T> errorData(IErrorCode IErrorCode, T data) {
+        return new ResponseDTO<>(IErrorCode, null, data);
+    }
+
+    public static <T> ResponseDTO<T> error(int i, String string) {
+        return new ResponseDTO<>(i, string, null);
+    }
+
+    public static <T> ResponseDTO<T> error(String msg, T data) {
+        return new ResponseDTO<>(ERROR_CODE, msg, data);
+    }
+
+}

+ 18 - 0
src/main/java/com/ymwk/supervisionserver/component/RestTemplateConfig.java

@@ -0,0 +1,18 @@
+package com.ymwk.supervisionserver.component;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestTemplateConfig {
+
+    @Bean
+    public RestTemplate restTemplate() {
+        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+        factory.setConnectTimeout(3000); // 连接超时 3s
+        factory.setReadTimeout(5000);    // 读取超时 5s
+        return new RestTemplate(factory);
+    }
+}

+ 89 - 0
src/main/java/com/ymwk/supervisionserver/component/SmartBeanUtil.java

@@ -0,0 +1,89 @@
+package com.ymwk.supervisionserver.component;
+
+import org.springframework.beans.BeanUtils;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.Validation;
+import javax.validation.Validator;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author autwindx
+ */
+public class SmartBeanUtil {
+
+    /**
+     * 验证器
+     */
+    private static final Validator VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
+
+    /**
+     * 复制bean的属性
+     *
+     * @param source 源 要复制的对象
+     * @param target 目标 复制到此对象
+     */
+    public static void copyProperties(Object source, Object target) {
+        BeanUtils.copyProperties(source, target);
+    }
+
+    /**
+     * 复制对象
+     *
+     * @param source 源 要复制的对象
+     * @param target 目标 复制到此对象
+     * @param <T>
+     * @return
+     */
+    public static <T> T copy(Object source, Class<T> target) {
+        if (source == null || target == null) {
+            return null;
+        }
+        try {
+            T newInstance = target.newInstance();
+            BeanUtils.copyProperties(source, newInstance);
+            return newInstance;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    /**
+     * 复制list
+     *
+     * @param source
+     * @param target
+     * @param <T>
+     * @param <K>
+     * @return
+     */
+    public static <T, K> List<K> copyList(List<T> source, Class<K> target) {
+        if (null == source || source.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return source.stream().map(e -> copy(e, target)).collect(Collectors.toList());
+    }
+
+    /**
+     * 手动验证对象 Model的属性
+     * 需要配合 hibernate-validator 校验注解
+     *
+     * @param t
+     * @return String 返回null代表验证通过,否则返回错误的信息
+     */
+    public static <T> String verify(T t) {
+        // 获取验证结果
+        Set<ConstraintViolation<T>> validate = VALIDATOR.validate(t);
+        if (validate.isEmpty()) {
+            // 验证通过
+            return null;
+        }
+        // 返回错误信息
+        List<String> messageList = validate.stream().map(ConstraintViolation::getMessage).collect(Collectors.toList());
+        return messageList.toString();
+    }
+}
+

+ 128 - 0
src/main/java/com/ymwk/supervisionserver/component/SmartPageUtil.java

@@ -0,0 +1,128 @@
+package com.ymwk.supervisionserver.component;
+
+
+import com.baomidou.mybatisplus.core.metadata.OrderItem;
+import com.baomidou.mybatisplus.core.toolkit.sql.SqlInjectionUtils;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.google.common.collect.Lists;
+import com.ymwk.supervisionserver.component.PageParam;
+import com.ymwk.supervisionserver.component.PageResult;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections4.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author autwindx
+ */
+@Slf4j
+public class SmartPageUtil {
+
+    /**
+     * 转换为查询参数
+     */
+    public static Page<?> convert2PageQuery(PageParam pageParam) {
+        Page<?> page = new Page<>(pageParam.getPageNum(), pageParam.getPageSize());
+
+        List<PageParam.SortItem> sortItemList = pageParam.getSortItemList();
+        if (CollectionUtils.isEmpty(sortItemList)) {
+            return page;
+        }
+
+        // 设置排序字段并检测是否含有sql注入
+        List<OrderItem> orderItemList = new ArrayList<>();
+        for (PageParam.SortItem sortItem : sortItemList) {
+
+            if (SmartStringUtil.isEmpty(sortItem.getColumn())) {
+                continue;
+            }
+
+            if (SqlInjectionUtils.check(sortItem.getColumn())) {
+                log.error("《存在SQL注入:》 : {}", sortItem.getColumn());
+                // throw new BusinessException("存在SQL注入风险,请联系技术工作人员!");
+            }
+
+            orderItemList.add(new OrderItem().setColumn(sortItem.getColumn()).setAsc(sortItem.getIsAsc()));
+        }
+        page.setOrders(orderItemList);
+        return page;
+    }
+
+    /**
+     * 转换为 PageResult 对象
+     */
+    public static <T, E> PageResult<T> convert2PageResult(Page<?> page, List<E> sourceList, Class<T> targetClazz) {
+        return convert2PageResult(page, SmartBeanUtil.copyList(sourceList, targetClazz));
+    }
+
+    /**
+     * 转换为 PageResult 对象
+     */
+    public static <E> PageResult<E> convert2PageResult(Page<?> page, List<E> sourceList) {
+        PageResult<E> pageResult = new PageResult<>();
+        pageResult.setPageNum(page.getCurrent());
+        pageResult.setPageSize(page.getSize());
+        pageResult.setTotal(page.getTotal());
+        pageResult.setPages(page.getPages());
+        pageResult.setList(sourceList);
+        pageResult.setEmptyFlag(CollectionUtils.isEmpty(sourceList));
+        return pageResult;
+    }
+
+    /**
+     * 转换分页结果对象
+     */
+    public static <E, T> PageResult<T> convert2PageResult(PageResult<E> pageResult, Class<T> targetClazz) {
+        PageResult<T> newPageResult = new PageResult<>();
+        newPageResult.setPageNum(pageResult.getPageNum());
+        newPageResult.setPageSize(pageResult.getPageSize());
+        newPageResult.setTotal(pageResult.getTotal());
+        newPageResult.setPages(pageResult.getPages());
+        newPageResult.setEmptyFlag(pageResult.getEmptyFlag());
+        newPageResult.setList(SmartBeanUtil.copyList(pageResult.getList(), targetClazz));
+        return newPageResult;
+    }
+
+//    /**
+//     * 转换分页结果对象
+//     * @param page 分页对象
+//     * @param targetClazz 目标类型
+//     * @return PageResult
+//     * @param <E> 分页泛型
+//     * @param <T> PageResult泛型
+//     */
+//    public static <E, T> PageResult<T> cover2PageResult(Page<E> page, Class<T> targetClazz){
+//        PageResult<T> pageResult = new PageResult<>();
+//        pageResult.setPageNum(page.getCurrent());
+//        pageResult.setPageSize(page.getSize());
+//        pageResult.setTotal(page.getTotal());
+//        pageResult.setPages(page.getPages());
+//        pageResult.setEmptyFlag(CollUtil.isEmpty(page.getRecords()));
+//        pageResult.setList(SmartBeanUtil.copyList(page.getRecords(), targetClazz));
+//        return pageResult;
+//    }
+
+    public static <T> PageResult subListPage(Integer pageNum, Integer pageSize, List<T> list) {
+        PageResult<T> pageRet = new PageResult<T>();
+        //总条数
+        int count = list.size();
+        int pages = count % pageSize == 0 ? count / pageSize : (count / pageSize + 1);
+        int fromIndex = (pageNum - 1) * pageSize;
+        int toIndex = Math.min(pageNum * pageSize, count);
+
+        if (pageNum > pages) {
+            pageRet.setList(Lists.newLinkedList());
+            pageRet.setPageNum(pageNum.longValue());
+            pageRet.setPages((long) pages);
+            pageRet.setTotal((long) count);
+            return pageRet;
+        }
+        List<T> pageList = list.subList(fromIndex, toIndex);
+        pageRet.setList(pageList);
+        pageRet.setPageNum(pageNum.longValue());
+        pageRet.setPages((long) pages);
+        pageRet.setTotal((long) count);
+        return pageRet;
+    }
+}

+ 320 - 0
src/main/java/com/ymwk/supervisionserver/component/SmartStringUtil.java

@@ -0,0 +1,320 @@
+package com.ymwk.supervisionserver.component;
+
+import cn.hutool.core.util.StrUtil;
+
+import java.util.*;
+
+/**
+ * @author autwindx
+ */
+public class SmartStringUtil extends StrUtil {
+
+    // ===============split =======================
+
+    public static Set<String> splitConvertToSet(String str, String split) {
+        if (isEmpty(str)) {
+            return new HashSet<String>();
+        }
+        String[] splitArr = str.split(split);
+        HashSet<String> set = new HashSet<String>(splitArr.length);
+        Collections.addAll(set, splitArr);
+        return set;
+    }
+
+    public static List<String> splitConvertToList(String str, String split) {
+        if (isEmpty(str)) {
+            return new ArrayList<String>();
+        }
+        String[] splitArr = str.split(split);
+        ArrayList<String> list = new ArrayList<String>(splitArr.length);
+        list.addAll(Arrays.asList(splitArr));
+        return list;
+    }
+
+    // ===============split Integer=======================
+
+    public static List<Integer> splitConvertToIntList(String str, String split, int defaultVal) {
+        if (isEmpty(str)) {
+            return new ArrayList<Integer>();
+        }
+        String[] strArr = str.split(split);
+        List<Integer> list = new ArrayList<Integer>(strArr.length);
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                int parseInt = Integer.parseInt(strArr[i]);
+                list.add(parseInt);
+            } catch (NumberFormatException e) {
+                list.add(defaultVal);
+                continue;
+            }
+        }
+        return list;
+    }
+
+    public static Set<Integer> splitConvertToIntSet(String str, String split, int defaultVal) {
+        if (isEmpty(str)) {
+            return new HashSet<Integer>();
+        }
+        String[] strArr = str.split(split);
+        HashSet<Integer> set = new HashSet<Integer>(strArr.length);
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                int parseInt = Integer.parseInt(strArr[i]);
+                set.add(parseInt);
+            } catch (NumberFormatException e) {
+                set.add(defaultVal);
+                continue;
+            }
+        }
+        return set;
+    }
+
+    public static Set<Integer> splitConvertToIntSet(String str, String split) {
+        return splitConvertToIntSet(str, split, 0);
+    }
+
+    public static List<Integer> splitConvertToIntList(String str, String split) {
+        return splitConvertToIntList(str, split, 0);
+    }
+
+    public static int[] splitConvertToIntArray(String str, String split, int defaultVal) {
+        if (isEmpty(str)) {
+            return new int[0];
+        }
+        String[] strArr = str.split(split);
+        int[] result = new int[strArr.length];
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                result[i] = Integer.parseInt(strArr[i]);
+            } catch (NumberFormatException e) {
+                result[i] = defaultVal;
+                continue;
+            }
+        }
+        return result;
+    }
+
+    public static int[] splitConvertToIntArray(String str, String split) {
+        return splitConvertToIntArray(str, split, 0);
+    }
+
+    // ===============split 2 Long=======================
+
+    public static List<Long> splitConvertToLongList(String str, String split, long defaultVal) {
+        if (isEmpty(str)) {
+            return new ArrayList<Long>();
+        }
+        String[] strArr = str.split(split);
+        List<Long> list = new ArrayList<Long>(strArr.length);
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                long parseLong = Long.parseLong(strArr[i]);
+                list.add(parseLong);
+            } catch (NumberFormatException e) {
+                list.add(defaultVal);
+                continue;
+            }
+        }
+        return list;
+    }
+
+    public static List<Long> splitConvertToLongList(String str, String split) {
+        return splitConvertToLongList(str, split, 0L);
+    }
+
+    public static long[] splitConvertToLongArray(String str, String split, long defaultVal) {
+        if (isEmpty(str)) {
+            return new long[0];
+        }
+        String[] strArr = str.split(split);
+        long[] result = new long[strArr.length];
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                result[i] = Long.parseLong(strArr[i]);
+            } catch (NumberFormatException e) {
+                result[i] = defaultVal;
+                continue;
+            }
+        }
+        return result;
+    }
+
+    public static long[] splitConvertToLongArray(String str, String split) {
+        return splitConvertToLongArray(str, split, 0L);
+    }
+
+    // ===============split convert byte=======================
+
+    public static List<Byte> splitConvertToByteList(String str, String split, byte defaultVal) {
+        if (isEmpty(str)) {
+            return new ArrayList<Byte>();
+        }
+        String[] strArr = str.split(split);
+        List<Byte> list = new ArrayList<Byte>(strArr.length);
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                byte parseByte = Byte.parseByte(strArr[i]);
+                list.add(parseByte);
+            } catch (NumberFormatException e) {
+                list.add(defaultVal);
+                continue;
+            }
+        }
+        return list;
+    }
+
+    public static List<Byte> splitConvertToByteList(String str, String split) {
+        return splitConvertToByteList(str, split, (byte) 0);
+    }
+
+    public static byte[] splitConvertToByteArray(String str, String split, byte defaultVal) {
+        if (isEmpty(str)) {
+            return new byte[0];
+        }
+        String[] strArr = str.split(split);
+        byte[] result = new byte[strArr.length];
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                result[i] = Byte.parseByte(strArr[i]);
+            } catch (NumberFormatException e) {
+                result[i] = defaultVal;
+                continue;
+            }
+        }
+        return result;
+    }
+
+    public static byte[] splitConvertToByteArray(String str, String split) {
+        return splitConvertToByteArray(str, split, (byte) 0);
+    }
+
+    // ===============split convert double=======================
+
+    public static List<Double> splitConvertToDoubleList(String str, String split, double defaultVal) {
+        if (isEmpty(str)) {
+            return new ArrayList<Double>();
+        }
+        String[] strArr = str.split(split);
+        List<Double> list = new ArrayList<Double>(strArr.length);
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                double parseByte = Double.parseDouble(strArr[i]);
+                list.add(parseByte);
+            } catch (NumberFormatException e) {
+                list.add(defaultVal);
+                continue;
+            }
+        }
+        return list;
+    }
+
+    public static List<Double> splitConvertToDoubleList(String str, String split) {
+        return splitConvertToDoubleList(str, split, 0);
+    }
+
+    public static double[] splitConvertToDoubleArray(String str, String split, double defaultVal) {
+        if (isEmpty(str)) {
+            return new double[0];
+        }
+        String[] strArr = str.split(split);
+        double[] result = new double[strArr.length];
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                result[i] = Double.parseDouble(strArr[i]);
+            } catch (NumberFormatException e) {
+                result[i] = defaultVal;
+                continue;
+            }
+        }
+        return result;
+    }
+
+    public static double[] splitConvertToDoubleArray(String str, String split) {
+        return splitConvertToDoubleArray(str, split, 0);
+    }
+
+    // ===============split convert float=======================
+
+    public static List<Float> splitConvertToFloatList(String str, String split, float defaultVal) {
+        if (isEmpty(str)) {
+            return new ArrayList<Float>();
+        }
+        String[] strArr = str.split(split);
+        List<Float> list = new ArrayList<Float>(strArr.length);
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                float parseByte = Float.parseFloat(strArr[i]);
+                list.add(parseByte);
+            } catch (NumberFormatException e) {
+                list.add(defaultVal);
+                continue;
+            }
+        }
+        return list;
+    }
+
+    public static List<Float> splitConvertToFloatList(String str, String split) {
+        return splitConvertToFloatList(str, split, 0f);
+    }
+
+    public static float[] splitConvertToFloatArray(String str, String split, float defaultVal) {
+        if (isEmpty(str)) {
+            return new float[0];
+        }
+        String[] strArr = str.split(split);
+        float[] result = new float[strArr.length];
+        for (int i = 0; i < strArr.length; i++) {
+            try {
+                result[i] = Float.parseFloat(strArr[i]);
+            } catch (NumberFormatException e) {
+                result[i] = defaultVal;
+                continue;
+            }
+        }
+        return result;
+    }
+
+    public static float[] splitConvertToFloatArray(String str, String split) {
+        return splitConvertToFloatArray(str, split, 0f);
+    }
+
+
+    public static String upperCaseFirstChar(String str) {
+        if (str != null && !str.isEmpty()) {
+            char firstChar = str.charAt(0);
+            if (Character.isUpperCase(firstChar)) {
+                return str;
+            } else {
+                char[] values = str.toCharArray();
+                values[0] = Character.toUpperCase(firstChar);
+                return new String(values);
+            }
+        } else {
+            return str;
+        }
+    }
+
+    public static String replace(String content, int begin, int end, String newStr) {
+        if (begin < content.length() && begin >= 0) {
+            if (end <= content.length() && end >= 0) {
+                if (begin > end) {
+                    return content;
+                } else {
+                    StringBuilder starStr = new StringBuilder();
+
+                    for (int i = begin; i < end; ++i) {
+                        starStr.append(newStr);
+                    }
+
+                    return content.substring(0, begin) + starStr + content.substring(end);
+                }
+            } else {
+                return content;
+            }
+        } else {
+            return content;
+        }
+    }
+
+
+}

+ 18 - 0
src/main/java/com/ymwk/supervisionserver/config/MybatisPlusConfig.java

@@ -0,0 +1,18 @@
+package com.ymwk.supervisionserver.config;
+
+import com.baomidou.mybatisplus.annotation.DbType;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class MybatisPlusConfig {
+
+    @Bean
+    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
+        return interceptor;
+    }
+}

+ 63 - 0
src/main/java/com/ymwk/supervisionserver/controller/ApiController.java

@@ -0,0 +1,63 @@
+package com.ymwk.supervisionserver.controller;
+
+import com.ymwk.supervisionserver.component.PageResult;
+import com.ymwk.supervisionserver.component.ResponseDTO;
+import com.ymwk.supervisionserver.entity.Api;
+import com.ymwk.supervisionserver.model.ApiAddRequest;
+import com.ymwk.supervisionserver.model.ApiListRequest;
+import com.ymwk.supervisionserver.service.ApiService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.server.ResponseStatusException;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/api/v1/apis")
+public class ApiController {
+
+    @Autowired
+    private ApiService apiService;
+
+    //查询
+    @PostMapping("/list")
+    public ResponseDTO<PageResult<Api>> list(@RequestBody ApiListRequest request) {
+        return apiService.list(request);
+    }
+    //根据regulatedIden查询
+    @PostMapping("/listByRegulatedIden")
+    public ResponseDTO<List<Api>> listByRegulatedIden(@RequestParam String regulatedIden) {
+
+
+        return apiService.listByRegulatedIden(regulatedIden);
+    }
+    //创建
+    @PostMapping("/create")
+    public ResponseDTO<Api> create(@RequestBody ApiAddRequest apiAddRequest) {
+
+        return apiService.create(apiAddRequest);
+    }
+
+    //删除
+    @PostMapping("/delete/{id}")
+    public ResponseDTO<Void> delete(@PathVariable Integer id) {
+
+        return apiService.delete(id);
+    }
+
+    //更新
+    @PostMapping("/update")
+    public ResponseDTO<Api> update( @RequestBody Api api) {
+
+        
+        return apiService.update(api);
+    }
+
+    //修改启停状态:1-启用,2-停用
+    @PostMapping("/changeStatus/{id}/{status}")
+    public ResponseDTO<Api> changeStatus(@PathVariable Integer id, @PathVariable Integer status) {
+        return apiService.changeStatus(id, status);
+    }
+
+}

+ 42 - 0
src/main/java/com/ymwk/supervisionserver/controller/RegulatedController.java

@@ -0,0 +1,42 @@
+package com.ymwk.supervisionserver.controller;
+
+import com.ymwk.supervisionserver.component.PageResult;
+import com.ymwk.supervisionserver.component.ResponseDTO;
+import com.ymwk.supervisionserver.entity.Regulated;
+import com.ymwk.supervisionserver.model.RegulatedAddRequest;
+import com.ymwk.supervisionserver.model.RegulatedListRequest;
+import com.ymwk.supervisionserver.service.RegulatedService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/v1/regulated")
+public class RegulatedController {
+
+    @Autowired
+    private RegulatedService regulatedService;
+
+    //查询
+    @PostMapping("/list")
+    public ResponseDTO<PageResult<Regulated>> list(@RequestBody RegulatedListRequest request) {
+        return regulatedService.list(request);
+    }
+
+    //创建
+    @PostMapping("/create")
+    public ResponseDTO<Regulated> create(@RequestBody RegulatedAddRequest request) {
+        return regulatedService.create(request);
+    }
+
+    //删除
+    @PostMapping("/delete")
+    public ResponseDTO<Void> delete(@RequestParam String iden) {
+        return regulatedService.delete(iden);
+    }
+
+    //更新
+    @PostMapping("/update")
+    public ResponseDTO<Regulated> update(@RequestBody Regulated regulated) {
+        return regulatedService.update(regulated);
+    }
+}

+ 598 - 0
src/main/java/com/ymwk/supervisionserver/controller/RemoteController.java

@@ -0,0 +1,598 @@
+package com.ymwk.supervisionserver.controller;
+
+
+
+import com.ymwk.supervisionserver.component.EndpointValidator;
+import com.ymwk.supervisionserver.component.ResponseDTO;
+import com.ymwk.supervisionserver.entity.Api;
+import com.ymwk.supervisionserver.entity.Regulated;
+import com.ymwk.supervisionserver.model.*;
+
+import com.ymwk.supervisionserver.service.ApiService;
+import com.ymwk.supervisionserver.service.RegulatedService;
+import jakarta.annotation.Resource;
+
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.JsonNode;
+import tools.jackson.databind.ObjectMapper;
+import tools.jackson.databind.node.ObjectNode;
+
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@RestController
+@RequestMapping("/api/v1/remote")
+public class RemoteController {
+
+    @Resource
+    private RegulatedService regulatedService;
+
+    @Resource
+    private ApiService apiService;
+    @Resource
+    private RestTemplate restTemplate;
+
+    @Resource
+    private ObjectMapper objectMapper;
+
+    //查询
+    @PostMapping("/dynamic")
+    public ResponseDTO<SystemInfo> dynamic(@RequestBody DynamicRequest request) {
+
+        Regulated regulated = regulatedService.selectByIden(request.getRegulatedIden());
+        if (regulated == null) {
+            return ResponseDTO.error(100001,"未找到对应的密码设施");
+        }
+        // 目前仅处理类型 1,其他类型可按需扩展
+        if (!Integer.valueOf(1).equals(regulated.getType())) {
+            System.out.println("暂不支持的密码设施类型:" + regulated.getType());
+            return ResponseDTO.error(100001,"暂不支持的密码设施类型:" + regulated.getType());
+        }
+
+        // 并发请求该设施下所有启用的 apiType=1 采集接口,停用的不请求
+        ResponseDTO<List<JsonNode>> fetched = fetchApiDataNodes(regulated, 1);
+        if (fetched.getCode() != ResponseDTO.OK_CODE) {
+            return ResponseDTO.error(fetched);
+        }
+        List<JsonNode> dataNodes = fetched.getData();
+
+        // 将各接口返回的数据分片按字段合并:字段以第一个返回的非空来源为准,重复字段不覆盖
+        ObjectNode merged = objectMapper.createObjectNode();
+        for (JsonNode dataNode : dataNodes) {
+            if (!dataNode.isObject()) {
+                continue;
+            }
+            for (Map.Entry<String, JsonNode> entry : dataNode.properties()) {
+                JsonNode value = entry.getValue();
+                if (value != null && !value.isNull() && !merged.has(entry.getKey())) {
+                    merged.set(entry.getKey(), value);
+                }
+            }
+        }
+
+        if (merged.isEmpty()) {
+            return ResponseDTO.error(100001,"采集接口返回数据为空");
+        }
+
+        SystemInfo systemInfo = objectMapper.treeToValue(merged, SystemInfo.class);
+        return ResponseDTO.ok(systemInfo, fetched.getMessage());
+    }
+
+    // 并发请求该密码设施下指定类型的全部启用采集接口;单个接口失败仅跳过并记录原因
+    // 返回值约定:code=100000 时 data 为成功解析的 data 节点列表,message 可能附带部分失败的接口地址;
+    // 全部失败或未配置接口时返回 code=100001 的错误提示
+    private ResponseDTO<List<JsonNode>> fetchApiDataNodes(Regulated regulated, Integer apiType) {
+        List<Api> apis = apiService.listByIdenAndType(regulated.getIden(), apiType);
+        if (apis.isEmpty()) {
+            return ResponseDTO.error(100001,"未找到对应的采集接口配置");
+        }
+
+        ExecutorService executor = Executors.newFixedThreadPool(apis.size());
+        List<JsonNode> dataNodes;
+        List<String> failedEndpoints = new ArrayList<>();
+        try {
+            List<CompletableFuture<JsonNode>> futures = new ArrayList<>(apis.size());
+            for (Api api : apis) {
+                futures.add(CompletableFuture.supplyAsync(() -> requestApiDataNode(api), executor));
+            }
+
+            dataNodes = new ArrayList<>(apis.size());
+            for (int i = 0; i < futures.size(); i++) {
+                Api api = apis.get(i);
+                try {
+                    JsonNode dataNode = futures.get(i).join();
+                    if (dataNode != null) {
+                        dataNodes.add(dataNode);
+                    }
+                } catch (CompletionException e) {
+                    // 详细失败原因只记录到服务端日志,返回给前端的提示保持简洁
+                    System.out.println("调用采集接口异常:" + api.getApiEndpoint() + ":" + e.getCause().getMessage());
+                    failedEndpoints.add(api.getApiEndpoint());
+                }
+            }
+        } finally {
+            executor.shutdown();
+        }
+
+        if (dataNodes.isEmpty()) {
+            return ResponseDTO.error(100001,"调用采集接口失败:" + String.join("、", failedEndpoints)
+                    + ",请检查该接口是否畅通");
+        }
+        if (failedEndpoints.isEmpty()) {
+            return ResponseDTO.ok(dataNodes);
+        }
+        return ResponseDTO.ok(dataNodes, "操作成功(部分采集接口调用失败:" + String.join("、", failedEndpoints)
+                + ",请检查该接口是否畅通,已返回其余采集结果)");
+    }
+
+    // 按接口配置的请求方法/参数调用采集接口,逐层剥离 data 包装,返回最内层的 data 节点
+    private JsonNode requestApiDataNode(Api api) {
+        ResponseEntity<String> responseEntity = sendApiRequest(api, parseApiParams(api));
+
+        if (!responseEntity.getStatusCode().is2xxSuccessful() || responseEntity.getBody() == null) {
+            throw new RestClientException("调用采集接口失败,HTTP 状态:" + responseEntity.getStatusCode());
+        }
+
+        // 兼容直接返回、单层 data 包装以及多层 data 包装的响应。
+        JsonNode root = objectMapper.readTree(responseEntity.getBody());
+        JsonNode dataNode = root;
+        while (dataNode.isObject() && dataNode.has("data")) {
+            dataNode = dataNode.get("data");
+        }
+
+        if (dataNode == null || dataNode.isNull()) {
+            throw new RestClientException("采集接口返回数据为空");
+        }
+
+        return dataNode;
+    }
+
+    // 解析接口配置的请求参数,未配置时返回空对象
+    private ObjectNode parseApiParams(Api api) {
+        String raw = api.getParams();
+        if (raw == null || raw.isBlank()) {
+            return objectMapper.createObjectNode();
+        }
+        JsonNode parsed;
+        try {
+            parsed = objectMapper.readTree(raw);
+        } catch (JacksonException e) {
+            throw new RestClientException("请求参数配置不是合法的JSON对象");
+        }
+        if (!parsed.isObject()) {
+            throw new RestClientException("请求参数配置不是合法的JSON对象");
+        }
+        return (ObjectNode) parsed;
+    }
+
+    // 按接口配置的请求方法和参数提交方式发送请求
+    // GET 固定将参数拼成 query string;POST 支持 json body / form 表单 / query string 三种方式
+    private ResponseEntity<String> sendApiRequest(Api api, ObjectNode params) {
+        String endpoint = api.getApiEndpoint() == null ? "" : api.getApiEndpoint().trim();
+        // 提前拦截非法地址,避免底层 HTTP 客户端抛出无法捕获的异常污染前端
+        if (!EndpointValidator.isValid(endpoint)) {
+            throw new RestClientException("接口地址不是合法的 http/https 地址:" + endpoint);
+        }
+
+        String method = api.getMethod() == null || api.getMethod().isBlank()
+                ? Api.METHOD_GET
+                : api.getMethod().trim().toUpperCase();
+
+        if (Api.METHOD_GET.equals(method)) {
+            URI uri = buildUriWithParams(endpoint, params);
+            System.out.println("调用接口请求 >> GET " + uri);
+            return restTemplate.getForEntity(uri, String.class);
+        }
+        if (!Api.METHOD_POST.equals(method)) {
+            throw new RestClientException("不支持的请求方法:" + api.getMethod());
+        }
+
+        String paramType = api.getParamType() == null || api.getParamType().isBlank()
+                ? Api.PARAM_TYPE_JSON
+                : api.getParamType().trim().toLowerCase();
+
+        if (Api.PARAM_TYPE_FORM.equals(paramType)) {
+            // 按 application/x-www-form-urlencoded 规范编码参数
+            StringBuilder form = new StringBuilder();
+            params.properties().forEach(entry -> {
+                if (form.length() > 0) {
+                    form.append('&');
+                }
+                form.append(URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8))
+                        .append('=')
+                        .append(URLEncoder.encode(paramToString(entry.getValue()), StandardCharsets.UTF_8));
+            });
+            System.out.println("调用接口请求 >> POST " + endpoint
+                    + " ct=application/x-www-form-urlencoded form=" + form);
+            return restTemplate.postForEntity(endpoint,
+                    byteEntity(form.toString(), MediaType.APPLICATION_FORM_URLENCODED), String.class);
+        }
+        if (Api.PARAM_TYPE_QUERY.equals(paramType)) {
+            URI uri = buildUriWithParams(endpoint, params);
+            System.out.println("调用接口请求 >> POST " + uri + " 无body");
+            return restTemplate.postForEntity(uri, null, String.class);
+        }
+        if (!Api.PARAM_TYPE_JSON.equals(paramType)) {
+            throw new RestClientException("不支持的参数提交方式:" + api.getParamType());
+        }
+        String json = params.toString();
+        System.out.println("调用接口请求 >> POST " + endpoint + " ct=application/json json=" + json);
+        return restTemplate.postForEntity(endpoint,
+                byteEntity(json, MediaType.APPLICATION_JSON), String.class);
+    }
+
+    // 以 byte[] 构造请求体并显式声明 Content-Length:
+    // 直接以 ObjectNode/对象作为 body 时 Jackson 转换器不设置 Content-Length,
+    // RestTemplate 会以 chunked 方式发送,部分服务端(如被监管平台)在 chunked 请求下解析不到参数;
+    // 参数值含中文时也会按 UTF-8 正确发送
+    private HttpEntity<byte[]> byteEntity(String body, MediaType contentType) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(contentType);
+        headers.setContentLength(body.getBytes(StandardCharsets.UTF_8).length);
+        return new HttpEntity<>(body.getBytes(StandardCharsets.UTF_8), headers);
+    }
+
+    // 将请求参数拼接到 endpoint 的 query string 上
+    private URI buildUriWithParams(String endpoint, ObjectNode params) {
+        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(endpoint);
+        params.properties().forEach(entry -> builder.queryParam(entry.getKey(), paramToString(entry.getValue())));
+        return builder.build().encode().toUri();
+    }
+
+    // 标量直接取文本,对象/数组序列化为 JSON 文本
+    private String paramToString(JsonNode value) {
+        return value.isValueNode() ? value.asText() : value.toString();
+    }
+
+
+    @PostMapping("/total")
+    public ResponseDTO<CryptoStatInfo> total(@RequestBody DynamicRequest request) {
+
+        Regulated regulated = regulatedService.selectByIden(request.getRegulatedIden());
+        if (regulated == null) {
+            return ResponseDTO.error(100001,"未找到对应的密码设施");
+        }
+        // 目前仅处理类型 1,其他类型可按需扩展
+        if (!Integer.valueOf(1).equals(regulated.getType())) {
+            System.out.println("暂不支持的密码设施类型:" + regulated.getType());
+            return ResponseDTO.error(100001,"暂不支持的密码设施类型:" + regulated.getType());
+        }
+
+        // 并发请求该设施下所有启用的 apiType=2 采集接口,停用的不请求
+        ResponseDTO<List<JsonNode>> fetched = fetchApiDataNodes(regulated, 2);
+        if (fetched.getCode() != ResponseDTO.OK_CODE) {
+            return ResponseDTO.error(fetched);
+        }
+        List<JsonNode> dataNodes = fetched.getData();
+
+        // 将各接口返回的数据分片按字段合并:字段以第一个返回的非空来源为准,重复字段不覆盖
+        // 例如一个源返回 signCount,另一个源返回 verifyCount,合并后补全为完整对象
+        ObjectNode merged = objectMapper.createObjectNode();
+        for (JsonNode dataNode : dataNodes) {
+            if (!dataNode.isObject()) {
+                continue;
+            }
+            for (Map.Entry<String, JsonNode> entry : dataNode.properties()) {
+                JsonNode value = entry.getValue();
+                if (value != null && !value.isNull() && !merged.has(entry.getKey())) {
+                    merged.set(entry.getKey(), value);
+                }
+            }
+        }
+
+        if (merged.isEmpty()) {
+            return ResponseDTO.error(100001,"采集接口返回数据为空");
+        }
+
+        CryptoStatInfo cryptoStatInfo = objectMapper.treeToValue(merged, CryptoStatInfo.class);
+        return ResponseDTO.ok(cryptoStatInfo, fetched.getMessage());
+
+    }
+
+    @PostMapping("/recentWeek")
+    public ResponseDTO<List<CryptoStatItem>> recentWeek(@RequestBody DynamicRequest request) {
+
+        Regulated regulated = regulatedService.selectByIden(request.getRegulatedIden());
+        if (regulated == null) {
+            return ResponseDTO.error(100001,"未找到对应的密码设施");
+        }
+        // 目前仅处理类型 1,其他类型可按需扩展
+        if (!Integer.valueOf(1).equals(regulated.getType())) {
+            System.out.println("暂不支持的密码设施类型:" + regulated.getType());
+            return ResponseDTO.error(100001,"暂不支持的密码设施类型:" + regulated.getType());
+        }
+
+        // 并发请求该设施下所有启用的 apiType=4 采集接口,停用的不请求
+        ResponseDTO<List<JsonNode>> fetched = fetchApiDataNodes(regulated, 4);
+        if (fetched.getCode() != ResponseDTO.OK_CODE) {
+            return ResponseDTO.error(fetched);
+        }
+        List<JsonNode> dataNodes = fetched.getData();
+
+        // 按日期(statAt)归组,同一天的数据分片按字段合并:字段以第一个返回的非空来源为准
+        // 例如一个源只返回同一天的 signCount,另一个源只返回同一天的 verifyCount,合并后补全为整行
+        Map<String, ObjectNode> mergedByStatAt = new LinkedHashMap<>();
+
+        for (JsonNode dataNode : dataNodes) {
+            List<JsonNode> items = new ArrayList<>();
+            if (dataNode.isArray()) {
+                dataNode.forEach(items::add);
+            } else {
+                items.add(dataNode);
+            }
+
+            for (JsonNode item : items) {
+                if (!item.isObject()) {
+                    continue;
+                }
+                // 没有 statAt 的数据无法关联到具体日期,直接跳过
+                String statAt = item.path("statAt").asText(null);
+                if (statAt == null || statAt.isBlank()) {
+                    continue;
+                }
+                ObjectNode target = mergedByStatAt
+                        .computeIfAbsent(statAt, k -> objectMapper.createObjectNode().put("statAt", k));
+                for (Map.Entry<String, JsonNode> entry : item.properties()) {
+                    JsonNode value = entry.getValue();
+                    if (value != null && !value.isNull() && !target.has(entry.getKey())) {
+                        target.set(entry.getKey(), value);
+                    }
+                }
+            }
+        }
+
+        if (mergedByStatAt.isEmpty()) {
+            return ResponseDTO.error(100001,"采集接口返回数据为空");
+        }
+
+        List<CryptoStatItem> cryptoStatItems = new ArrayList<>(mergedByStatAt.size());
+        for (ObjectNode row : mergedByStatAt.values()) {
+            cryptoStatItems.add(objectMapper.treeToValue(row, CryptoStatItem.class));
+        }
+        return ResponseDTO.ok(cryptoStatItems, fetched.getMessage());
+
+    }
+
+    @PostMapping("/static")
+    public ResponseDTO<Map<String, ComputerInfo>> staticInfo(@RequestBody DynamicRequest request) {
+
+        Regulated regulated = regulatedService.selectByIden(request.getRegulatedIden());
+        if (regulated == null) {
+            return ResponseDTO.error(100001,"未找到对应的密码设施");
+        }
+        // 目前仅处理类型 1,其他类型可按需扩展
+        if (!Integer.valueOf(1).equals(regulated.getType())) {
+            System.out.println("暂不支持的密码设施类型:" + regulated.getType());
+            return ResponseDTO.error(100001,"暂不支持的密码设施类型:" + regulated.getType());
+        }
+
+        // 并发请求该设施下所有启用的 apiType=3 采集接口,停用的不请求
+        ResponseDTO<List<JsonNode>> fetched = fetchApiDataNodes(regulated, 3);
+        if (fetched.getCode() != ResponseDTO.OK_CODE) {
+            return ResponseDTO.error(fetched);
+        }
+        List<JsonNode> dataNodes = fetched.getData();
+
+        // 将各接口返回的数据分片按字段合并:字段以第一个返回的非空来源为准,重复字段不覆盖
+        // 若某个源返回数组,取第一个元素参与合并
+        ObjectNode merged = objectMapper.createObjectNode();
+        for (JsonNode dataNode : dataNodes) {
+            if (dataNode.isArray()) {
+                if (dataNode.isEmpty()) {
+                    continue;
+                }
+                dataNode = dataNode.get(0);
+            }
+            if (!dataNode.isObject()) {
+                continue;
+            }
+            for (Map.Entry<String, JsonNode> entry : dataNode.properties()) {
+                JsonNode value = entry.getValue();
+                if (value != null && !value.isNull() && !merged.has(entry.getKey())) {
+                    merged.set(entry.getKey(), value);
+                }
+            }
+        }
+
+        if (merged.isEmpty()) {
+            return ResponseDTO.error(100001,"采集接口返回数据为空");
+        }
+
+        ComputerInfo computerInfo = objectMapper.treeToValue(merged, ComputerInfo.class);
+        return ResponseDTO.ok(Map.of("data", computerInfo), fetched.getMessage());
+
+    }
+
+    //应用接口列表采集
+    @PostMapping("/appApiList")
+    public ResponseDTO<Map<String, List<AppApiItem>>> appApiList(@RequestBody DynamicRequest request) {
+
+        Regulated regulated = regulatedService.selectByIden(request.getRegulatedIden());
+        if (regulated == null) {
+            return ResponseDTO.error(100001,"未找到对应的密码设施");
+        }
+
+        // 并发请求该设施下所有启用的 apiType=5 采集接口,停用的不请求
+        ResponseDTO<List<JsonNode>> fetched = fetchApiDataNodes(regulated, 5);
+        if (fetched.getCode() != ResponseDTO.OK_CODE) {
+            return ResponseDTO.error(fetched);
+        }
+        List<JsonNode> dataNodes = fetched.getData();
+
+        // 展开各来源的条目:兼容 {"list":[...]} 包装、直接数组以及单个对象
+        List<JsonNode> rawItems = new ArrayList<>();
+        for (JsonNode dataNode : dataNodes) {
+            JsonNode rowNode = dataNode;
+            if (rowNode.isObject() && rowNode.has("list") && rowNode.get("list").isArray()) {
+                rowNode = rowNode.get("list");
+            }
+            if (rowNode.isArray()) {
+                rowNode.forEach(rawItems::add);
+            } else {
+                rawItems.add(rowNode);
+            }
+        }
+
+        // 按 id 归组去重合并:字段以第一个返回的非空来源为准;无 id 的条目按整条内容去重
+        Map<Object, ObjectNode> keyed = new LinkedHashMap<>();
+        for (JsonNode item : rawItems) {
+            if (!item.isObject()) {
+                continue;
+            }
+            JsonNode idNode = item.path("id");
+            Object key = !idNode.isMissingNode() && !idNode.isNull()
+                    ? "id:" + idNode.asText()
+                    : item.toString();
+            ObjectNode target = keyed.computeIfAbsent(key, k -> objectMapper.createObjectNode());
+            for (Map.Entry<String, JsonNode> entry : item.properties()) {
+                JsonNode value = entry.getValue();
+                if (value != null && !value.isNull() && !target.has(entry.getKey())) {
+                    target.set(entry.getKey(), value);
+                }
+            }
+        }
+
+        if (keyed.isEmpty()) {
+            return ResponseDTO.error(100001,"采集接口返回数据为空");
+        }
+
+        List<AppApiItem> items = new ArrayList<>(keyed.size());
+        for (ObjectNode row : keyed.values()) {
+            items.add(objectMapper.treeToValue(row, AppApiItem.class));
+        }
+        return ResponseDTO.ok(Map.of("list", items), fetched.getMessage());
+
+    }
+
+    //应用接口启停
+    @PostMapping("/appApiToggle")
+    public ResponseDTO<Void> appApiToggle(@RequestBody AppApiToggleRequest request) {
+
+        if (request.getId() == null) {
+            return ResponseDTO.error(100001,"id 不能为空");
+        }
+        Regulated regulated = regulatedService.selectByIden(request.getRegulatedIden());
+        if (regulated == null) {
+            return ResponseDTO.error(100001,"未找到对应的密码设施");
+        }
+
+        // 启停接口应唯一;存在多条启用配置时直接报错,避免调用到错误的接口
+        List<Api> apis = apiService.listByIdenAndType(request.getRegulatedIden(), 6);
+        if (apis.isEmpty()) {
+            return ResponseDTO.error(100001,"未找到对应的采集接口配置");
+        }
+        if (apis.size() > 1) {
+            return ResponseDTO.error(100001,"存在多个启用的启停接口配置,请仅保留一条");
+        }
+        Api api = apis.get(0);
+
+        try {
+            ObjectNode params = parseApiParams(api);
+            // id 与配置的请求参数合并,同名参数以本次指令为准
+            params.put("id", request.getId());
+
+            ResponseEntity<String> responseEntity = sendApiRequest(api, params);
+
+            if (!responseEntity.getStatusCode().is2xxSuccessful() || responseEntity.getBody() == null) {
+                return ResponseDTO.error(100001,"下发启停指令失败:" + api.getApiEndpoint()
+                        + ",请检查该接口是否畅通");
+            }
+
+            // 校验采集端返回的包装响应:code=100000 成功,100001 失败
+            JsonNode root = objectMapper.readTree(responseEntity.getBody());
+            JsonNode codeNode = root.path("code");
+            if (!root.isObject() || !codeNode.isNumber()) {
+                return ResponseDTO.error(100001,"启停接口返回数据异常:" + api.getApiEndpoint());
+            }
+            int code = codeNode.asInt();
+            if (code == ResponseDTO.OK_CODE) {
+                return ResponseDTO.okMsg("操作成功");
+            }
+            // 非成功码统一透出对端的 message,便于定位问题
+            String remoteMsg = root.path("message").asText(null);
+            String detail = remoteMsg != null && !remoteMsg.isBlank() ? remoteMsg : "返回码:" + code;
+            return ResponseDTO.error(100001,"下发启停指令失败:" + detail);
+        } catch (RestClientException e) {
+            System.out.println("下发启停指令异常:" + api.getApiEndpoint() + ":" + e.getMessage());
+            return ResponseDTO.error(100001,"下发启停指令异常:" + api.getApiEndpoint()
+                    + ",请检查该接口是否畅通");
+        } catch (JacksonException e) {
+            System.out.println("下发启停指令响应解析异常:" + e.getMessage());
+            return ResponseDTO.error(100001,"启停接口返回数据解析异常:" + api.getApiEndpoint());
+        }
+
+    }
+
+    //连通性测试:按 t_api 配置的 method/paramType/params 实际调用一次该接口,返回连通结果
+    @PostMapping("/appApiTest")
+    public ResponseDTO<Map<String, Object>> appApiTest(@RequestBody AppApiTestRequest request) {
+
+        if (request.getId() == null) {
+            return ResponseDTO.error(100001,"id 不能为空");
+        }
+        Regulated regulated = regulatedService.selectByIden(request.getRegulatedIden());
+        if (regulated == null) {
+            return ResponseDTO.error(100001,"未找到对应的密码设施");
+        }
+        Api api = apiService.getApiById(request.getId());
+        // 该接口配置必须属于传入的密码设施,避免跨设施测试
+        if (api == null || !api.getRegulatedIden().equals(regulated.getIden())) {
+            return ResponseDTO.error(100001,"未找到对应的接口配置");
+        }
+        if (!EndpointValidator.isValid(api.getApiEndpoint())) {
+            return ResponseDTO.error(100001,"接口地址不是合法的 http/https 地址");
+        }
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("method", api.getMethod());
+        result.put("paramType", api.getParamType());
+        result.put("endpoint", api.getApiEndpoint());
+
+        long start = System.currentTimeMillis();
+        try {
+            ResponseEntity<String> responseEntity = sendApiRequest(api, parseApiParams(api));
+            result.put("costMs", System.currentTimeMillis() - start);
+            int httpStatus = responseEntity.getStatusCode().value();
+            result.put("httpStatus", httpStatus);
+            boolean success = responseEntity.getStatusCode().is2xxSuccessful() && responseEntity.getBody() != null;
+            result.put("success", success);
+            result.put("message", success ? null : "HTTP 状态异常:" + httpStatus);
+            return ResponseDTO.ok(result);
+        } catch (RestClientException e) {
+            System.out.println("接口连通性测试异常:" + api.getApiEndpoint() + ":" + e.getMessage());
+            result.put("costMs", System.currentTimeMillis() - start);
+            result.put("success", false);
+            result.put("message", e.getMessage());
+            return ResponseDTO.ok(result);
+        } catch (JacksonException e) {
+            System.out.println("接口连通性测试请求构造异常:" + e.getMessage());
+            result.put("costMs", System.currentTimeMillis() - start);
+            result.put("success", false);
+            result.put("message", "请求参数配置异常:" + e.getMessage());
+            return ResponseDTO.ok(result);
+        }
+
+    }
+    
+}

+ 77 - 0
src/main/java/com/ymwk/supervisionserver/entity/Api.java

@@ -0,0 +1,77 @@
+package com.ymwk.supervisionserver.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_api")
+public class Api {
+
+    /**
+     * 状态:1-启用
+     */
+    public static final Integer STATUS_ENABLED = 1;
+    /**
+     * 状态:2-停用
+     */
+    public static final Integer STATUS_DISABLED = 2;
+
+    /**
+     * 请求方法:GET
+     */
+    public static final String METHOD_GET = "GET";
+    /**
+     * 请求方法:POST
+     */
+    public static final String METHOD_POST = "POST";
+
+    /**
+     * 参数提交方式:JSON body
+     */
+    public static final String PARAM_TYPE_JSON = "json";
+    /**
+     * 参数提交方式:表单 body(application/x-www-form-urlencoded)
+     */
+    public static final String PARAM_TYPE_FORM = "form";
+    /**
+     * 参数提交方式:拼成 query string
+     */
+    public static final String PARAM_TYPE_QUERY = "query";
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    private String regulatedIden;
+
+    private String apiEndpoint;
+
+    private Integer apiType;
+
+    /**
+     * 状态:1-启用,2-停用,默认1
+     */
+    private Integer status;
+
+    /**
+     * 请求方法:GET/POST,默认GET
+     */
+    private String method;
+
+    /**
+     * 请求参数:JSON 对象字符串;GET 时拼成 query string,POST 时作为 JSON body
+     */
+    private String params;
+
+    /**
+     * 参数提交方式:json/form/query,默认json;GET 方法固定按 query 处理
+     */
+    private String paramType;
+
+    private LocalDateTime createAt;
+
+    private LocalDateTime updateAt;
+}

+ 32 - 0
src/main/java/com/ymwk/supervisionserver/entity/Regulated.java

@@ -0,0 +1,32 @@
+package com.ymwk.supervisionserver.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_regulated")
+public class Regulated {
+
+    @TableId(type = IdType.AUTO)
+    private Integer id;
+
+    private String iden;
+
+    private String name;
+
+    private String ip;
+
+    private String port;
+
+    private Integer type;
+
+    private Integer status;
+
+    private LocalDateTime createAt;
+
+    private LocalDateTime updateAt;
+}

+ 15 - 0
src/main/java/com/ymwk/supervisionserver/mapper/ApiMapper.java

@@ -0,0 +1,15 @@
+package com.ymwk.supervisionserver.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ymwk.supervisionserver.entity.Api;
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface ApiMapper extends BaseMapper<Api> {
+
+
+    @Delete("DELETE FROM t_api WHERE regulated_iden = #{iden}")
+    void deleteByRegulatedIden(String iden);
+}

+ 31 - 0
src/main/java/com/ymwk/supervisionserver/mapper/RegulatedMapper.java

@@ -0,0 +1,31 @@
+package com.ymwk.supervisionserver.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ymwk.supervisionserver.entity.Regulated;
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+public interface RegulatedMapper extends BaseMapper<Regulated> {
+
+    @Select("SELECT * FROM t_regulated WHERE iden = #{iden} LIMIT 1")
+    Regulated selectByIden(@Param("iden") String iden);
+
+    @Select("<script>" +
+            "SELECT * FROM t_regulated " +
+            "<where>" +
+            "  <if test='name != null and name != \"\"'>" +
+            "    name LIKE CONCAT('%', #{name}, '%')" +
+            "  </if>" +
+            "</where>" +
+            "</script>")
+    List<Regulated> selectByName(@Param("name") String name);
+
+    @Select("SELECT * FROM t_regulated WHERE ip = #{ip} AND port = #{port} LIMIT 1")
+    Regulated selectByIpAndPort(@Param("ip") String ip, @Param("port") String port);
+
+    @Delete("DELETE FROM t_regulated WHERE iden = #{iden}")
+    int deleteByIden(String iden);
+}

+ 27 - 0
src/main/java/com/ymwk/supervisionserver/model/ApiAddRequest.java

@@ -0,0 +1,27 @@
+package com.ymwk.supervisionserver.model;
+
+
+import lombok.Data;
+
+@Data
+public class ApiAddRequest {
+
+    private String regulatedIden;
+    private String apiEndpoint;
+    private Integer apiType;
+
+    /**
+     * 请求方法:GET/POST,不传默认GET
+     */
+    private String method;
+
+    /**
+     * 请求参数:JSON 对象字符串,可不传
+     */
+    private String params;
+
+    /**
+     * 参数提交方式:json/form/query,不传默认json;GET 方法固定按 query 处理
+     */
+    private String paramType;
+}

+ 15 - 0
src/main/java/com/ymwk/supervisionserver/model/ApiListRequest.java

@@ -0,0 +1,15 @@
+package com.ymwk.supervisionserver.model;
+
+import com.ymwk.supervisionserver.component.PageParam;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class ApiListRequest extends PageParam {
+
+    /**
+     * 状态:1-启用,2-停用;null-不过滤
+     */
+    private Integer status;
+}

+ 32 - 0
src/main/java/com/ymwk/supervisionserver/model/AppApiItem.java

@@ -0,0 +1,32 @@
+package com.ymwk.supervisionserver.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+/**
+ * 应用接口
+ */
+@Data
+public class AppApiItem {
+
+    /** 状态:1-启用 */
+    public static final Integer STATUS_ENABLED = 1;
+    /** 状态:0-停用 */
+    public static final Integer STATUS_DISABLED = 0;
+
+    /** 应用接口编号 */
+    @JsonProperty("id")
+    private Integer id;
+
+    /** 接口地址 */
+    @JsonProperty("url")
+    private String url;
+
+    /** 备注 */
+    @JsonProperty("remark")
+    private String remark;
+
+    /** 状态 */
+    @JsonProperty("status")
+    private Integer status;
+}

+ 16 - 0
src/main/java/com/ymwk/supervisionserver/model/AppApiTestRequest.java

@@ -0,0 +1,16 @@
+package com.ymwk.supervisionserver.model;
+
+import lombok.Data;
+
+/**
+ * 应用接口连通性测试请求
+ */
+@Data
+public class AppApiTestRequest {
+
+    /** 密码设施标识 */
+    private String regulatedIden;
+
+    /** t_api 记录 id */
+    private Integer id;
+}

+ 16 - 0
src/main/java/com/ymwk/supervisionserver/model/AppApiToggleRequest.java

@@ -0,0 +1,16 @@
+package com.ymwk.supervisionserver.model;
+
+import lombok.Data;
+
+/**
+ * 应用接口启停请求
+ */
+@Data
+public class AppApiToggleRequest {
+
+    /** 密码设施标识 */
+    private String regulatedIden;
+
+    private Integer id;
+
+}

+ 27 - 0
src/main/java/com/ymwk/supervisionserver/model/ComputerInfo.java

@@ -0,0 +1,27 @@
+package com.ymwk.supervisionserver.model;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+@Data
+public class ComputerInfo {
+
+    /** 主机名 */
+    @JsonProperty("computerName")
+    private String computerName;
+
+    /** 主机 IP */
+    @JsonProperty("computerIp")
+    private String computerIp;
+
+    /** 操作系统名称,如 windows */
+    @JsonProperty("osName")
+    private String osName;
+
+    /** 系统架构,如 x86_64 */
+    @JsonProperty("osArch")
+    private String osArch;
+
+    /** 用户目录,如 C:\\Users\\Administrator */
+    @JsonProperty("userDir")
+    private String userDir;
+}

+ 25 - 0
src/main/java/com/ymwk/supervisionserver/model/CryptoStatInfo.java

@@ -0,0 +1,25 @@
+package com.ymwk.supervisionserver.model;
+
+import lombok.Data;
+
+@Data
+public class CryptoStatInfo {
+
+    /** 签名次数 */
+    private Integer signCount;
+
+    /** 验签次数 */
+    private Integer verifyCount;
+
+    /** 加密次数 */
+    private Integer encryptCount;
+
+    /** 解密次数 */
+    private Integer decryptCount;
+
+    /** HMAC 次数 */
+    private Integer hmacCount;
+
+    /** 摘要次数 */
+    private Integer digestCount;
+}

+ 14 - 0
src/main/java/com/ymwk/supervisionserver/model/CryptoStatItem.java

@@ -0,0 +1,14 @@
+package com.ymwk.supervisionserver.model;
+
+import lombok.Data;
+
+@Data
+public class CryptoStatItem {
+    private Integer signCount;
+    private Integer verifyCount;
+    private Integer encryptCount;
+    private Integer decryptCount;
+    private Integer hmacCount;
+    private Integer digestCount;
+    private String statAt;
+}

+ 9 - 0
src/main/java/com/ymwk/supervisionserver/model/DynamicRequest.java

@@ -0,0 +1,9 @@
+package com.ymwk.supervisionserver.model;
+
+import lombok.Data;
+
+@Data
+public class DynamicRequest {
+
+    private String regulatedIden;
+}

+ 17 - 0
src/main/java/com/ymwk/supervisionserver/model/RegulatedAddRequest.java

@@ -0,0 +1,17 @@
+package com.ymwk.supervisionserver.model;
+
+import lombok.Data;
+
+@Data
+public class RegulatedAddRequest {
+
+    private String name;
+
+    private String ip;
+
+    private String port;
+
+    private Integer type;
+
+    private Integer status;
+}

+ 13 - 0
src/main/java/com/ymwk/supervisionserver/model/RegulatedListRequest.java

@@ -0,0 +1,13 @@
+package com.ymwk.supervisionserver.model;
+
+import com.ymwk.supervisionserver.component.PageParam;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@EqualsAndHashCode(callSuper = true)
+@Data
+public class RegulatedListRequest extends PageParam {
+
+    private String name;
+
+}

+ 55 - 0
src/main/java/com/ymwk/supervisionserver/model/SystemInfo.java

@@ -0,0 +1,55 @@
+package com.ymwk.supervisionserver.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import java.util.List;
+
+@Data
+public class SystemInfo {
+
+    @JsonProperty("memInfo")
+    private MemInfo memInfo;
+
+    @JsonProperty("cpuInfo")
+    private CpuInfo cpuInfo;
+
+    @JsonProperty("disks")
+    private List<DiskInfo> disks;
+
+    @JsonProperty("startTime")
+    private String startTime;
+
+    @JsonProperty("runTime")
+    private String runTime;
+
+    @Data
+    public static class MemInfo {
+        @JsonProperty("total") private String total;
+        @JsonProperty("used")  private String used;
+        @JsonProperty("free")  private String free;
+        @JsonProperty("usage") private String usage;
+    }
+
+    @Data
+    public static class CpuInfo {
+        @JsonProperty("cpuNum") private Integer cpuNum;
+        @JsonProperty("total")  private Integer total;
+        @JsonProperty("sys")    private Integer sys;
+        @JsonProperty("used")   private String used;
+        @JsonProperty("wait")   private Integer wait;
+        @JsonProperty("free")   private Integer free;
+    }
+
+    @Data
+    public static class DiskInfo {
+        @JsonProperty("dirName")     private String dirName;
+        @JsonProperty("sysTypeName") private String sysTypeName;
+        @JsonProperty("typeName")    private String typeName;
+        @JsonProperty("total")       private String total;
+        @JsonProperty("value")       private Integer value;
+        @JsonProperty("name")        private String name;
+        @JsonProperty("free")        private String free;
+        @JsonProperty("used")        private String used;
+        @JsonProperty("usage")       private String usage;
+    }
+}

+ 33 - 0
src/main/java/com/ymwk/supervisionserver/service/ApiService.java

@@ -0,0 +1,33 @@
+package com.ymwk.supervisionserver.service;
+
+import com.ymwk.supervisionserver.component.PageResult;
+import com.ymwk.supervisionserver.component.ResponseDTO;
+import com.ymwk.supervisionserver.entity.Api;
+import com.ymwk.supervisionserver.model.ApiAddRequest;
+import com.ymwk.supervisionserver.model.ApiListRequest;
+
+import java.util.List;
+
+public interface ApiService  {
+
+    Api selectByIden(String regulatedIden,Integer apiType );
+
+
+    ResponseDTO<PageResult<Api>> list(ApiListRequest request);
+
+    ResponseDTO<Api> create(ApiAddRequest apiAddRequest);
+
+    ResponseDTO<Void> delete(Integer id);
+
+    ResponseDTO<Api> update(Api api);
+
+    ResponseDTO<Api> changeStatus(Integer id, Integer status);
+
+    ResponseDTO<List<Api>> listByRegulatedIden(String regulatedIden);
+
+    List<Api> listByIdenAndType(String regulatedIden, Integer apiType);
+
+    Api getApiById(Integer id);
+
+
+}

+ 34 - 0
src/main/java/com/ymwk/supervisionserver/service/RegulatedService.java

@@ -0,0 +1,34 @@
+package com.ymwk.supervisionserver.service;
+
+import com.ymwk.supervisionserver.component.PageResult;
+import com.ymwk.supervisionserver.component.ResponseDTO;
+import com.ymwk.supervisionserver.entity.Regulated;
+import com.ymwk.supervisionserver.model.RegulatedAddRequest;
+import com.ymwk.supervisionserver.model.RegulatedListRequest;
+
+import java.util.List;
+
+public interface RegulatedService {
+    /**
+     * 根据iden查询Regulated
+     */
+    Regulated selectByIden(String iden);
+    
+    /**
+     * 根据name查询Regulated列表
+     */
+    List<Regulated> selectByName(String name);
+    
+    /**
+     * 根据ip和port查询Regulated
+     */
+    Regulated selectByIpAndPort(String ip, String port);
+
+    ResponseDTO<PageResult<Regulated>> list(RegulatedListRequest request);
+
+    ResponseDTO<Regulated> create(RegulatedAddRequest request);
+
+    ResponseDTO<Void> delete(String iden);
+
+    ResponseDTO<Regulated> update(Regulated regulated);
+}

+ 210 - 0
src/main/java/com/ymwk/supervisionserver/service/impl/ApiServiceImpl.java

@@ -0,0 +1,210 @@
+package com.ymwk.supervisionserver.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ymwk.supervisionserver.component.EndpointValidator;
+import com.ymwk.supervisionserver.component.PageResult;
+import com.ymwk.supervisionserver.component.ResponseDTO;
+import com.ymwk.supervisionserver.entity.Api;
+import com.ymwk.supervisionserver.mapper.ApiMapper;
+import com.ymwk.supervisionserver.model.ApiAddRequest;
+import com.ymwk.supervisionserver.model.ApiListRequest;
+import com.ymwk.supervisionserver.service.ApiService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.ObjectMapper;
+
+import java.util.List;
+
+@Service
+public class ApiServiceImpl implements ApiService {
+
+    @Resource
+    private ApiMapper apiMapper;
+
+    @Resource
+    private ObjectMapper objectMapper;
+
+
+    @Override
+    public Api selectByIden(String regulatedIden, Integer apiType) {
+        LambdaQueryWrapper<Api> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Api::getRegulatedIden, regulatedIden);
+        wrapper.eq(Api::getApiType, apiType);
+        return apiMapper.selectOne(wrapper);
+    }
+
+    @Override
+    public ResponseDTO<PageResult<Api>> list(ApiListRequest request) {
+        LambdaQueryWrapper<Api> wrapper = new LambdaQueryWrapper<>();
+        if (request.getStatus() != null) {
+            wrapper.eq(Api::getStatus, request.getStatus());
+        }
+
+        Page<Api> page = apiMapper.selectPage(
+                new Page<>(request.getPageNum(), request.getPageSize()), wrapper);
+
+        PageResult<Api> result = new PageResult<>();
+        result.setTotal(page.getTotal());
+        result.setList(page.getRecords());
+        return ResponseDTO.ok(result);
+    }
+
+    @Override
+    public ResponseDTO<Api> create(ApiAddRequest apiAddRequest) {
+        if (!EndpointValidator.isValid(apiAddRequest.getApiEndpoint())) {
+            return ResponseDTO.error(100001, "接口地址必须是完整的 http/https 地址");
+        }
+        String method = normalizeMethod(apiAddRequest.getMethod());
+        if (method == null) {
+            return ResponseDTO.error(100001, "不支持的请求方法:仅支持GET或POST");
+        }
+        String paramType = normalizeParamType(apiAddRequest.getParamType());
+        if (paramType == null) {
+            return ResponseDTO.error(100001, "不支持的参数提交方式:仅支持json/form/query");
+        }
+        if (!isValidParams(apiAddRequest.getParams())) {
+            return ResponseDTO.error(100001, "请求参数必须是合法的JSON对象");
+        }
+        Api api = new Api();
+        api.setRegulatedIden(apiAddRequest.getRegulatedIden());
+        api.setApiEndpoint(apiAddRequest.getApiEndpoint().trim());
+        api.setApiType(apiAddRequest.getApiType());
+        api.setMethod(method);
+        api.setParams(apiAddRequest.getParams());
+        api.setParamType(paramType);
+        // 默认启用
+        api.setStatus(Api.STATUS_ENABLED);
+        return ResponseDTO.ok(apiMapper.insert(api) > 0 ? api : null);
+    }
+
+    @Override
+    public ResponseDTO<Void> delete(Integer id) {
+        if (apiMapper.deleteById(id) > 0) {
+            return ResponseDTO.ok(null);
+        } else {
+            return ResponseDTO.error(100001, "删除api失败");
+        }
+    }
+
+    @Override
+    public ResponseDTO<Api> changeStatus(Integer id, Integer status) {
+        if (!Api.STATUS_ENABLED.equals(status) && !Api.STATUS_DISABLED.equals(status)) {
+            return ResponseDTO.error(100001, "不合法的状态值,仅支持1(启用)或2(停用)");
+        }
+        Api existingApi = apiMapper.selectById(id);
+        if (existingApi == null) {
+            return ResponseDTO.error(100001, "未找到对应的api");
+        }
+        if (status.equals(existingApi.getStatus())) {
+            return ResponseDTO.ok(existingApi);
+        }
+        existingApi.setStatus(status);
+        if (apiMapper.updateById(existingApi) > 0) {
+            return ResponseDTO.ok(existingApi);
+        } else {
+            return ResponseDTO.error(100001, "修改启停状态失败");
+        }
+    }
+
+    @Override
+    public ResponseDTO<Api> update(Api api) {
+
+        Api existingApi = apiMapper.selectById(api.getId());
+        if (existingApi != null) {
+            // method 不传时保持原值;params 传空串表示清空,传 null 表示保持原值
+            String method = api.getMethod() == null || api.getMethod().isBlank()
+                    ? normalizeMethod(existingApi.getMethod())
+                    : normalizeMethod(api.getMethod());
+            if (method == null) {
+                return ResponseDTO.error(100001, "不支持的请求方法:仅支持GET或POST");
+            }
+            String params = api.getParams() == null ? existingApi.getParams() : api.getParams();
+            if (!isValidParams(params)) {
+                return ResponseDTO.error(100001, "请求参数必须是合法的JSON对象");
+            }
+            if (!EndpointValidator.isValid(api.getApiEndpoint())) {
+                return ResponseDTO.error(100001, "接口地址必须是完整的 http/https 地址");
+            }
+            // paramType 不传时保持原值
+            String paramType = api.getParamType() == null || api.getParamType().isBlank()
+                    ? normalizeParamType(existingApi.getParamType())
+                    : normalizeParamType(api.getParamType());
+            if (paramType == null) {
+                return ResponseDTO.error(100001, "不支持的参数提交方式:仅支持json/form/query");
+            }
+            existingApi.setRegulatedIden(api.getRegulatedIden());
+            existingApi.setApiEndpoint(api.getApiEndpoint().trim());
+            existingApi.setApiType(api.getApiType());
+            existingApi.setMethod(method);
+            existingApi.setParams(params);
+            existingApi.setParamType(paramType);
+            api = existingApi;
+        }
+        return ResponseDTO.ok(apiMapper.updateById(api) > 0 ? api : null);
+
+    }
+
+    // 规范化请求方法:空值默认GET,仅支持GET/POST,其他返回null
+    private String normalizeMethod(String method) {
+        if (method == null || method.isBlank()) {
+            return Api.METHOD_GET;
+        }
+        String normalized = method.trim().toUpperCase();
+        if (Api.METHOD_GET.equals(normalized) || Api.METHOD_POST.equals(normalized)) {
+            return normalized;
+        }
+        return null;
+    }
+
+    // 规范化参数提交方式:空值默认json,仅支持json/form/query,其他返回null
+    private String normalizeParamType(String paramType) {
+        if (paramType == null || paramType.isBlank()) {
+            return Api.PARAM_TYPE_JSON;
+        }
+        String normalized = paramType.trim().toLowerCase();
+        if (Api.PARAM_TYPE_JSON.equals(normalized)
+                || Api.PARAM_TYPE_FORM.equals(normalized)
+                || Api.PARAM_TYPE_QUERY.equals(normalized)) {
+            return normalized;
+        }
+        return null;
+    }
+
+    // 请求参数允许为空;不为空时必须是合法的JSON对象
+    private boolean isValidParams(String params) {
+        if (params == null || params.isBlank()) {
+            return true;
+        }
+        try {
+            return objectMapper.readTree(params).isObject();
+        } catch (JacksonException e) {
+            return false;
+        }
+    }
+
+    @Override
+    public ResponseDTO<List<Api>> listByRegulatedIden(String regulatedIden) {
+        LambdaQueryWrapper<Api> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Api::getRegulatedIden, regulatedIden);
+        List<Api> list = apiMapper.selectList(wrapper);
+        return ResponseDTO.ok(list);
+    }
+
+    @Override
+    public List<Api> listByIdenAndType(String regulatedIden, Integer apiType) {
+        LambdaQueryWrapper<Api> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Api::getRegulatedIden, regulatedIden);
+        wrapper.eq(Api::getApiType, apiType);
+        // 仅返回启用状态的接口,停用的不参与远程请求
+        wrapper.eq(Api::getStatus, Api.STATUS_ENABLED);
+        wrapper.orderByAsc(Api::getId);
+        return apiMapper.selectList(wrapper);
+    }
+
+    @Override
+    public Api getApiById(Integer id) {
+        return apiMapper.selectById(id);
+    }
+}

+ 111 - 0
src/main/java/com/ymwk/supervisionserver/service/impl/RegulatedServiceImpl.java

@@ -0,0 +1,111 @@
+package com.ymwk.supervisionserver.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.ymwk.supervisionserver.component.PageResult;
+import com.ymwk.supervisionserver.component.ResponseDTO;
+import com.ymwk.supervisionserver.component.SmartPageUtil;
+import com.ymwk.supervisionserver.entity.Api;
+import com.ymwk.supervisionserver.entity.Regulated;
+import com.ymwk.supervisionserver.mapper.ApiMapper;
+import com.ymwk.supervisionserver.mapper.RegulatedMapper;
+import com.ymwk.supervisionserver.model.RegulatedAddRequest;
+import com.ymwk.supervisionserver.model.RegulatedListRequest;
+import com.ymwk.supervisionserver.service.RegulatedService;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class RegulatedServiceImpl  implements RegulatedService {
+
+    @Resource
+    private  RegulatedMapper regulatedMapper;
+
+    @Resource
+    private ApiMapper apiMapper;
+
+    @Override
+    public Regulated selectByIden(String iden) {
+        return regulatedMapper.selectByIden(iden);
+    }
+
+    @Override
+    public List<Regulated> selectByName(String name) {
+        return regulatedMapper.selectByName(name);
+    }
+
+    @Override
+    public Regulated selectByIpAndPort(String ip, String port) {
+        return regulatedMapper.selectByIpAndPort(ip, port);
+    }
+
+    @Override
+    public ResponseDTO<PageResult<Regulated>> list(RegulatedListRequest request) {
+        LambdaQueryWrapper<Regulated> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(request.getName() != null, Regulated::getName, request.getName());
+        Page<Regulated> page = new Page<>(request.getPageNum(), request.getPageSize());
+        page = regulatedMapper.selectPage(page, wrapper);
+
+        PageResult<Regulated> pageResult = SmartPageUtil.convert2PageResult(page, page.getRecords());
+
+        return ResponseDTO.ok(pageResult);
+    }
+
+    @Override
+    @Transactional
+    public ResponseDTO<Regulated> create(RegulatedAddRequest request) {
+        Regulated regulated = new Regulated();
+        regulated.setIden(UUID.randomUUID().toString().toUpperCase().replace("-", ""));
+        regulated.setName(request.getName());
+        regulated.setIp(request.getIp());
+        regulated.setPort(request.getPort());
+        regulated.setType(request.getType());
+        regulated.setStatus(1); // 默认状态为1
+
+        if (regulatedMapper.insert(regulated) <= 0) {
+            return ResponseDTO.ok(null);
+        }
+
+        for (int apiType = 1; apiType <= 4; apiType++) {
+            Api api = new Api();
+            api.setRegulatedIden(regulated.getIden());
+            api.setApiType(apiType);
+            api.setApiEndpoint("");
+            if (apiMapper.insert(api) <= 0) {
+                throw new IllegalStateException("创建regulated的默认api记录失败");
+            }
+        }
+
+        return ResponseDTO.ok(regulated);
+    }
+
+    @Override
+    public ResponseDTO<Void> delete(String iden) {
+
+        if (regulatedMapper.deleteByIden(iden) > 0) {
+            // 同时删除关联的API记录
+            apiMapper.deleteByRegulatedIden(iden);
+            return ResponseDTO.ok(null);
+        }
+        return ResponseDTO.error(100001, "删除regulated失败");
+    }
+
+    @Override
+    public ResponseDTO<Regulated> update(Regulated regulated) {
+        Regulated existingRegulated = regulatedMapper.selectById(regulated.getId());
+        if (existingRegulated != null) {
+            existingRegulated.setIden(regulated.getIden());
+            existingRegulated.setName(regulated.getName());
+            existingRegulated.setIp(regulated.getIp());
+            existingRegulated.setPort(regulated.getPort());
+            existingRegulated.setType(regulated.getType());
+            existingRegulated.setStatus(regulated.getStatus());
+            regulated = existingRegulated;
+        }
+        return ResponseDTO.ok(regulatedMapper.updateById(regulated) > 0 ? regulated : null);
+    }
+}

+ 41 - 0
src/main/resources/application.yaml

@@ -0,0 +1,41 @@
+spring:
+  application:
+    name: supervisionServer
+  servlet:
+    multipart:
+      max-file-size: 300MB
+      max-request-size: 300MB
+  datasource:
+    # 驱动类 (使用 MySQL 8.0 及以上版本推荐驱动)
+    driver-class-name: com.mysql.cj.jdbc.Driver
+    # 数据库连接 URL,注意替换库名、时区和编码设置
+    url: jdbc:mysql://10.10.10.30:3306/supervision?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
+    # 数据库用户名
+    username: supervision
+    # 数据库密码
+    password: EjfXzFMksCKFiRin
+
+server:
+  port: 9055
+
+
+minio:
+  endpoint: http://10.10.10.25:9020
+  bucket-name: test
+  access-key: minio_bxM8k5
+  secret-key: minio_mP6zC4
+
+# 2. MyBatis-Plus 专属配置
+mybatis-plus:
+  # 指定 XML 映射文件的位置 (用于自定义 SQL)
+  # 默认值为 classpath*:/mapper/**/*.xml,通常无需修改
+  mapper-locations: classpath*:/mapper/**/*.xml
+  # 设置实体类的别名包,这样在 XML 中可以直接使用类名,无需全限定名
+  type-aliases-package: com.ymwk.supervisionserver.entity
+  # 原生 MyBatis 的详细配置
+  configuration:
+    # 开启下划线到驼峰的自动映射 (例如: user_name -> userName)
+    # 默认为 true,建议保持开启
+    map-underscore-to-camel-case: true
+    # 将执行的 SQL 语句输出到控制台 (开发时很有用)
+    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

+ 13 - 0
src/test/java/com/ymwk/supervisionserver/SupervisionServerApplicationTests.java

@@ -0,0 +1,13 @@
+package com.ymwk.supervisionserver;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class SupervisionServerApplicationTests {
+
+    @Test
+    void contextLoads() {
+    }
+
+}