From e281b21bfd0f598f57283a718a33117f576bf834 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=B6=E8=AE=AD?= Date: Thu, 26 Feb 2026 18:13:37 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .mvn/wrapper/maven-wrapper.jar | Bin 0 -> 62547 bytes .mvn/wrapper/maven-wrapper.properties | 2 + mvnw | 308 ++++++++ mvnw.cmd | 205 ++++++ pom.xml | 167 +++++ .../workutils/WorkUtilsAppliction.java | 252 +++++++ .../biz/git/config/GitOptimizationConfig.java | 272 +++++++ .../biz/git/model/GitCommandDTO.java | 59 ++ .../biz/git/service/GitCommandStrategy.java | 19 + .../workutils/biz/git/service/GitService.java | 49 ++ .../biz/git/service/impl/GitServiceImpl.java | 178 +++++ .../git/service/impl/GitStrategyFactory.java | 64 ++ .../strategy/CheckBranchChangesStrategy.java | 125 ++++ .../strategy/CheckoutAndPullStrategy.java | 154 ++++ .../biz/git/util/GitCommandController.java | 317 ++++++++ .../biz/git/util/GitCommandExecutor.java | 658 +++++++++++++++++ .../biz/git/util/GitRepositoryManager.java | 688 ++++++++++++++++++ .../biz/git/util/GitResultCache.java | 268 +++++++ .../mybatis/export/MyBatisExportService.java | 411 +++++++++++ .../biz/mybatis/model/MybatisTagBO.java | 63 ++ .../biz/mybatis/service/XMLMapperChecker.java | 49 ++ .../workutils/biz/output/OutputService.java | 249 +++++++ .../biz/scanner/config/ScanConfigBO.java | 60 ++ .../scanner/service/FileOperationService.java | 496 +++++++++++++ .../biz/scanner/service/ScanService.java | 349 +++++++++ .../workutils/config/WorkUtilsProperties.java | 108 +++ .../workutils/menu/api/MenuRunner.java | 171 +++++ .../menu/handler/AbstractMenuHandler.java | 116 +++ .../workutils/menu/handler/BatchHandler.java | 197 +++++ .../menu/handler/GitHealthHandler.java | 240 ++++++ .../menu/handler/MyBatisHandler.java | 173 +++++ .../workutils/menu/handler/ScanHandler.java | 107 +++ .../workutils/menu/model/MenuGroup.java | 68 ++ .../workutils/menu/model/MenuItem.java | 30 + src/main/resources/application.yml | 16 + .../resources/config/代码仓库项目路径.txt | 72 ++ src/main/resources/logback.xml | 15 + 项目说明.md | 166 +++++ 38 files changed, 6941 insertions(+) create mode 100644 .mvn/wrapper/maven-wrapper.jar create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 src/main/java/com/example/workutils/WorkUtilsAppliction.java create mode 100644 src/main/java/com/example/workutils/biz/git/config/GitOptimizationConfig.java create mode 100644 src/main/java/com/example/workutils/biz/git/model/GitCommandDTO.java create mode 100644 src/main/java/com/example/workutils/biz/git/service/GitCommandStrategy.java create mode 100644 src/main/java/com/example/workutils/biz/git/service/GitService.java create mode 100644 src/main/java/com/example/workutils/biz/git/service/impl/GitServiceImpl.java create mode 100644 src/main/java/com/example/workutils/biz/git/service/impl/GitStrategyFactory.java create mode 100644 src/main/java/com/example/workutils/biz/git/service/impl/strategy/CheckBranchChangesStrategy.java create mode 100644 src/main/java/com/example/workutils/biz/git/service/impl/strategy/CheckoutAndPullStrategy.java create mode 100644 src/main/java/com/example/workutils/biz/git/util/GitCommandController.java create mode 100644 src/main/java/com/example/workutils/biz/git/util/GitCommandExecutor.java create mode 100644 src/main/java/com/example/workutils/biz/git/util/GitRepositoryManager.java create mode 100644 src/main/java/com/example/workutils/biz/git/util/GitResultCache.java create mode 100644 src/main/java/com/example/workutils/biz/mybatis/export/MyBatisExportService.java create mode 100644 src/main/java/com/example/workutils/biz/mybatis/model/MybatisTagBO.java create mode 100644 src/main/java/com/example/workutils/biz/mybatis/service/XMLMapperChecker.java create mode 100644 src/main/java/com/example/workutils/biz/output/OutputService.java create mode 100644 src/main/java/com/example/workutils/biz/scanner/config/ScanConfigBO.java create mode 100644 src/main/java/com/example/workutils/biz/scanner/service/FileOperationService.java create mode 100644 src/main/java/com/example/workutils/biz/scanner/service/ScanService.java create mode 100644 src/main/java/com/example/workutils/config/WorkUtilsProperties.java create mode 100644 src/main/java/com/example/workutils/menu/api/MenuRunner.java create mode 100644 src/main/java/com/example/workutils/menu/handler/AbstractMenuHandler.java create mode 100644 src/main/java/com/example/workutils/menu/handler/BatchHandler.java create mode 100644 src/main/java/com/example/workutils/menu/handler/GitHealthHandler.java create mode 100644 src/main/java/com/example/workutils/menu/handler/MyBatisHandler.java create mode 100644 src/main/java/com/example/workutils/menu/handler/ScanHandler.java create mode 100644 src/main/java/com/example/workutils/menu/model/MenuGroup.java create mode 100644 src/main/java/com/example/workutils/menu/model/MenuItem.java create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/config/代码仓库项目路径.txt create mode 100644 src/main/resources/logback.xml create mode 100644 项目说明.md diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..cb28b0e37c7d206feb564310fdeec0927af4123a GIT binary patch literal 62547 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 A?*IS* literal 0 HcmV?d00001 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..5f0536e --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.5/apache-maven-3.9.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..66df285 --- /dev/null +++ b/mvnw @@ -0,0 +1,308 @@ +#!/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 +# +# https://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.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..95ba6f5 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,205 @@ +@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 https://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.2.0 +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %WRAPPER_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file +SET WRAPPER_SHA_256_SUM="" +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B +) +IF NOT %WRAPPER_SHA_256_SUM%=="" ( + powershell -Command "&{"^ + "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^ + "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^ + " Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^ + " Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^ + " Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^ + " exit 1;"^ + "}"^ + "}" + if ERRORLEVEL 1 goto error +) + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f2460d6 --- /dev/null +++ b/pom.xml @@ -0,0 +1,167 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + com.example + workutils + 0.0.1-SNAPSHOT + workutils + workutils + + 8 + 8 + 8 + + + + + org.mybatis + mybatis + 3.5.7 + + + + com.github.jsqlparser + jsqlparser + 4.5 + + + + xerces + xercesImpl + 2.12.0 + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-logging + + + + + + com.opencsv + opencsv + 5.2 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + commons-io + commons-io + 2.11.0 + + + + org.apache.dubbo + dubbo + 2.7.12 + + + + + org.apache.commons + commons-csv + 1.9.0 + + + + + org.eclipse.jgit + org.eclipse.jgit + 6.8.0.202311291450-r + + + + org.projectlombok + lombok + + + + com.google.guava + guava + 33.0.0-jre + + + + org.dom4j + dom4j + 2.1.3 + + + + jaxen + jaxen + 1.2.0 + + + + com.alibaba + fastjson + 1.2.83 + + + + ch.qos.logback + logback-classic + 1.2.12 + + + + com.alibaba + easyexcel + 3.1.1 + + + + redis.clients + jedis + 3.7.0 + + + + org.apache.poi + poi-ooxml + 5.2.3 + + + org.apache.logging.log4j + log4j-core + + + org.slf4j + slf4j-log4j12 + + + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/src/main/java/com/example/workutils/WorkUtilsAppliction.java b/src/main/java/com/example/workutils/WorkUtilsAppliction.java new file mode 100644 index 0000000..054f5ed --- /dev/null +++ b/src/main/java/com/example/workutils/WorkUtilsAppliction.java @@ -0,0 +1,252 @@ +package com.example.workutils; + +import com.example.workutils.config.WorkUtilsProperties; +import com.example.workutils.menu.api.MenuRunner; +import com.example.workutils.menu.handler.GitHealthHandler; +import com.example.workutils.biz.mybatis.export.MyBatisExportService; +import com.example.workutils.biz.scanner.service.ScanService; +import com.example.workutils.biz.git.service.GitService; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) +@Component +@Slf4j +public class WorkUtilsAppliction implements CommandLineRunner { + + @Autowired + private MyBatisExportService myBatisExportService; + + @Autowired + private ScanService scanService; + + @Autowired + private GitService gitService; + + @Autowired + private MenuRunner menuRunner; + + @Autowired + private GitHealthHandler gitHealthHandler; + + @Autowired + private WorkUtilsProperties properties; + + public static void main(String[] args) { + SpringApplication.run(WorkUtilsAppliction.class, args); + } + + @Override + public void run(String... args) throws Exception { + if (args.length == 0) { + showInteractiveMenu(); + } else { + processCommand(args); + } + } + + private void showInteractiveMenu() { + Scanner scanner = new Scanner(System.in); + menuRunner.run(scanner); + } + + private void processCommand(String... args) throws Exception { + if (args.length < 1) { + showUsage(); + return; + } + + String command = args[0].toLowerCase(); + + switch (command) { + case "sql": + case "mysbql": + handleSqlCommand(args); + break; + case "scan": + handleScanCommand(args); + break; + default: + System.err.println("未知命令:" + command); + showUsage(); + } + } + + private void handleSqlCommand(String[] args) throws Exception { + if (args.length > 1) { + String subCommand = args[1].toLowerCase(); + if ("table".equals(subCommand) && args.length > 2) { + // Export SQL by table name + myBatisExportService.exportSQLByTable(args[2]); + System.out.println("表名SQL导出完成!"); + } else if ("tables".equals(subCommand) && args.length > 2) { + // Export SQL by multiple table names + List tableNames = new ArrayList<>(); + for (int i = 2; i < args.length; i++) { + tableNames.add(args[i]); + } + myBatisExportService.exportSQLByTables(tableNames); + System.out.println("多表SQL导出完成!"); + } else { + // Default: export all SQL + myBatisExportService.exportSQL(); + System.out.println("所有SQL导出完成!"); + } + } else { + // Default: export all SQL + myBatisExportService.exportSQL(); + System.out.println("所有SQL导出完成!"); + } + } + + private void handleScanCommand(String[] args) throws Exception { + if (args.length < 3) { + System.err.println("扫描命令格式: scan [outputfile]"); + return; + } + + String type = args[1].toLowerCase(); + String pattern = args[2]; + String outputFile = args.length > 3 ? args[3] : "scan_result.csv"; + + switch (type) { + case "keyword": + scanService.scanKeywords(pattern, outputFile); + break; + case "redis": + scanService.scanRedisKeys(pattern, outputFile); + break; + case "dubbo": + scanService.scanDubboInterfaces(pattern, outputFile); + break; + default: + System.err.println("未知扫描类型:" + type); + return; + } + + System.out.println("扫描完成,结果已保存到:" + outputFile); + } + + private void handleGitManagement(Scanner scanner) { + while (true) { + System.out.println("\n=== Git管理 ==="); + System.out.println("1. 切换并拉取最新代码"); + System.out.println("2. 检查分支变更情况"); + System.out.println("3. Git仓库健康检测"); + System.out.println("0. 返回上级菜单"); + System.out.print("请选择选项:"); + + try { + int choice = scanner.nextInt(); + scanner.nextLine(); // 消费换行符 + + switch (choice) { + case 1: + handleGitCheckoutAndPull(scanner); + break; + case 2: + handleCheckBranchChanges(scanner); + break; + case 3: + gitHealthHandler.execute(scanner); + break; + case 0: + return; + default: + System.out.println("无效选项,请重新选择。"); + } + } catch (Exception e) { + log.error("Git管理操作失败", e); + System.out.println("操作失败:" + e.getMessage()); + scanner.nextLine(); // 清除错误输入 + } + } + } + + private void handleCheckBranchChanges(Scanner scanner) { + System.out.println("\n=== 检查分支变更情况 ==="); + System.out.print("请输入仓库根目录路径(默认:D:\\bestpay-srdcould):"); + String repoRoot = scanner.nextLine().trim(); + if (StringUtils.isBlank(repoRoot)) { + repoRoot = "D:/bestpay-srdcloud"; + } + + System.out.println("可用的分支对配置:"); + System.out.println("1. origin/DEV-XQYFGL-96408,origin/SIT-20240829"); + System.out.println("2. origin/DEV_XQYFGL96408,origin/SIT-20240829"); + System.out.println("3. origin/SIT-20240829,origin/master"); + System.out.print("请选择或输入自定义分支对 (格式: 源分支,目标分支):"); + String branchPairsInput = scanner.nextLine().trim(); + + String branchPairs; + switch (branchPairsInput) { + case "1": + branchPairs = "origin/DEV-XQYFGL-96408,origin/SIT-20240829"; + break; + case "2": + branchPairs = "origin/DEV_XQYFGL96408,origin/SIT-20240829"; + break; + case "3": + branchPairs = "origin/SIT-20240829,origin/master"; + break; + default: + branchPairs = branchPairsInput; + break; + } + + if (branchPairs.isEmpty()) { + System.out.println("未提供分支对信息,操作取消。"); + return; + } + + System.out.println("正在执行检查,请稍候..."); + gitService.checkBranchChanges(repoRoot, branchPairs); + } + + private void handleGitCheckoutAndPull(Scanner scanner) { + System.out.println("\n=== 切换并拉取最新代码 ==="); + System.out.print("请输入仓库根目录路径(默认:D:\\bestpay-srdcould):"); + String repoRoot = scanner.nextLine().trim(); + if (StringUtils.isBlank(repoRoot)) { + repoRoot = "D:/bestpay-srdcloud"; + } + + System.out.print("请输入要拉取的分支名称:(默认:master):"); + String targetBranch = scanner.nextLine().trim(); + + if (targetBranch.isEmpty()) { + targetBranch = "master"; + } + + System.out.println("正在执行分支切换和拉取,请稍候..."); + gitService.checkoutAndPull(repoRoot, targetBranch); + } + + + private void showUsage() { + System.out.println("WorkUtils 使用说明:"); + System.out.println(""); + System.out.println("交互模式(无参数):"); + System.out.println(" java -jar workutils.jar"); + System.out.println(""); + System.out.println("命令行模式:"); + System.out.println(" sql [all|incremental] # SQL导出"); + System.out.println(" scan [output] # 扫描功能"); + System.out.println(" batch # 批量处理"); + System.out.println(""); + System.out.println("示例:"); + System.out.println(" java -jar workutils.jar sql # 导出所有SQL"); + System.out.println(" java -jar workutils.jar scan keyword \"TODO\" # 扫描TODO关键词"); + System.out.println(" java -jar workutils.jar batch # 执行所有检查"); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/git/config/GitOptimizationConfig.java b/src/main/java/com/example/workutils/biz/git/config/GitOptimizationConfig.java new file mode 100644 index 0000000..cf893a1 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/git/config/GitOptimizationConfig.java @@ -0,0 +1,272 @@ +package com.example.workutils.biz.git.config; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +/** + * Git操作优化配置类 + * 管理所有Git相关的性能优化配置 + */ +@Data +@Slf4j +public class GitOptimizationConfig { + + // 默认配置实例 + private static GitOptimizationConfig defaultConfig = new GitOptimizationConfig(); + + // 超时配置 + private long defaultCommandTimeoutSeconds = 20; + private long minCommandTimeoutSeconds = 10; + private long maxCommandTimeoutSeconds = 300; // 5分钟 + + // 线程池配置 + private int defaultThreadPoolSize = 8; + private int maxThreadPoolSize = 32; + private int minThreadPoolSize = 1; + private boolean enableThreadPoolAutoScaling = true; + private double cpuMultiplier = 2.0; // 线程池大小 = CPU核心数 * multiplier + + // 缓存配置 + private boolean enableResultCache = true; + private long repoInfoCacheTtlMinutes = 5; + private long branchInfoCacheTtlMinutes = 3; + private long commitInfoCacheTtlMinutes = 2; + private int maxCacheEntries = 1000; + private boolean enableCacheCleanup = true; + private long cacheCleanupIntervalMinutes = 1; + + // 批处理配置 + private boolean enableBatchProcessing = true; + private int maxBatchSize = 10; // 每个仓库最多批处理的命令数 + private boolean preferBatchCommands = true; + + // Fetch优化配置 + private boolean enableSmartFetch = true; + private boolean fetchOnlyMasterByDefault = true; + private boolean enableFetchParallelism = true; + private int maxFetchParallelism = 4; + + // 性能监控配置 + private boolean enablePerformanceMonitoring = true; + private boolean logSlowOperations = true; + private long slowOperationThresholdMs = 5000; // 超过5秒的操作记录为慢操作 + private boolean enableDetailedLogging = false; + + // 网络配置 + private int maxRetries = 3; + private long retryDelayMs = 1000; + private boolean enableRetryWithBackoff = true; + private double backoffMultiplier = 2.0; + + // 仓库大小计算配置 + private boolean enableRepoSizeBasedTimeout = true; + private long timeoutIncreasePer100MbSeconds = 10; + private long maxRepoSizeForTimeoutCalculationMb = 1024; // 1GB + + /** + * 创建默认配置 + */ + public static GitOptimizationConfig createDefault() { + return new GitOptimizationConfig(); + } + + /** + * 创建高性能配置(适用于CI/CD等高频场景) + */ + public static GitOptimizationConfig createHighPerformance() { + GitOptimizationConfig config = new GitOptimizationConfig(); + + // 更大的线程池 + config.setDefaultThreadPoolSize(16); + config.setMaxThreadPoolSize(64); + config.setCpuMultiplier(4.0); + + // 更短的缓存TTL(更频繁的更新) + config.setRepoInfoCacheTtlMinutes(2); + config.setBranchInfoCacheTtlMinutes(1); + config.setCommitInfoCacheTtlMinutes(1); + + // 更大的批处理容量 + config.setMaxBatchSize(20); + + // 更激进的Fetch策略 + config.setEnableFetchParallelism(true); + config.setMaxFetchParallelism(8); + + // 更宽松的超时设置 + config.setMaxCommandTimeoutSeconds(600); // 10分钟 + + return config; + } + + /** + * 创建资源节省配置(适用于资源受限环境) + */ + public static GitOptimizationConfig createResourceConservative() { + GitOptimizationConfig config = new GitOptimizationConfig(); + + // 更小的线程池 + config.setDefaultThreadPoolSize(2); + config.setMaxThreadPoolSize(4); + config.setCpuMultiplier(1.0); + + // 更长的缓存TTL(减少网络请求) + config.setRepoInfoCacheTtlMinutes(15); + config.setBranchInfoCacheTtlMinutes(10); + config.setCommitInfoCacheTtlMinutes(5); + + // 禁用一些高级功能 + config.setEnableFetchParallelism(false); + config.setEnableDetailedLogging(false); + + // 更短的超时(快速失败) + config.setDefaultCommandTimeoutSeconds(15); + config.setMaxCommandTimeoutSeconds(60); + + return config; + } + + /** + * 验证配置的有效性 + */ + public boolean validate() { + boolean isValid = true; + + // 验证超时配置 + if (minCommandTimeoutSeconds <= 0 || maxCommandTimeoutSeconds <= minCommandTimeoutSeconds) { + log.error("Invalid timeout configuration: min={}, max={}", minCommandTimeoutSeconds, maxCommandTimeoutSeconds); + isValid = false; + } + + // 验证线程池配置 + if (minThreadPoolSize <= 0 || maxThreadPoolSize < minThreadPoolSize) { + log.error("Invalid thread pool configuration: min={}, max={}", minThreadPoolSize, maxThreadPoolSize); + isValid = false; + } + + // 验证缓存配置 + if (repoInfoCacheTtlMinutes <= 0 || branchInfoCacheTtlMinutes <= 0 || commitInfoCacheTtlMinutes <= 0) { + log.error("Invalid cache TTL configuration"); + isValid = false; + } + + // 验证批处理配置 + if (maxBatchSize <= 0) { + log.error("Invalid batch size configuration: {}", maxBatchSize); + isValid = false; + } + + return isValid; + } + + /** + * 应用配置到系统 + */ + public void apply() { + if (!validate()) { + throw new IllegalArgumentException("Invalid configuration detected"); + } + + log.info("Applying Git optimization configuration:"); + log.info("- Default timeout: {} seconds", defaultCommandTimeoutSeconds); + log.info("- Thread pool size: {} (auto-scaling: {})", defaultThreadPoolSize, enableThreadPoolAutoScaling); + log.info("- Cache enabled: {} (TTL: {}/{}/{} minutes)", enableResultCache, + repoInfoCacheTtlMinutes, branchInfoCacheTtlMinutes, commitInfoCacheTtlMinutes); + log.info("- Batch processing: {} (max batch size: {})", enableBatchProcessing, maxBatchSize); + log.info("- Smart fetch: {}", enableSmartFetch); + + // 这里可以调用实际的配置应用逻辑 + // 例如更新GitCommandExecutor和GitResultCache的配置 + } + + /** + * 获取配置摘要 + */ + public String getSummary() { + return String.format( + "GitOptimizationConfig{timeout=%ds, threadPool=%d, cache=%s, batch=%s, smartFetch=%s}", + defaultCommandTimeoutSeconds, + defaultThreadPoolSize, + enableResultCache ? "enabled" : "disabled", + enableBatchProcessing ? "enabled" : "disabled", + enableSmartFetch ? "enabled" : "disabled" + ); + } + + /** + * 复制配置 + */ + public GitOptimizationConfig copy() { + GitOptimizationConfig copy = new GitOptimizationConfig(); + + // 超时配置 + copy.setDefaultCommandTimeoutSeconds(this.defaultCommandTimeoutSeconds); + copy.setMinCommandTimeoutSeconds(this.minCommandTimeoutSeconds); + copy.setMaxCommandTimeoutSeconds(this.maxCommandTimeoutSeconds); + + // 线程池配置 + copy.setDefaultThreadPoolSize(this.defaultThreadPoolSize); + copy.setMaxThreadPoolSize(this.maxThreadPoolSize); + copy.setMinThreadPoolSize(this.minThreadPoolSize); + copy.setEnableThreadPoolAutoScaling(this.enableThreadPoolAutoScaling); + copy.setCpuMultiplier(this.cpuMultiplier); + + // 缓存配置 + copy.setEnableResultCache(this.enableResultCache); + copy.setRepoInfoCacheTtlMinutes(this.repoInfoCacheTtlMinutes); + copy.setBranchInfoCacheTtlMinutes(this.branchInfoCacheTtlMinutes); + copy.setCommitInfoCacheTtlMinutes(this.commitInfoCacheTtlMinutes); + copy.setMaxCacheEntries(this.maxCacheEntries); + copy.setEnableCacheCleanup(this.enableCacheCleanup); + copy.setCacheCleanupIntervalMinutes(this.cacheCleanupIntervalMinutes); + + // 批处理配置 + copy.setEnableBatchProcessing(this.enableBatchProcessing); + copy.setMaxBatchSize(this.maxBatchSize); + copy.setPreferBatchCommands(this.preferBatchCommands); + + // Fetch优化配置 + copy.setEnableSmartFetch(this.enableSmartFetch); + copy.setFetchOnlyMasterByDefault(this.fetchOnlyMasterByDefault); + copy.setEnableFetchParallelism(this.enableFetchParallelism); + copy.setMaxFetchParallelism(this.maxFetchParallelism); + + // 性能监控配置 + copy.setEnablePerformanceMonitoring(this.enablePerformanceMonitoring); + copy.setLogSlowOperations(this.logSlowOperations); + copy.setSlowOperationThresholdMs(this.slowOperationThresholdMs); + copy.setEnableDetailedLogging(this.enableDetailedLogging); + + // 网络配置 + copy.setMaxRetries(this.maxRetries); + copy.setRetryDelayMs(this.retryDelayMs); + copy.setEnableRetryWithBackoff(this.enableRetryWithBackoff); + copy.setBackoffMultiplier(this.backoffMultiplier); + + // 仓库大小计算配置 + copy.setEnableRepoSizeBasedTimeout(this.enableRepoSizeBasedTimeout); + copy.setTimeoutIncreasePer100MbSeconds(this.timeoutIncreasePer100MbSeconds); + copy.setMaxRepoSizeForTimeoutCalculationMb(this.maxRepoSizeForTimeoutCalculationMb); + + return copy; + } + + /** + * 获取默认配置 + */ + public static GitOptimizationConfig getDefault() { + return defaultConfig; + } + + /** + * 设置默认配置 + */ + public static void setDefault(GitOptimizationConfig config) { + if (config != null && config.validate()) { + defaultConfig = config.copy(); + log.info("Default Git optimization configuration updated: {}", config.getSummary()); + } else { + log.error("Failed to set default configuration - invalid config provided"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/git/model/GitCommandDTO.java b/src/main/java/com/example/workutils/biz/git/model/GitCommandDTO.java new file mode 100644 index 0000000..3d43682 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/git/model/GitCommandDTO.java @@ -0,0 +1,59 @@ +package com.example.workutils.biz.git.model; + +import lombok.Data; + +/** + * Git 命令传输对象 + */ +@Data +public class GitCommandDTO { + + private String repoRoot; + private String targetBranch; + private String branchPairs; + private String branches; + private String scriptPath; + private GitCommandType commandType; + + public static GitCommandDTO create(String repoRoot, String value, GitCommandType type) { + GitCommandDTO dto = new GitCommandDTO(); + dto.setRepoRoot(repoRoot); + dto.setCommandType(type); + switch (type) { + case CHECK_BRANCH_CHANGES: + dto.setBranchPairs(value); + dto.setScriptPath("src/main/resources/git/check_branch_changes.cmd"); + break; + case CHECK_RELEASE_BRANCH: + dto.setBranches(value); + dto.setScriptPath("src/main/resources/git/check_release_branch.cmd"); + break; + case GET_COMMIT_ID: + dto.setTargetBranch(value); + dto.setScriptPath("src/main/resources/git/getcommidid.cmd"); + break; + case CHECKOUT: + dto.setTargetBranch(value); + dto.setScriptPath("src/main/resources/git/git_checkout.cmd"); + break; + case CHECKOUT_AND_MERGE: + dto.setTargetBranch(value); + dto.setScriptPath("src/main/resources/git/git_checkout_and_merges.cmd"); + break; + case CHECKOUT_AND_PULL: + dto.setTargetBranch(value); + dto.setScriptPath("src/main/resources/git/git_checkout_and_pull.cmd"); + break; + } + return dto; + } + + public enum GitCommandType { + CHECK_BRANCH_CHANGES, + CHECK_RELEASE_BRANCH, + GET_COMMIT_ID, + CHECKOUT, + CHECKOUT_AND_MERGE, + CHECKOUT_AND_PULL + } +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/git/service/GitCommandStrategy.java b/src/main/java/com/example/workutils/biz/git/service/GitCommandStrategy.java new file mode 100644 index 0000000..8384120 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/git/service/GitCommandStrategy.java @@ -0,0 +1,19 @@ +package com.example.workutils.biz.git.service; + +import com.example.workutils.biz.git.model.GitCommandDTO; + +/** + * Git 命令策略接口 + */ +public interface GitCommandStrategy { + + /** + * 执行 Git 命令 + */ + void execute(GitCommandDTO commandDTO); + + /** + * 获取支持的命令类型 + */ + GitCommandDTO.GitCommandType getSupportedCommandType(); +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/git/service/GitService.java b/src/main/java/com/example/workutils/biz/git/service/GitService.java new file mode 100644 index 0000000..7b3b95d --- /dev/null +++ b/src/main/java/com/example/workutils/biz/git/service/GitService.java @@ -0,0 +1,49 @@ +package com.example.workutils.biz.git.service; + +import com.example.workutils.biz.git.util.GitCommandExecutor; + +import java.util.List; +import java.util.Scanner; + +/** + * Git 服务接口 + */ +public interface GitService { + + /** + * 检查分支变更情况 + */ + void checkBranchChanges(String repoRoot, String branchPairs); + + /** + * 切换并拉取最新代码 + */ + void checkoutAndPull(String repoRoot, String targetBranch); + + /** + * 检测所有仓库健康状态 + * @return 损坏的仓库列表 + */ + List checkCorruptedRepos(); + + /** + * 检测所有仓库健康状态(支持用户中断) + * @param scanner 用户输入扫描器(可为空) + * @return 损坏的仓库列表 + */ + List checkCorruptedRepos(Scanner scanner); + + /** + * 修复损坏的仓库(删除并重新克隆master分支) + * @param repoPath 仓库路径 + * @return 是否修复成功 + */ + boolean fixCorruptedRepo(String repoPath); + + /** + * 获取损坏仓库列表(从配置文件中读取标记为CORRUPTED的) + * @return 损坏仓库路径列表 + */ + List listCorruptedRepos(); + +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/git/service/impl/GitServiceImpl.java b/src/main/java/com/example/workutils/biz/git/service/impl/GitServiceImpl.java new file mode 100644 index 0000000..183843b --- /dev/null +++ b/src/main/java/com/example/workutils/biz/git/service/impl/GitServiceImpl.java @@ -0,0 +1,178 @@ +package com.example.workutils.biz.git.service.impl; + +import com.example.workutils.biz.git.model.GitCommandDTO; +import com.example.workutils.biz.git.service.GitService; +import com.example.workutils.biz.git.util.GitCommandExecutor; +import com.example.workutils.biz.scanner.service.FileOperationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.stream.Collectors; + +/** + * Git 服务实现类 - 使用策略模式 + */ +@Service +@RequiredArgsConstructor +@Slf4j +public class GitServiceImpl implements GitService { + + private final GitStrategyFactory strategyFactory; + + @Autowired + private FileOperationService fileOperationService; + + private static final String DEFAULT_BRANCH = "master"; + + @Override + public void checkBranchChanges(String repoRoot, String branchPairs) { + GitCommandDTO commandDTO = GitCommandDTO.create(repoRoot, branchPairs, + GitCommandDTO.GitCommandType.CHECK_BRANCH_CHANGES); + strategyFactory.executeCommand(commandDTO); + } + + @Override + public void checkoutAndPull(String repoRoot, String targetBranch) { + GitCommandDTO commandDTO = GitCommandDTO.create(repoRoot, targetBranch, + GitCommandDTO.GitCommandType.CHECKOUT_AND_PULL); + strategyFactory.executeCommand(commandDTO); + } + + @Override + public List checkCorruptedRepos() { + return checkCorruptedRepos(null); + } + + @Override + public List checkCorruptedRepos(Scanner scanner) { + List corruptedRepos = new ArrayList<>(); + List projects; + + try { + projects = fileOperationService.getProjectPathsWithStatus(); + } catch (IOException e) { + log.error("读取项目路径配置文件失败", e); + return corruptedRepos; + } + + log.info("开始检测 {} 个仓库的健康状态...", projects.size()); + System.out.println("\n========== 开始检测仓库健康状态 =========="); + + int checkedCount = 0; + int totalCount = projects.size(); + boolean isAborted = false; + + for (FileOperationService.ProjectPathInfo project : projects) { + String repoPath = project.getPath(); + String repoName = extractProjectName(repoPath); + + log.info("检测仓库: {}", repoPath); + System.out.printf("[%d/%d] 检测: %s ...%n", checkedCount + 1, totalCount, repoName); + + // 检测仓库健康状态 + GitCommandExecutor.RepoHealthResult healthResult = GitCommandExecutor.checkRepoHealth(repoPath); + checkedCount++; + + // 处理检测结果 + if (healthResult.isHealthy()) { + try { + fileOperationService.updateProjectStatus(repoPath, FileOperationService.ProjectStatus.NORMAL); + System.out.printf("[%d/%d] %s - 正常%n", checkedCount, totalCount, repoName); + } catch (IOException e) { + log.warn("更新配置文件失败: {}", repoPath, e); + } + } else { + corruptedRepos.add(healthResult); + try { + fileOperationService.updateProjectStatus(repoPath, FileOperationService.ProjectStatus.CORRUPTED); + System.out.printf("[%d/%d] %s - 损坏: %s%n", checkedCount, totalCount, repoName, healthResult.getCorruptionReason()); + } catch (IOException e) { + log.warn("更新配置文件失败: {}", repoPath, e); + } + } + + // 检查用户是否按了键(使用System.in.available()非阻塞检测) + try { + if (System.in.available() > 0) { + int ch = System.in.read(); + char inputChar = (char) ch; + if (inputChar == 'a' || inputChar == 'A') { + System.out.println("\n>> 用户中止操作"); + isAborted = true; + break; + } else { + // 消耗掉缓冲区中的其他字符 + while (System.in.available() > 0) { + System.in.read(); + } + } + } + } catch (Exception e) { + // 忽略输入检测异常 + } + } + + log.info("检测完成,共 {} 个仓库,损坏 {} 个", checkedCount, corruptedRepos.size()); + System.out.println("========== 检测完成 =========="); + System.out.println("总计: " + checkedCount + " 个仓库,损坏: " + corruptedRepos.size() + " 个"); + + return corruptedRepos; + } + + private String extractProjectName(String projectPath) { + if (projectPath == null || projectPath.isEmpty()) { + return "unknown"; + } + int lastSep = Math.max(projectPath.lastIndexOf('/'), projectPath.lastIndexOf('\\')); + return lastSep >= 0 ? projectPath.substring(lastSep + 1) : projectPath; + } + + @Override + public boolean fixCorruptedRepo(String repoPath) { + log.info("开始修复损坏的仓库: {}", repoPath); + + // 获取远程仓库URL + String remoteUrl = GitCommandExecutor.getRemoteUrl(repoPath); + if (remoteUrl == null) { + log.error("无法获取远程仓库URL: {}", repoPath); + return false; + } + log.info("远程仓库URL: {}", remoteUrl); + + // 执行克隆 + GitCommandExecutor.CommandResult result = GitCommandExecutor.cloneRepo(remoteUrl, repoPath, DEFAULT_BRANCH); + + if (result.isSuccess()) { + log.info("仓库修复成功: {}", repoPath); + // 更新配置文件状态为NORMAL + try { + fileOperationService.updateProjectStatus(repoPath, FileOperationService.ProjectStatus.NORMAL); + } catch (IOException e) { + log.warn("更新配置文件失败: {}", repoPath, e); + } + return true; + } else { + log.error("仓库修复失败: {} - {}", repoPath, result.getErrorMessage()); + return false; + } + } + + @Override + public List listCorruptedRepos() { + try { + return fileOperationService.getProjectPathsWithStatus().stream() + .filter(p -> p.getStatus() == FileOperationService.ProjectStatus.CORRUPTED) + .map(FileOperationService.ProjectPathInfo::getPath) + .collect(Collectors.toList()); + } catch (IOException e) { + log.error("读取损坏仓库列表失败", e); + return new ArrayList<>(); + } + } +} diff --git a/src/main/java/com/example/workutils/biz/git/service/impl/GitStrategyFactory.java b/src/main/java/com/example/workutils/biz/git/service/impl/GitStrategyFactory.java new file mode 100644 index 0000000..836e6e6 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/git/service/impl/GitStrategyFactory.java @@ -0,0 +1,64 @@ +package com.example.workutils.biz.git.service.impl; + +import com.example.workutils.biz.git.model.GitCommandDTO; +import com.example.workutils.biz.git.service.GitCommandStrategy; +import com.example.workutils.biz.git.service.impl.strategy.*; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Git 策略工厂类 + */ +@Component +@Slf4j +public class GitStrategyFactory { + + private final Map strategyMap = new HashMap<>(); + + public GitStrategyFactory() { + // 初始化所有策略 + registerStrategy(new CheckBranchChangesStrategy()); + registerStrategy(new CheckoutAndPullStrategy()); + log.debug("Git策略工厂已初始化,共注册{}个策略", strategyMap.size()); + } + + /** + * 注册策略 + */ + private void registerStrategy(GitCommandStrategy strategy) { + strategyMap.put(strategy.getSupportedCommandType(), strategy); + log.debug("注册Git策略: {} -> {}", + strategy.getSupportedCommandType(), + strategy.getClass().getSimpleName()); + } + + /** + * 获取策略 + */ + public GitCommandStrategy getStrategy(GitCommandDTO.GitCommandType commandType) { + GitCommandStrategy strategy = strategyMap.get(commandType); + if (strategy == null) { + throw new IllegalArgumentException("不支持的Git命令类型: " + commandType); + } + return strategy; + } + + /** + * 执行Git命令 + */ + public void executeCommand(GitCommandDTO commandDTO) { + GitCommandStrategy strategy = getStrategy(commandDTO.getCommandType()); + strategy.execute(commandDTO); + } + + /** + * 获取所有支持的命令类型 + */ + public Set getSupportedCommandTypes() { + return strategyMap.keySet(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/git/service/impl/strategy/CheckBranchChangesStrategy.java b/src/main/java/com/example/workutils/biz/git/service/impl/strategy/CheckBranchChangesStrategy.java new file mode 100644 index 0000000..38a5925 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/git/service/impl/strategy/CheckBranchChangesStrategy.java @@ -0,0 +1,125 @@ +package com.example.workutils.biz.git.service.impl.strategy; + +import com.example.workutils.biz.git.model.GitCommandDTO; +import com.example.workutils.biz.git.service.GitCommandStrategy; +import com.example.workutils.biz.git.util.GitCommandExecutor; +import com.example.workutils.biz.git.util.GitRepositoryManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 检查分支变更策略 + */ +@Component +@Slf4j +public class CheckBranchChangesStrategy implements GitCommandStrategy { + + /** + * 统一路径格式处理方法 + */ + private String normalizePath(String path) { + if (path == null || path.trim().isEmpty()) { + return path; + } + // 统一使用正斜杠,Git在Windows下也能正确处理正斜杠路径 + return path.replace("\\", "/"); + } + + /** + * 安全的路径拼接方法 + */ + private String joinPath(String basePath, String relativePath) { + return normalizePath(basePath) + "/" + relativePath; + } + + @Override + public void execute(GitCommandDTO commandDTO) { + System.out.println("正在检查分支变更情况..."); + System.out.println("仓库根目录: " + commandDTO.getRepoRoot()); + System.out.println("分支对: " + commandDTO.getBranchPairs()); + + try { + String[] branchPair = commandDTO.getBranchPairs().split(","); + if (branchPair.length != 2) { + System.out.println("错误:分支对格式不正确,应为 '源分支,目标分支'"); + return; + } + + String sourceBranch = branchPair[0].trim(); + String targetBranch = branchPair[1].trim(); + + // 获取所有仓库 + List repositories = GitRepositoryManager.getGitRepositories(commandDTO.getRepoRoot()); + + if (repositories.isEmpty()) { + System.out.println("在指定目录中未找到 Git 仓库: " + commandDTO.getRepoRoot()); + return; + } + + System.out.println("找到 " + repositories.size() + " 个仓库,开始检查..."); + + // 执行分支比较 + List results = repositories.stream() + .map(repoName -> { + // 判断根目录本身是否是Git仓库 + File rootGitDir = new File(normalizePath(commandDTO.getRepoRoot()), ".git"); + String repoPath; + if (rootGitDir.exists() && rootGitDir.isDirectory()) { + // 根目录本身就是Git仓库 + repoPath = normalizePath(commandDTO.getRepoRoot()); + } else { + // 子目录是Git仓库 - 使用安全的路径拼接 + repoPath = joinPath(commandDTO.getRepoRoot(), repoName); + } + + GitCommandExecutor.RepoResult result = new GitCommandExecutor.RepoResult(repoName, repoPath); + + try { + // 比较两个分支 + GitCommandExecutor.CommandResult compareResult = GitRepositoryManager.compareBranches( + repoPath, sourceBranch, targetBranch); + + result.setSuccess(compareResult.isSuccess()); + result.setOutputLines(compareResult.getOutputLines()); + result.setErrorLines(compareResult.getErrorLines()); + result.setErrorMessage(compareResult.getErrorMessage()); + + } catch (Exception e) { + result.setSuccess(false); + result.setErrorMessage("比较分支失败: " + e.getMessage()); + } + + return result; + }) + .collect(Collectors.toList()); + + // 保存结果到文件 + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); + String outputFile = "logs/branch_changes_" + timestamp + ".txt"; + GitRepositoryManager.saveResultsToFile(outputFile, results, "分支变更检查结果", true); + + // 显示简要结果 + System.out.println("\n=== 检查结果摘要 ==="); + long successCount = results.stream().mapToLong(r -> r.isSuccess() ? 1 : 0).sum(); + System.out.println("总仓库数: " + results.size()); + System.out.println("成功检查: " + successCount); + System.out.println("失败: " + (results.size() - successCount)); + System.out.println("详细结果已保存到: " + outputFile); + + } catch (Exception e) { + log.error("检查分支变更失败", e); + System.out.println("执行失败:" + e.getMessage()); + } + } + + @Override + public GitCommandDTO.GitCommandType getSupportedCommandType() { + return GitCommandDTO.GitCommandType.CHECK_BRANCH_CHANGES; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/git/service/impl/strategy/CheckoutAndPullStrategy.java b/src/main/java/com/example/workutils/biz/git/service/impl/strategy/CheckoutAndPullStrategy.java new file mode 100644 index 0000000..2c992b0 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/git/service/impl/strategy/CheckoutAndPullStrategy.java @@ -0,0 +1,154 @@ +package com.example.workutils.biz.git.service.impl.strategy; + +import com.example.workutils.biz.git.model.GitCommandDTO; +import com.example.workutils.biz.git.service.GitCommandStrategy; +import com.example.workutils.biz.git.util.GitCommandExecutor; +import com.example.workutils.biz.git.util.GitRepositoryManager; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.stream.Collectors; + + /** + * 切换并拉取分支策略 - 并发优化版本 + */ + @Component + @Slf4j + public class CheckoutAndPullStrategy implements GitCommandStrategy { + + private static final int DEFAULT_CONCURRENCY = 8; // 默认并发度 + + /** + * 统一路径格式处理方法 + */ + private String normalizePath(String path) { + if (path == null || path.trim().isEmpty()) { + return path; + } + // 统一使用正斜杠,Git在Windows下也能正确处理正斜杠路径 + return path.replace("\\", "/"); + } + + /** + * 安全的路径拼接方法 + */ + private String joinPath(String basePath, String relativePath) { + return normalizePath(basePath) + "/" + relativePath; + } + + @Override + public void execute(GitCommandDTO commandDTO) { + System.out.println("正在切换并拉取最新代码..."); + System.out.println("仓库根目录: " + commandDTO.getRepoRoot()); + System.out.println("目标分支: " + commandDTO.getTargetBranch()); + + try { + String targetBranch = commandDTO.getTargetBranch().trim(); + + // 获取所有仓库 + List repositories = GitRepositoryManager.getGitRepositories(commandDTO.getRepoRoot()); + + if (repositories.isEmpty()) { + System.out.println("在指定目录中未找到 Git 仓库: " + commandDTO.getRepoRoot()); + return; + } + + System.out.println("找到 " + repositories.size() + " 个仓库,开始切换并拉取..."); + + // 切换并拉取每个仓库的分支 + List results = repositories.stream() + .map(repoName -> { + // 判断根目录本身是否是Git仓库 + File rootGitDir = new File(normalizePath(commandDTO.getRepoRoot()), ".git"); + String repoPath; + if (rootGitDir.exists() && rootGitDir.isDirectory()) { + // 根目录本身就是Git仓库 + repoPath = normalizePath(commandDTO.getRepoRoot()); + } else { + // 子目录是Git仓库 - 使用安全的路径拼接 + repoPath = joinPath(commandDTO.getRepoRoot(), repoName); + } + + GitCommandExecutor.RepoResult result = new GitCommandExecutor.RepoResult(repoName, repoPath); + + try { + // 检查是否有未提交的更改 + if (GitRepositoryManager.hasUncommittedChanges(repoPath)) { + result.setSuccess(false); + result.setErrorMessage("仓库存在未提交的更改,请先提交或暂存"); + return result; + } + + // 执行分支切换并拉取 + GitCommandExecutor.CommandResult pullResult = + GitRepositoryManager.checkoutAndPull(repoPath, targetBranch); + + result.setSuccess(pullResult.isSuccess()); + result.setOutputLines(pullResult.getOutputLines()); + result.setErrorLines(pullResult.getErrorLines()); + result.setErrorMessage(pullResult.getErrorMessage()); + + // 验证操作是否成功 + if (pullResult.isSuccess()) { + String currentBranch = GitRepositoryManager.getCurrentBranch(repoPath); + if (targetBranch.equals(currentBranch)) { + result.setOutputLines(java.util.Arrays.asList( + "成功切换到分支: " + targetBranch, + "成功拉取最新代码", + "当前分支: " + currentBranch + )); + } else { + result.setSuccess(false); + result.setErrorMessage("分支切换验证失败,当前分支: " + currentBranch); + } + } + + } catch (Exception e) { + result.setSuccess(false); + result.setErrorMessage("切换并拉取分支失败: " + e.getMessage()); + } + + return result; + }) + .collect(Collectors.toList()); + + // 保存结果到文件 + String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")); + String outputFile = "logs/checkout_and_pull_" + timestamp + ".txt"; + GitRepositoryManager.saveResultsToFile(outputFile, results, "分支切换并拉取结果", true); + + // 显示简要结果 + System.out.println("\n=== 操作结果摘要 ==="); + long successCount = results.stream().mapToLong(r -> r.isSuccess() ? 1 : 0).sum(); + System.out.println("总仓库数: " + results.size()); + System.out.println("操作成功: " + successCount); + System.out.println("操作失败: " + (results.size() - successCount)); + System.out.println("详细结果已保存到: " + outputFile); + + // 显示失败的仓库 + List failedResults = results.stream() + .filter(r -> !r.isSuccess()) + .collect(Collectors.toList()); + + if (!failedResults.isEmpty()) { + System.out.println("\n=== 操作失败的仓库 ==="); + for (GitCommandExecutor.RepoResult result : failedResults) { + System.out.println(result.getRepoName() + ": " + result.getErrorMessage()); + } + } + + } catch (Exception e) { + log.error("切换并拉取分支失败", e); + System.out.println("执行失败:" + e.getMessage()); + } + } + + @Override + public GitCommandDTO.GitCommandType getSupportedCommandType() { + return GitCommandDTO.GitCommandType.CHECKOUT_AND_PULL; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/git/util/GitCommandController.java b/src/main/java/com/example/workutils/biz/git/util/GitCommandController.java new file mode 100644 index 0000000..d65a482 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/git/util/GitCommandController.java @@ -0,0 +1,317 @@ +package com.example.workutils.biz.git.util; + +import lombok.extern.slf4j.Slf4j; + +import java.util.Scanner; +import java.util.concurrent.*; +import java.util.function.Consumer; + +/** + * Git命令控制器 - 管理git命令的异步执行、跳过、中断功能 + */ +@Slf4j +public class GitCommandController { + + // 命令执行状态 + public enum CommandState { + IDLE, // 空闲 + RUNNING, // 执行中 + SKIPPED, // 已跳过 + INTERRUPTED, // 已中断 + COMPLETED // 已完成 + } + + // 用户操作类型 + public enum UserAction { + NONE, // 无操作 + SKIP, // 跳过当前 + INTERRUPT, // 中断当前 + ABORT // 中止全部 + } + + // 状态存储 + private volatile CommandState currentState = CommandState.IDLE; + private volatile UserAction pendingAction = UserAction.NONE; + private volatile Process currentProcess; + private volatile long startTime; + private volatile String currentTaskName; + + // 超时配置 + private long defaultTimeoutSeconds = 300; // 默认5分钟 + private long warningThresholdSeconds = 60; // 1分钟后开始提示 + private long checkIntervalSeconds = 5; // 每5秒检查一次用户操作 + + // 线程池 + private ExecutorService executorService; + + // 用户输入监听器 + private volatile Scanner userInputScanner; + private Thread inputListenerThread; + + public GitCommandController() { + this.executorService = Executors.newSingleThreadExecutor(); + } + + /** + * 执行可中断的命令 + * @param taskName 任务名称(用于显示) + * @param commandSupplier 执行命令的 supplier + * @param statusConsumer 状态回调(定时输出进度) + * @param scanner 用户输入扫描器(为null时不监听用户输入) + * @return 执行结果 + */ + public CompletableFuture executeInterruptible( + String taskName, + SupplierWithException commandSupplier, + Consumer statusConsumer, + Scanner scanner) { + + currentState = CommandState.RUNNING; + currentTaskName = taskName; + startTime = System.currentTimeMillis(); + pendingAction = UserAction.NONE; + this.userInputScanner = scanner; + + return CompletableFuture.supplyAsync(() -> { + try { + // 启动状态监控线程 + ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor(); + ScheduledFuture monitorFuture = monitor.scheduleAtFixedRate(() -> { + // 检查用户操作 + if (pendingAction == UserAction.SKIP) { + log.info("用户选择跳过任务: {}", taskName); + currentState = CommandState.SKIPPED; + if (currentProcess != null) { + currentProcess.destroy(); + } + } else if (pendingAction == UserAction.INTERRUPT || pendingAction == UserAction.ABORT) { + log.info("用户选择中断任务: {}", taskName); + currentState = CommandState.INTERRUPTED; + if (currentProcess != null) { + currentProcess.destroy(); + } + } + + // 输出状态 + long elapsed = (System.currentTimeMillis() - startTime) / 1000; + if (statusConsumer != null) { + statusConsumer.accept(String.format("[%s] %s (已耗时: %d秒)", + getStateIcon(), taskName, elapsed)); + } + }, warningThresholdSeconds, checkIntervalSeconds, TimeUnit.SECONDS); + + // 启动用户输入监听线程 + if (scanner != null) { + startInputListener(); + } + + try { + // 执行命令 + GitCommandExecutor.CommandResult result = commandSupplier.get(); + result.setSuccess(currentState != CommandState.INTERRUPTED && + currentState != CommandState.SKIPPED); + if (currentState == CommandState.SKIPPED) { + result.setErrorMessage("用户跳过"); + } else if (currentState == CommandState.INTERRUPTED) { + result.setErrorMessage("用户中断"); + } + currentState = CommandState.COMPLETED; + return result; + } finally { + stopInputListener(); + monitorFuture.cancel(true); + monitor.shutdown(); + } + + } catch (Exception e) { + log.error("命令执行异常: {}", taskName, e); + currentState = CommandState.INTERRUPTED; + GitCommandExecutor.CommandResult errorResult = new GitCommandExecutor.CommandResult(); + errorResult.setSuccess(false); + errorResult.setErrorMessage("执行异常: " + e.getMessage()); + return errorResult; + } + }, executorService); + } + + /** + * 启动用户输入监听线程 + */ + private void startInputListener() { + inputListenerThread = new Thread(() -> { + try { + while (currentState == CommandState.RUNNING && pendingAction == UserAction.NONE) { + if (userInputScanner != null && userInputScanner.hasNext()) { + String input = userInputScanner.nextLine().trim().toLowerCase(); + handleUserInput(input); + } + Thread.sleep(200); // 短暂休眠避免CPU占用过高 + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + inputListenerThread.setDaemon(true); + inputListenerThread.start(); + } + + /** + * 停止用户输入监听线程 + */ + private void stopInputListener() { + if (inputListenerThread != null && inputListenerThread.isAlive()) { + inputListenerThread.interrupt(); + inputListenerThread = null; + } + } + + /** + * 处理用户输入 + */ + private void handleUserInput(String input) { + switch (input) { + case "s": + case "skip": + System.out.println("\n>> 跳过当前任务: " + currentTaskName); + pendingAction = UserAction.SKIP; + if (currentProcess != null) { + currentProcess.destroy(); + } + break; + case "i": + case "interrupt": + System.out.println("\n>> 中断当前任务: " + currentTaskName); + pendingAction = UserAction.INTERRUPT; + if (currentProcess != null) { + currentProcess.destroy(); + } + break; + case "a": + case "abort": + System.out.println("\n>> 中止所有任务"); + pendingAction = UserAction.ABORT; + if (currentProcess != null) { + currentProcess.destroy(); + } + break; + case "p": + case "progress": + long elapsed = (System.currentTimeMillis() - startTime) / 1000; + System.out.printf("\n>> 当前任务: %s | 状态: %s | 已耗时: %d秒%n", + currentTaskName, getStateIcon(), elapsed); + System.out.print("请选择操作: "); + break; + default: + break; + } + } + + /** + * 设置当前进程(供外部调用) + */ + public void setCurrentProcess(Process process) { + this.currentProcess = process; + } + + /** + * 获取当前状态 + */ + public CommandState getCurrentState() { + return currentState; + } + + /** + * 跳过当前命令 + */ + public void skip() { + pendingAction = UserAction.SKIP; + } + + /** + * 中断当前命令 + */ + public void interrupt() { + pendingAction = UserAction.INTERRUPT; + } + + /** + * 中止所有任务 + */ + public void abort() { + pendingAction = UserAction.ABORT; + if (currentProcess != null) { + currentProcess.destroy(); + } + } + + /** + * 获取已耗时(秒) + */ + public long getElapsedSeconds() { + return (System.currentTimeMillis() - startTime) / 1000; + } + + /** + * 获取状态图标 + */ + private String getStateIcon() { + switch (currentState) { + case RUNNING: return "运行中"; + case SKIPPED: return "已跳过"; + case INTERRUPTED: return "已中断"; + case COMPLETED: return "已完成"; + default: return "等待中"; + } + } + + /** + * 获取操作提示信息 + */ + public static String getOperationHint() { + return "\n请选择操作:\n" + + " - 输入 's' 或 'skip': 跳过当前命令,继续下一个\n" + + " - 输入 'i' 或 'interrupt': 中断当前命令\n" + + " - 输入 'a' 或 'abort': 中断所有任务\n" + + " - 输入 'p' 或 'progress': 查看当前进度\n" + + " - 不输入: 继续等待\n"; + } + + /** + * 重置控制器状态 + */ + public void reset() { + currentState = CommandState.IDLE; + pendingAction = UserAction.NONE; + currentProcess = null; + currentTaskName = null; + } + + /** + * 检查是否应该继续 + */ + public boolean shouldContinue() { + return pendingAction != UserAction.ABORT; + } + + /** + * 关闭线程池 + */ + public void shutdown() { + executorService.shutdown(); + try { + if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + } + } catch (InterruptedException e) { + executorService.shutdownNow(); + } + } + + /** + * 带异常处理的Supplier接口 + */ + @FunctionalInterface + public interface SupplierWithException { + T get() throws Exception; + } +} diff --git a/src/main/java/com/example/workutils/biz/git/util/GitCommandExecutor.java b/src/main/java/com/example/workutils/biz/git/util/GitCommandExecutor.java new file mode 100644 index 0000000..2652bba --- /dev/null +++ b/src/main/java/com/example/workutils/biz/git/util/GitCommandExecutor.java @@ -0,0 +1,658 @@ +package com.example.workutils.biz.git.util; + +import lombok.extern.slf4j.Slf4j; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.*; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Git 命令执行器 + */ +@Slf4j +public class GitCommandExecutor { + + private static final long DEFAULT_COMMAND_TIMEOUT_SECONDS = 20; // 默认超时 + private static final int DEFAULT_THREAD_POOL_SIZE = 8; // 默认线程池大小 + private static final long MIN_COMMAND_TIMEOUT_SECONDS = 10; // 最小超时 + private static final long MAX_COMMAND_TIMEOUT_SECONDS = 300; // 最大超时(5分钟) + + // 线程池管理 + private static volatile ExecutorService executorService; + private static final Object threadPoolLock = new Object(); + + /** + * 路径格式化工具方法 - 统一路径格式 + */ + private static String normalizePath(String path) { + if (path == null || path.trim().isEmpty()) { + return path; + } + + // 统一使用正斜杠,Git在Windows下也能正确处理正斜杠路径 + return path.replace("\\", "/"); + } + + /** + * 执行 Git 命令 + * @param repoRoot 仓库根目录 + * @param commands 命令列表 + * @return 执行结果 + */ + public static CommandResult executeCommands(String repoRoot, List commands) { + // 规范化路径格式 + String normalizedRepoRoot = normalizePath(repoRoot); + log.debug("Using normalized repo path: {}", normalizedRepoRoot); + + CommandResult result = new CommandResult(); + + for (String command : commands) { + try { + CommandResult singleResult = executeCommand(normalizedRepoRoot, command); + result.getOutputLines().addAll(singleResult.getOutputLines()); + result.getErrorLines().addAll(singleResult.getErrorLines()); + + if (!singleResult.isSuccess()) { + result.setSuccess(false); + result.setErrorMessage(singleResult.getErrorMessage()); + return result; + } + } catch (Exception e) { + log.error("Failed to execute command: {}", command, e); + result.setSuccess(false); + result.setErrorMessage("Command execution failed: " + e.getMessage()); + return result; + } + } + + result.setSuccess(true); + return result; + } + + /** + * 执行单个命令 + * @param workingDir 工作目录 + * @param command 命令 + * @param processConsumer 可选的Process消费者,用于获取Process对象以便中断(可为空) + */ + private static CommandResult executeCommand(String workingDir, String command, Consumer processConsumer) throws IOException, InterruptedException { + // 再次确保路径格式正确 + String normalizedWorkingDir = normalizePath(workingDir); + log.info("Executing command in directory {}: {}", normalizedWorkingDir, command); + + ProcessBuilder processBuilder = new ProcessBuilder(); + if (isWindows()) { + processBuilder.command("cmd.exe", "/c", command); + } else { + processBuilder.command("bash", "-c", command); + } + + if (normalizedWorkingDir != null && !normalizedWorkingDir.trim().isEmpty()) { + processBuilder.directory(new File(normalizedWorkingDir)); + } + + Process process = processBuilder.start(); + + // 如果提供了Process消费者,将Process传递出去 + if (processConsumer != null) { + processConsumer.accept(process); + } + + CommandResult result = new CommandResult(); + + // 读取输出流(使用异步读取避免缓冲区满导致进程阻塞) + ExecutorService readerPool = Executors.newFixedThreadPool(2); + CountDownLatch latch = new CountDownLatch(2); + + CompletableFuture> outputFuture = new CompletableFuture<>(); + CompletableFuture> errorFuture = new CompletableFuture<>(); + + readerPool.submit(() -> { + try { + List lines = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { + String line; + while ((line = reader.readLine()) != null) { + lines.add(line); + } + } + outputFuture.complete(lines); + } catch (Exception e) { + outputFuture.completeExceptionally(e); + } finally { + latch.countDown(); + } + }); + + readerPool.submit(() -> { + try { + List lines = new ArrayList<>(); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) { + String line; + while ((line = reader.readLine()) != null) { + lines.add(line); + } + } + errorFuture.complete(lines); + } catch (Exception e) { + errorFuture.completeExceptionally(e); + } finally { + latch.countDown(); + } + }); + + long timeout = calculateDynamicTimeout(workingDir, command); + try { + // 等待进程完成或超时 + boolean finished = process.waitFor(timeout, TimeUnit.SECONDS); + + if (!finished) { + // 尝试优雅终止 + process.destroy(); + try { + Thread.sleep(2000); // 等待2秒让进程优雅退出 + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + if (process.isAlive()) { + process.destroyForcibly(); + throw new RuntimeException("Command timeout after " + timeout + " seconds"); + } + } + + // 收集输出 + result.setOutputLines(outputFuture.get()); + result.setErrorLines(errorFuture.get()); + + } catch (ExecutionException e) { + result.setSuccess(false); + result.setErrorMessage("Error reading output: " + e.getMessage()); + } finally { + latch.await(1, TimeUnit.SECONDS); + readerPool.shutdown(); + } + + int exitCode = process.exitValue(); + if (exitCode != 0) { + result.setSuccess(false); + result.setErrorMessage("Command failed with exit code " + exitCode); + } else { + result.setSuccess(true); + } + + return result; + } + + /** + * 执行单个命令(兼容旧版本) + */ + private static CommandResult executeCommand(String workingDir, String command) throws IOException, InterruptedException { + return executeCommand(workingDir, command, null); + } + + /** + * 批量执行Git命令(支持多仓库)- 并发优化版本 + */ + public static List executeGitCommandsInRepos(String reposPath, List repoNames, List gitCommands) { + return executeGitCommandsInRepos(reposPath, repoNames, gitCommands, DEFAULT_THREAD_POOL_SIZE); + } + + /** + * 批量执行Git命令(支持多仓库)- 自定义并发度 + */ + public static List executeGitCommandsInRepos(String reposPath, List repoNames, List gitCommands, int concurrency) { + // 规范化基础路径 + String normalizedReposPath = normalizePath(reposPath); + + // 计算最优线程池大小 + int optimalPoolSize = calculateOptimalThreadPoolSize(repoNames.size(), concurrency); + log.info("Using thread pool size: {} for {} repositories", optimalPoolSize, repoNames.size()); + + // 创建线程池(根据最优大小调整) + ExecutorService threadPool = Executors.newFixedThreadPool(optimalPoolSize); + + try { + // 使用并发执行 + List> futures = repoNames.stream() + .map(repoName -> CompletableFuture.supplyAsync(() -> + executeGitCommandsForRepo(normalizedReposPath, repoName, gitCommands), threadPool)) + .collect(Collectors.toList()); + + // 等待所有任务完成并收集结果 + return futures.stream() + .map(future -> { + try { + return future.get(calculateDynamicTimeout(reposPath, gitCommands.toString()), TimeUnit.SECONDS); + } catch (Exception e) { + log.error("Error executing Git command for repository", e); + RepoResult errorResult = new RepoResult("unknown", "unknown"); + errorResult.setSuccess(false); + errorResult.setErrorMessage("Execution timeout or error: " + e.getMessage()); + return errorResult; + } + }) + .collect(Collectors.toList()); + + } finally { + threadPool.shutdown(); + try { + if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) { + threadPool.shutdownNow(); + } + } catch (InterruptedException e) { + threadPool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } + + /** + * 为单个仓库执行Git命令 + */ + private static RepoResult executeGitCommandsForRepo(String normalizedReposPath, String repoName, List gitCommands) { + // 使用规范化路径拼接,确保格式统一 + String repoPath = normalizedReposPath + "/" + repoName; + RepoResult repoResult = new RepoResult(repoName, repoPath); + + if (!new File(repoPath).exists()) { + repoResult.setSuccess(false); + repoResult.setErrorMessage("Repository directory not found: " + repoPath); + return repoResult; + } + + try { + CommandResult commandResult = executeCommands(repoPath, gitCommands); + repoResult.setSuccess(commandResult.isSuccess()); + repoResult.setOutputLines(commandResult.getOutputLines()); + repoResult.setErrorLines(commandResult.getErrorLines()); + repoResult.setErrorMessage(commandResult.getErrorMessage()); + } catch (Exception e) { + repoResult.setSuccess(false); + repoResult.setErrorMessage("Failed to execute commands: " + e.getMessage()); + } + + return repoResult; + } + + /** + * 批量执行Git命令(支持多仓库)- 串行版本(保持向后兼容) + */ + public static List executeGitCommandsInReposSequential(String reposPath, List repoNames, List gitCommands) { + List results = new ArrayList<>(); + + // 规范化基础路径 + String normalizedReposPath = normalizePath(reposPath); + + for (String repoName : repoNames) { + RepoResult repoResult = executeGitCommandsForRepo(normalizedReposPath, repoName, gitCommands); + results.add(repoResult); + } + + return results; + } + + private static boolean isWindows() { + return System.getProperty("os.name").toLowerCase().contains("win"); + } + + /** + * 动态计算命令超时时间 + * 根据仓库大小、命令复杂度和网络状况调整 + */ + private static long calculateDynamicTimeout(String workingDir, String command) { + long baseTimeout = DEFAULT_COMMAND_TIMEOUT_SECONDS; + + // 根据命令复杂度调整 + if (command.contains("fetch") || command.contains("clone") || command.contains("pull")) { + baseTimeout *= 2; // 网络操作时间加倍 + } else if (command.contains("merge") || command.contains("rebase")) { + baseTimeout *= 1.5; // 复杂操作时间增加50% + } + + // 根据仓库大小调整 + try { + if (workingDir != null && !workingDir.isEmpty()) { + File repoDir = new File(workingDir); + if (repoDir.exists()) { + long repoSize = calculateDirectorySize(repoDir); + // 每100MB增加10秒超时时间 + long sizeMultiplier = repoSize / (100 * 1024 * 1024); + if (sizeMultiplier > 0) { + baseTimeout += sizeMultiplier * 10; + } + } + } + } catch (Exception e) { + log.debug("Failed to calculate repository size for timeout", e); + } + + // 确保超时时间在合理范围内 + return Math.max(MIN_COMMAND_TIMEOUT_SECONDS, Math.min(baseTimeout, MAX_COMMAND_TIMEOUT_SECONDS)); + } + + /** + * 计算目录大小(递归) + */ + private static long calculateDirectorySize(File directory) { + long size = 0; + if (directory.isFile()) { + return directory.length(); + } + + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isFile()) { + size += file.length(); + } else if (file.isDirectory() && !file.getName().equals(".git")) { + // 递归计算子目录大小,但排除.git目录 + size += calculateDirectorySize(file); + } + } + } + return size; + } + + /** + * 计算最优线程池大小 + * 考虑仓库数量、CPU核心数和用户指定的并发度 + */ + private static int calculateOptimalThreadPoolSize(int repoCount, int userConcurrency) { + int cpuCores = Runtime.getRuntime().availableProcessors(); + + // 基于CPU核心数的推荐大小(通常为CPU核心数的1-2倍) + int cpuBasedSize = cpuCores * 2; + + // 基于仓库数量的大小(不超过仓库数量) + int repoBasedSize = Math.min(repoCount, cpuBasedSize); + + // 取用户指定并发度、CPU推荐大小和仓库数量的最小值,但至少为1 + return Math.max(1, Math.min(userConcurrency, repoBasedSize)); + } + + // ==================== 仓库健康检测相关方法 ==================== + + /** + * 仓库健康检测结果 + */ + public static class RepoHealthResult { + private String repoPath; + private boolean isHealthy; + private String corruptionReason; + + public RepoHealthResult(String repoPath, boolean isHealthy) { + this.repoPath = repoPath; + this.isHealthy = isHealthy; + } + + public RepoHealthResult(String repoPath, boolean isHealthy, String corruptionReason) { + this.repoPath = repoPath; + this.isHealthy = isHealthy; + this.corruptionReason = corruptionReason; + } + + public String getRepoPath() { return repoPath; } + public boolean isHealthy() { return isHealthy; } + public String getCorruptionReason() { return corruptionReason; } + } + + /** + * 检查仓库健康状态 + * @param repoPath 仓库路径 + * @return 健康检测结果 + */ + public static RepoHealthResult checkRepoHealth(String repoPath) { + String normalizedPath = normalizePath(repoPath); + File repoDir = new File(normalizedPath); + + // 检查目录是否存在 + if (!repoDir.exists()) { + return new RepoHealthResult(repoPath, false, "仓库目录不存在"); + } + + // 检查.git目录是否存在 + File gitDir = new File(normalizedPath + "/.git"); + if (!gitDir.exists() || !gitDir.isDirectory()) { + return new RepoHealthResult(repoPath, false, ".git目录缺失或损坏"); + } + + // 检查git rev-parse是否成功 + try { + CommandResult result = executeCommand(normalizedPath, "git rev-parse --git-dir"); + if (!result.isSuccess()) { + return new RepoHealthResult(repoPath, false, "git rev-parse失败: " + result.getErrorMessage()); + } + } catch (Exception e) { + return new RepoHealthResult(repoPath, false, "执行git命令失败: " + e.getMessage()); + } + + // 检查git fsck是否成功 + try { + CommandResult result = executeCommand(normalizedPath, "git fsck --full"); + if (!result.isSuccess()) { + String errorMsg = result.getErrorMessage(); + // 如果有错误输出,说明仓库可能损坏 + if (result.getErrorLines() != null && !result.getErrorLines().isEmpty()) { + return new RepoHealthResult(repoPath, false, "fsck检测失败: " + String.join("; ", result.getErrorLines())); + } + return new RepoHealthResult(repoPath, false, "fsck检测失败: " + errorMsg); + } + } catch (Exception e) { + return new RepoHealthResult(repoPath, false, "执行fsck失败: " + e.getMessage()); + } + + // 检查git status是否正常 + try { + CommandResult result = executeCommand(normalizedPath, "git status"); + if (!result.isSuccess()) { + return new RepoHealthResult(repoPath, false, "git status失败: " + result.getErrorMessage()); + } + } catch (Exception e) { + return new RepoHealthResult(repoPath, false, "执行git status失败: " + e.getMessage()); + } + + return new RepoHealthResult(repoPath, true, null); + } + + /** + * 获取远程仓库URL + * @param repoPath 仓库路径 + * @return 远程仓库URL,失败返回null + */ + public static String getRemoteUrl(String repoPath) { + String normalizedPath = normalizePath(repoPath); + try { + CommandResult result = executeCommand(normalizedPath, "git remote -v"); + if (!result.isSuccess() || result.getOutputLines().isEmpty()) { + log.warn("获取远程仓库URL失败: {}", repoPath); + return null; + } + + // 解析fetch行的URL(第一行通常是fetch) + for (String line : result.getOutputLines()) { + if (line.contains("(fetch)")) { + // 格式: origin http://xxx/xxx.git (fetch) + String[] parts = line.trim().split("\\s+"); + if (parts.length >= 2) { + return parts[1]; + } + } + } + + // 如果没有fetch,取第一行 + return result.getOutputLines().get(0).trim().split("\\s+")[1]; + + } catch (Exception e) { + log.error("获取远程仓库URL异常: {}", repoPath, e); + return null; + } + } + + /** + * 克隆仓库 + * @param remoteUrl 远程仓库URL + * @param targetPath 目标路径 + * @param branch 分支名 + * @return 克隆结果 + */ + public static CommandResult cloneRepo(String remoteUrl, String targetPath, String branch) { + String normalizedTarget = normalizePath(targetPath); + String command = String.format("git clone -b %s %s \"%s\"", branch, remoteUrl, normalizedTarget); + log.info("执行克隆命令: {}", command); + + try { + // 先删除目标目录(如果存在) + File targetDir = new File(normalizedTarget); + if (targetDir.exists()) { + deleteDirectory(targetDir); + } + + return executeCommand(new File(normalizedTarget).getParent(), command); + } catch (Exception e) { + CommandResult result = new CommandResult(); + result.setSuccess(false); + result.setErrorMessage("克隆失败: " + e.getMessage()); + return result; + } + } + + /** + * 递归删除目录 + */ + private static void deleteDirectory(File directory) { + if (directory.exists()) { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + file.delete(); + } + } + } + directory.delete(); + } + } + + /** + * 获取或创建线程池(单例模式,线程安全) + */ + private static ExecutorService getOrCreateExecutorService() { + if (executorService == null) { + synchronized (threadPoolLock) { + if (executorService == null) { + int optimalSize = calculateOptimalThreadPoolSize(DEFAULT_THREAD_POOL_SIZE, DEFAULT_THREAD_POOL_SIZE); + executorService = Executors.newFixedThreadPool(optimalSize); + log.info("Created default thread pool with size: {}", optimalSize); + } + } + } + return executorService; + } + + /** + * 关闭线程池(用于应用程序关闭时清理资源) + */ + public static void shutdownThreadPool() { + synchronized (threadPoolLock) { + if (executorService != null && !executorService.isShutdown()) { + executorService.shutdown(); + try { + if (!executorService.awaitTermination(30, TimeUnit.SECONDS)) { + executorService.shutdownNow(); + if (!executorService.awaitTermination(15, TimeUnit.SECONDS)) { + log.warn("Thread pool did not terminate gracefully"); + } + } + } catch (InterruptedException e) { + executorService.shutdownNow(); + Thread.currentThread().interrupt(); + } + executorService = null; + log.info("Thread pool shutdown completed"); + } + } + } + + /** + * 命令执行结果 + */ + public static class CommandResult { + private boolean success; + private String errorMessage; + private List outputLines = new ArrayList<>(); + private List errorLines = new ArrayList<>(); + + public CommandResult() {} + + public CommandResult(boolean success, String errorMessage) { + this.success = success; + this.errorMessage = errorMessage; + } + + public void addOutputLine(String line) { + outputLines.add(line); + } + + public void addErrorLine(String line) { + errorLines.add(line); + } + + // Getters and setters + public boolean isSuccess() { return success; } + public void setSuccess(boolean success) { this.success = success; } + public String getErrorMessage() { return errorMessage; } + public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } + public List getOutputLines() { return outputLines; } + public void setOutputLines(List outputLines) { this.outputLines = outputLines; } + public List getErrorLines() { return errorLines; } + public void setErrorLines(List errorLines) { this.errorLines = errorLines; } + + public String getOutput() { + return String.join("\n", outputLines); + } + + public String getErrors() { + return String.join("\n", errorLines); + } + } + + /** + * 仓库执行结果 + */ + public static class RepoResult { + private String repoName; + private String repoPath; + private boolean success; + private String errorMessage; + private List outputLines = new ArrayList<>(); + private List errorLines = new ArrayList<>(); + + public RepoResult(String repoName, String repoPath) { + this.repoName = repoName; + this.repoPath = repoPath; + } + + // Getters and setters + public String getRepoName() { return repoName; } + public void setRepoName(String repoName) { this.repoName = repoName; } + public String getRepoPath() { return repoPath; } + public void setRepoPath(String repoPath) { this.repoPath = repoPath; } + public boolean isSuccess() { return success; } + public void setSuccess(boolean success) { this.success = success; } + public String getErrorMessage() { return errorMessage; } + public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } + public List getOutputLines() { return outputLines; } + public void setOutputLines(List outputLines) { this.outputLines = outputLines; } + public List getErrorLines() { return errorLines; } + public void setErrorLines(List errorLines) { this.errorLines = errorLines; } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/git/util/GitRepositoryManager.java b/src/main/java/com/example/workutils/biz/git/util/GitRepositoryManager.java new file mode 100644 index 0000000..443e2d3 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/git/util/GitRepositoryManager.java @@ -0,0 +1,688 @@ +package com.example.workutils.biz.git.util; + +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * Git 仓库管理器 + */ +@Slf4j +public class GitRepositoryManager { + + /** + * 获取指定目录下的所有 Git 仓库 + */ + public static List getGitRepositories(String repoRoot) { + List repositories = new ArrayList<>(); + File rootDir = new File(repoRoot); + + if (!rootDir.exists() || !rootDir.isDirectory()) { + log.warn("Repository root directory does not exist: {}", repoRoot); + return repositories; + } + + // 首先检查根目录本身是否是Git仓库 + File rootGitDir = new File(rootDir, ".git"); + if (rootGitDir.exists() && rootGitDir.isDirectory()) { + repositories.add(rootDir.getName()); + log.info("Found Git repository at root: {}", repoRoot); + return repositories; + } + + // 如果根目录不是Git仓库,则检查子目录 + File[] subDirs = rootDir.listFiles(File::isDirectory); + if (subDirs == null) { + return repositories; + } + + for (File subDir : subDirs) { + File gitDir = new File(subDir, ".git"); + if (gitDir.exists() && gitDir.isDirectory()) { + repositories.add(subDir.getName()); + } + } + + log.info("Found {} Git repositories in {}", repositories.size(), repoRoot); + return repositories; + } + + /** + * 验证分支是否存在 + */ + public static boolean branchExists(String repoPath, String branchName) { + GitCommandExecutor.CommandResult result = GitCommandExecutor.executeCommands( + repoPath, + Arrays.asList("git branch --all") + ); + + if (result.isSuccess()) { + return result.getOutput().contains(branchName) || + result.getOutput().contains("remotes/origin/" + branchName); + } + + return false; + } + + /** + * 获取当前分支 + */ + public static String getCurrentBranch(String repoPath) { + GitCommandExecutor.CommandResult result = GitCommandExecutor.executeCommands( + repoPath, + Arrays.asList("git rev-parse --abbrev-ref HEAD") + ); + + if (result.isSuccess()) { + return result.getOutput().trim(); + } + + return null; + } + + /** + * 检查是否有未提交的更改 + */ + public static boolean hasUncommittedChanges(String repoPath) { + GitCommandExecutor.CommandResult result = GitCommandExecutor.executeCommands( + repoPath, + Arrays.asList("git status --porcelain") + ); + + if (result.isSuccess()) { + return !result.getOutput().trim().isEmpty(); + } + + return true; // 出错时假设有未提交的更改 + } + + /** + * 智能fetch远程分支 - 优化版本,默认只fetch master分支 + */ + private static GitCommandExecutor.CommandResult smartFetch(String repoPath, String branchName) { + List commands = Arrays.asList("git fetch origin master"); + + // 如果不是master分支且需要更多信息,再fetch特定分支 + if (branchName != null && !"master".equals(branchName) && !"origin/master".equals(branchName)) { + commands = Arrays.asList("git fetch origin " + branchName); + } + + return GitCommandExecutor.executeCommands(repoPath, commands); + } + + /** + * Fetch多个分支(用于分支比较等场景) + */ + private static GitCommandExecutor.CommandResult fetchBranches(String repoPath, String... branchNames) { + List commands = new ArrayList<>(); + commands.add("git fetch origin master"); // 总是fetch master + + // 去重并添加其他分支 + Set branchesToFetch = new HashSet<>(); + for (String branch : branchNames) { + if (branch != null && !branch.isEmpty()) { + // 移除origin/前缀 + String cleanBranch = branch.replace("origin/", ""); + if (!"master".equals(cleanBranch) && !branchesToFetch.contains(cleanBranch)) { + branchesToFetch.add(cleanBranch); + } + } + } + + for (String branch : branchesToFetch) { + commands.add("git fetch origin " + branch); + } + + return GitCommandExecutor.executeCommands(repoPath, commands); + } + + /** + * 获取最新提交的 Commit ID - 优化版本,使用缓存和命令批处理 + */ + public static String getLatestCommitId(String repoPath, String branchName) { + String cleanBranch = branchName.replace("origin/", ""); + + // 先检查缓存 + GitResultCache.CommitInfo cachedInfo = GitResultCache.getCommitInfo(repoPath, cleanBranch); + if (cachedInfo != null && cachedInfo.getCommitId() != null) { + log.debug("Commit ID for {} retrieved from cache: {}", cleanBranch, cachedInfo.getCommitId()); + return cachedInfo.getCommitId(); + } + + // 使用智能fetch和批处理命令 + List commands = new ArrayList<>(); + commands.add("git fetch origin " + (cleanBranch.equals("master") ? "master" : cleanBranch)); + commands.add("git rev-parse " + branchName); + + GitCommandExecutor.CommandResult result = GitCommandExecutor.executeCommands(repoPath, commands); + + String commitId = null; + if (result.isSuccess()) { + String output = result.getOutput().trim(); + String[] lines = output.split("\n"); + // 返回最后一行(commit ID),跳过fetch输出 + for (int i = lines.length - 1; i >= 0; i--) { + String line = lines[i].trim(); + if (!line.isEmpty() && !line.startsWith("From ")) { + commitId = line; + break; + } + } + } + + // 将结果存入缓存 + if (commitId != null) { + GitResultCache.putCommitInfo(repoPath, cleanBranch, commitId); + log.debug("Commit ID for {} cached: {}", cleanBranch, commitId); + } + + return commitId; + } + + /** + * 比较两个分支的差异 - 优化版本 + */ + public static GitCommandExecutor.CommandResult compareBranches(String repoPath, String sourceBranch, String targetBranch) { + // Fetch两个相关分支 + GitCommandExecutor.CommandResult fetchResult = fetchBranches(repoPath, sourceBranch, targetBranch); + if (!fetchResult.isSuccess()) { + log.warn("Fetch failed for branch comparison: {}", fetchResult.getErrorMessage()); + } + + List commands = Arrays.asList("git log --oneline " + sourceBranch + ".." + targetBranch); + return GitCommandExecutor.executeCommands(repoPath, commands); + } + + /** + * 切换分支 - 优化版本 + */ + public static GitCommandExecutor.CommandResult checkoutBranch(String repoPath, String branchName) { + String cleanBranch = branchName.replace("origin/", ""); + + // 智能fetch + GitCommandExecutor.CommandResult fetchResult = smartFetch(repoPath, cleanBranch); + if (!fetchResult.isSuccess()) { + log.warn("Fetch failed for branch {}: {}", branchName, fetchResult.getErrorMessage()); + } + + List commands = Arrays.asList("git checkout " + cleanBranch); + return GitCommandExecutor.executeCommands(repoPath, commands); + } + + /** + * 切换并合并分支 - 优化版本 + */ + public static GitCommandExecutor.CommandResult checkoutAndMerge(String repoPath, String branchName) { + String cleanBranch = branchName.replace("origin/", ""); + + // 智能fetch + GitCommandExecutor.CommandResult fetchResult = smartFetch(repoPath, cleanBranch); + if (!fetchResult.isSuccess()) { + log.warn("Fetch failed for branch {}: {}", branchName, fetchResult.getErrorMessage()); + } + + List commands = Arrays.asList( + "git checkout " + cleanBranch, + "git merge origin/" + cleanBranch + ); + + return GitCommandExecutor.executeCommands(repoPath, commands); + } + + /** + * 切换并拉取最新代码 - 优化版本 + */ + public static GitCommandExecutor.CommandResult checkoutAndPull(String repoPath, String branchName) { + String cleanBranch = branchName.replace("origin/", ""); + + // 智能fetch + GitCommandExecutor.CommandResult fetchResult = smartFetch(repoPath, cleanBranch); + if (!fetchResult.isSuccess()) { + log.warn("Fetch failed for branch {}: {}", branchName, fetchResult.getErrorMessage()); + } + + List commands = Arrays.asList( + "git checkout " + cleanBranch, + "git pull origin " + cleanBranch + ); + + return GitCommandExecutor.executeCommands(repoPath, commands); + } + + /** + * 批量获取多个仓库的状态信息 - 优化版本,使用并发和缓存 + */ + public static List batchGetRepositoryStatus(String reposPath, List repoNames) { + List results = new ArrayList<>(); + + // 规范化基础路径 + String normalizedReposPath = reposPath.replace("\\", "/"); + + // 使用并发获取仓库状态 + List> futures = repoNames.stream() + .map(repoName -> CompletableFuture.supplyAsync(() -> { + String repoPath = normalizedReposPath + "/" + repoName; + RepositoryStatus status = new RepositoryStatus(repoName, repoPath); + + if (!new File(repoPath).exists()) { + status.setError("Repository directory not found: " + repoPath); + return status; + } + + try { + // 批量获取仓库信息(减少命令调用次数) + List commands = Arrays.asList( + "git status --porcelain", + "git rev-parse --abbrev-ref HEAD", + "git branch --all" + ); + + GitCommandExecutor.CommandResult result = GitCommandExecutor.executeCommands(repoPath, commands); + if (result.isSuccess()) { + String output = result.getOutput(); + String[] lines = output.split("\n"); + + // 解析输出 + boolean hasUncommittedChanges = false; + String currentBranch = null; + List branches = new ArrayList<>(); + + boolean statusSection = true; // 第一部分是status输出 + for (String line : lines) { + line = line.trim(); + if (line.isEmpty()) continue; + + if (statusSection && !line.startsWith("fatal:") && !line.startsWith("error:")) { + if (!line.isEmpty() && !line.startsWith("*") && !line.startsWith("remotes/")) { + hasUncommittedChanges = true; + } + } else if (line.startsWith("*") || line.startsWith("remotes/")) { + statusSection = false; + if (line.startsWith("*")) { + currentBranch = line.substring(2).trim(); + } + branches.add(line); + } else if (!statusSection && !line.startsWith("fatal:") && !line.startsWith("error:")) { + branches.add(line); + } + } + + status.setHasUncommittedChanges(hasUncommittedChanges); + status.setCurrentBranch(currentBranch); + status.setAvailableBranches(branches); + + // 更新缓存 + GitResultCache.putRepoInfo(repoPath, hasUncommittedChanges, currentBranch); + } else { + status.setError(result.getErrorMessage()); + } + } catch (Exception e) { + status.setError("Failed to get repository status: " + e.getMessage()); + } + + return status; + })) + .collect(Collectors.toList()); + + // 等待所有任务完成 + for (CompletableFuture future : futures) { + try { + results.add(future.get(30, TimeUnit.SECONDS)); + } catch (Exception e) { + log.error("Error getting repository status", e); + RepositoryStatus errorStatus = new RepositoryStatus("unknown", "unknown"); + errorStatus.setError("Execution timeout or error: " + e.getMessage()); + results.add(errorStatus); + } + } + + return results; + } + + /** + * 批量执行多个Git操作 - 高级批处理优化 + */ + public static BatchOperationResult batchExecuteGitOperations(String reposPath, List repoNames, List operations) { + BatchOperationResult batchResult = new BatchOperationResult(); + + // 按仓库分组操作 + Map> repoOperations = new ConcurrentHashMap<>(); + for (BatchGitOperation operation : operations) { + if (repoNames.contains(operation.getRepoName())) { + repoOperations.computeIfAbsent(operation.getRepoName(), k -> new ArrayList<>()).add(operation); + } + } + + // 并发执行每个仓库的操作 + List> futures = repoOperations.entrySet().stream() + .map(entry -> CompletableFuture.supplyAsync(() -> { + String repoName = entry.getKey(); + List ops = entry.getValue(); + + String repoPath = reposPath.replace("\\", "/") + "/" + repoName; + RepositoryOperationResult repoResult = new RepositoryOperationResult(repoName, repoPath); + + if (!new File(repoPath).exists()) { + repoResult.setError("Repository directory not found: " + repoPath); + return repoResult; + } + + // 合并可以批处理的命令 + List batchCommands = new ArrayList<>(); + List individualCommands = new ArrayList<>(); + + for (BatchGitOperation op : ops) { + if (op.canBatch()) { + batchCommands.addAll(op.getCommands()); + } else { + individualCommands.addAll(op.getCommands()); + } + } + + try { + // 先执行批处理命令 + if (!batchCommands.isEmpty()) { + GitCommandExecutor.CommandResult batchCmdResult = GitCommandExecutor.executeCommands(repoPath, batchCommands); + if (!batchCmdResult.isSuccess()) { + repoResult.setError("Batch commands failed: " + batchCmdResult.getErrorMessage()); + return repoResult; + } + repoResult.addBatchResult(batchCommands, batchCmdResult); + } + + // 再执行个别命令 + for (String command : individualCommands) { + GitCommandExecutor.CommandResult result = GitCommandExecutor.executeCommands(repoPath, Arrays.asList(command)); + repoResult.addIndividualResult(command, result); + } + + // 清理相关缓存 + GitResultCache.clearRepoCache(repoPath); + + } catch (Exception e) { + repoResult.setError("Failed to execute operations: " + e.getMessage()); + } + + return repoResult; + })) + .collect(Collectors.toList()); + + // 收集结果 + for (CompletableFuture future : futures) { + try { + batchResult.addRepositoryResult(future.get(60, TimeUnit.SECONDS)); + } catch (Exception e) { + log.error("Error executing batch operations", e); + RepositoryOperationResult errorResult = new RepositoryOperationResult("unknown", "unknown"); + errorResult.setError("Execution timeout or error: " + e.getMessage()); + batchResult.addRepositoryResult(errorResult); + } + } + + return batchResult; + } + + /** + * 仓库状态信息类 + */ + public static class RepositoryStatus { + private final String repoName; + private final String repoPath; + private boolean hasUncommittedChanges; + private String currentBranch; + private List availableBranches = new ArrayList<>(); + private String error; + + public RepositoryStatus(String repoName, String repoPath) { + this.repoName = repoName; + this.repoPath = repoPath; + } + + public boolean hasError() { + return error != null && !error.isEmpty(); + } + + // Getters and setters + public String getRepoName() { return repoName; } + public String getRepoPath() { return repoPath; } + public boolean hasUncommittedChanges() { return hasUncommittedChanges; } + public void setHasUncommittedChanges(boolean hasUncommittedChanges) { this.hasUncommittedChanges = hasUncommittedChanges; } + public String getCurrentBranch() { return currentBranch; } + public void setCurrentBranch(String currentBranch) { this.currentBranch = currentBranch; } + public List getAvailableBranches() { return availableBranches; } + public void setAvailableBranches(List availableBranches) { this.availableBranches = availableBranches; } + public String getError() { return error; } + public void setError(String error) { this.error = error; } + } + + /** + * 批量Git操作类 + */ + public static class BatchGitOperation { + private final String repoName; + private final String operationType; + private final List commands; + private final boolean canBatch; + + public BatchGitOperation(String repoName, String operationType, List commands, boolean canBatch) { + this.repoName = repoName; + this.operationType = operationType; + this.commands = new ArrayList<>(commands); + this.canBatch = canBatch; + } + + // 创建常用的批量操作 + public static BatchGitOperation checkoutAndPull(String repoName, String branchName) { + return new BatchGitOperation(repoName, "checkout_pull", + Arrays.asList("git fetch origin " + branchName, "git checkout " + branchName, "git pull origin " + branchName), + true); + } + + public static BatchGitOperation getCommitId(String repoName, String branchName) { + return new BatchGitOperation(repoName, "get_commit", + Arrays.asList("git fetch origin " + branchName, "git rev-parse " + branchName), + true); + } + + public static BatchGitOperation getStatus(String repoName) { + return new BatchGitOperation(repoName, "get_status", + Arrays.asList("git status --porcelain", "git rev-parse --abbrev-ref HEAD"), + false); + } + + // Getters + public String getRepoName() { return repoName; } + public String getOperationType() { return operationType; } + public List getCommands() { return new ArrayList<>(commands); } + public boolean canBatch() { return canBatch; } + } + + /** + * 仓库操作结果类 + */ + public static class RepositoryOperationResult { + private final String repoName; + private final String repoPath; + private String error; + private List batchResults = new ArrayList<>(); + private List individualResults = new ArrayList<>(); + + public RepositoryOperationResult(String repoName, String repoPath) { + this.repoName = repoName; + this.repoPath = repoPath; + } + + public void addBatchResult(List commands, GitCommandExecutor.CommandResult result) { + batchResults.add(new BatchCommandResult(commands, result)); + } + + public void addIndividualResult(String command, GitCommandExecutor.CommandResult result) { + individualResults.add(new IndividualCommandResult(command, result)); + } + + public boolean hasError() { + return error != null && !error.isEmpty(); + } + + // Getters and setters + public String getRepoName() { return repoName; } + public String getRepoPath() { return repoPath; } + public String getError() { return error; } + public void setError(String error) { this.error = error; } + public List getBatchResults() { return batchResults; } + public List getIndividualResults() { return individualResults; } + } + + /** + * 批量命令结果类 + */ + public static class BatchCommandResult { + private final List commands; + private final GitCommandExecutor.CommandResult result; + + public BatchCommandResult(List commands, GitCommandExecutor.CommandResult result) { + this.commands = new ArrayList<>(commands); + this.result = result; + } + + // Getters + public List getCommands() { return commands; } + public GitCommandExecutor.CommandResult getResult() { return result; } + } + + /** + * 个别命令结果类 + */ + public static class IndividualCommandResult { + private final String command; + private final GitCommandExecutor.CommandResult result; + + public IndividualCommandResult(String command, GitCommandExecutor.CommandResult result) { + this.command = command; + this.result = result; + } + + // Getters + public String getCommand() { return command; } + public GitCommandExecutor.CommandResult getResult() { return result; } + } + + /** + * 批量操作结果类 + */ + public static class BatchOperationResult { + private List repositoryResults = new ArrayList<>(); + + public void addRepositoryResult(RepositoryOperationResult result) { + repositoryResults.add(result); + } + + public long getSuccessCount() { + return repositoryResults.stream().filter(r -> !r.hasError()).count(); + } + + public long getFailureCount() { + return repositoryResults.stream().filter(RepositoryOperationResult::hasError).count(); + } + + public double getSuccessRate() { + if (repositoryResults.isEmpty()) return 0.0; + return (double) getSuccessCount() / repositoryResults.size() * 100; + } + + // Getters + public List getRepositoryResults() { return repositoryResults; } + } + + /** + * 将结果保存到文件(支持并发结果) + */ + public static void saveResultsToFile(String filePath, List results, String title, boolean isConcurrent) throws IOException { + List lines = new ArrayList<>(); + lines.add("=== " + title + " ==="); + lines.add("执行时间: " + java.time.LocalDateTime.now()); + lines.add("执行模式: " + (isConcurrent ? "并发执行" : "串行执行")); + lines.add("处理仓库数量: " + results.size()); + + // 统计结果 + long successCount = results.stream().mapToLong(r -> r.isSuccess() ? 1 : 0).sum(); + long failureCount = results.size() - successCount; + lines.add("成功数量: " + successCount); + lines.add("失败数量: " + failureCount); + lines.add("成功率: " + String.format("%.2f%%", (double) successCount / results.size() * 100)); + lines.add(""); + + // 按状态分组显示 + List successResults = results.stream() + .filter(GitCommandExecutor.RepoResult::isSuccess) + .collect(java.util.stream.Collectors.toList()); + + List failedResults = results.stream() + .filter(r -> !r.isSuccess()) + .collect(java.util.stream.Collectors.toList()); + + if (!successResults.isEmpty()) { + lines.add("=== 成功处理的仓库 ==="); + for (GitCommandExecutor.RepoResult result : successResults) { + lines.add("✓ " + result.getRepoName() + " (" + result.getRepoPath() + ")"); + } + lines.add(""); + } + + if (!failedResults.isEmpty()) { + lines.add("=== 处理失败的仓库 ==="); + for (GitCommandExecutor.RepoResult result : failedResults) { + lines.add("✗ " + result.getRepoName() + " (" + result.getRepoPath() + ")"); + lines.add(" 错误信息: " + result.getErrorMessage()); + } + lines.add(""); + } + + // 详细输出(仅在失败或需要详细信息时显示) + boolean hasDetailedOutput = results.stream().anyMatch(r -> + !r.getOutputLines().isEmpty() || !r.getErrorLines().isEmpty()); + + if (hasDetailedOutput) { + lines.add("=== 详细输出 ==="); + for (GitCommandExecutor.RepoResult result : results) { + lines.add("仓库: " + result.getRepoName()); + lines.add("路径: " + result.getRepoPath()); + lines.add("状态: " + (result.isSuccess() ? "成功" : "失败")); + + if (!result.isSuccess()) { + lines.add("错误信息: " + result.getErrorMessage()); + } + + if (!result.getOutputLines().isEmpty()) { + lines.add("输出:"); + result.getOutputLines().forEach(line -> lines.add(" " + line)); + } + + if (!result.getErrorLines().isEmpty()) { + lines.add("错误:"); + result.getErrorLines().forEach(line -> lines.add(" " + line)); + } + + lines.add("----------------------------------------"); + lines.add(""); + } + } + + Files.write(Paths.get(filePath), lines); + log.info("Results saved to: {} (Mode: {}, Success: {}/{})", + filePath, isConcurrent ? "Concurrent" : "Sequential", successCount, results.size()); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/git/util/GitResultCache.java b/src/main/java/com/example/workutils/biz/git/util/GitResultCache.java new file mode 100644 index 0000000..2fd06eb --- /dev/null +++ b/src/main/java/com/example/workutils/biz/git/util/GitResultCache.java @@ -0,0 +1,268 @@ +package com.example.workutils.biz.git.util; + +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Git结果缓存管理器 + * 用于缓存频繁访问的Git操作结果,提高执行效率 + */ +@Slf4j +public class GitResultCache { + + // 仓库信息缓存 + private static final ConcurrentHashMap repoInfoCache = new ConcurrentHashMap<>(); + + // 分支信息缓存 + private static final ConcurrentHashMap branchInfoCache = new ConcurrentHashMap<>(); + + // 提交ID缓存 + private static final ConcurrentHashMap commitInfoCache = new ConcurrentHashMap<>(); + + // 缓存过期时间(毫秒) + private static final long REPO_INFO_TTL = 5 * 60 * 1000; // 5分钟 + private static final long BRANCH_INFO_TTL = 3 * 60 * 1000; // 3分钟 + private static final long COMMIT_INFO_TTL = 2 * 60 * 1000; // 2分钟 + + // 定时清理任务 + private static final ScheduledExecutorService cleanupExecutor = Executors.newSingleThreadScheduledExecutor(); + + static { + // 每分钟清理一次过期缓存 + cleanupExecutor.scheduleAtFixedRate(GitResultCache::cleanupExpiredCache, 1, 1, TimeUnit.MINUTES); + log.info("Git result cache initialized with periodic cleanup"); + } + + /** + * 仓库信息缓存类 + */ + public static class RepoInfo { + private final String repoPath; + private final boolean hasUncommittedChanges; + private final String currentBranch; + private final long timestamp; + + public RepoInfo(String repoPath, boolean hasUncommittedChanges, String currentBranch) { + this.repoPath = repoPath; + this.hasUncommittedChanges = hasUncommittedChanges; + this.currentBranch = currentBranch; + this.timestamp = System.currentTimeMillis(); + } + + public boolean isExpired() { + return System.currentTimeMillis() - timestamp > REPO_INFO_TTL; + } + + // Getters + public String getRepoPath() { return repoPath; } + public boolean hasUncommittedChanges() { return hasUncommittedChanges; } + public String getCurrentBranch() { return currentBranch; } + public long getTimestamp() { return timestamp; } + } + + /** + * 分支信息缓存类 + */ + public static class BranchInfo { + private final String repoPath; + private final String branchName; + private final boolean exists; + private final long timestamp; + + public BranchInfo(String repoPath, String branchName, boolean exists) { + this.repoPath = repoPath; + this.branchName = branchName; + this.exists = exists; + this.timestamp = System.currentTimeMillis(); + } + + public boolean isExpired() { + return System.currentTimeMillis() - timestamp > BRANCH_INFO_TTL; + } + + // Getters + public String getRepoPath() { return repoPath; } + public String getBranchName() { return branchName; } + public boolean exists() { return exists; } + public long getTimestamp() { return timestamp; } + } + + /** + * 提交信息缓存类 + */ + public static class CommitInfo { + private final String repoPath; + private final String branchName; + private final String commitId; + private final long timestamp; + + public CommitInfo(String repoPath, String branchName, String commitId) { + this.repoPath = repoPath; + this.branchName = branchName; + this.commitId = commitId; + this.timestamp = System.currentTimeMillis(); + } + + public boolean isExpired() { + return System.currentTimeMillis() - timestamp > COMMIT_INFO_TTL; + } + + // Getters + public String getRepoPath() { return repoPath; } + public String getBranchName() { return branchName; } + public String getCommitId() { return commitId; } + public long getTimestamp() { return timestamp; } + } + + // 仓库信息缓存操作 + public static RepoInfo getRepoInfo(String repoPath) { + RepoInfo info = repoInfoCache.get(repoPath); + if (info != null && !info.isExpired()) { + return info; + } + return null; + } + + public static void putRepoInfo(String repoPath, boolean hasUncommittedChanges, String currentBranch) { + repoInfoCache.put(repoPath, new RepoInfo(repoPath, hasUncommittedChanges, currentBranch)); + } + + // 分支信息缓存操作 + public static BranchInfo getBranchInfo(String repoPath, String branchName) { + String key = repoPath + ":" + branchName; + BranchInfo info = branchInfoCache.get(key); + if (info != null && !info.isExpired()) { + return info; + } + return null; + } + + public static void putBranchInfo(String repoPath, String branchName, boolean exists) { + String key = repoPath + ":" + branchName; + branchInfoCache.put(key, new BranchInfo(repoPath, branchName, exists)); + } + + // 提交信息缓存操作 + public static CommitInfo getCommitInfo(String repoPath, String branchName) { + String key = repoPath + ":" + branchName; + CommitInfo info = commitInfoCache.get(key); + if (info != null && !info.isExpired()) { + return info; + } + return null; + } + + public static void putCommitInfo(String repoPath, String branchName, String commitId) { + String key = repoPath + ":" + branchName; + commitInfoCache.put(key, new CommitInfo(repoPath, branchName, commitId)); + } + + // 缓存管理操作 + public static void clearCache() { + repoInfoCache.clear(); + branchInfoCache.clear(); + commitInfoCache.clear(); + log.info("All git result caches cleared"); + } + + public static void clearRepoCache(String repoPath) { + repoInfoCache.remove(repoPath); + + // 清理相关的分支和提交缓存 + branchInfoCache.entrySet().removeIf(entry -> entry.getKey().startsWith(repoPath + ":")); + commitInfoCache.entrySet().removeIf(entry -> entry.getKey().startsWith(repoPath + ":")); + + log.info("Cache cleared for repository: {}", repoPath); + } + + /** + * 清理过期缓存 + */ + private static void cleanupExpiredCache() { + AtomicLong cleanedCount = new AtomicLong(); + + // 清理过期的仓库信息缓存 + repoInfoCache.entrySet().removeIf(entry -> { + boolean expired = entry.getValue().isExpired(); + if (expired) cleanedCount.incrementAndGet(); + return expired; + }); + + // 清理过期的分支信息缓存 + branchInfoCache.entrySet().removeIf(entry -> { + boolean expired = entry.getValue().isExpired(); + if (expired) cleanedCount.incrementAndGet(); + return expired; + }); + + // 清理过期的提交信息缓存 + commitInfoCache.entrySet().removeIf(entry -> { + boolean expired = entry.getValue().isExpired(); + if (expired) cleanedCount.incrementAndGet(); + return expired; + }); + + if (cleanedCount.get() > 0) { + log.debug("Cleaned up {} expired cache entries", cleanedCount.get()); + } + } + + /** + * 获取缓存统计信息 + */ + public static CacheStats getCacheStats() { + return new CacheStats( + repoInfoCache.size(), + branchInfoCache.size(), + commitInfoCache.size() + ); + } + + /** + * 缓存统计信息类 + */ + public static class CacheStats { + private final int repoInfoCount; + private final int branchInfoCount; + private final int commitInfoCount; + + public CacheStats(int repoInfoCount, int branchInfoCount, int commitInfoCount) { + this.repoInfoCount = repoInfoCount; + this.branchInfoCount = branchInfoCount; + this.commitInfoCount = commitInfoCount; + } + + @Override + public String toString() { + return String.format("CacheStats{repoInfo=%d, branchInfo=%d, commitInfo=%d}", + repoInfoCount, branchInfoCount, commitInfoCount); + } + + // Getters + public int getRepoInfoCount() { return repoInfoCount; } + public int getBranchInfoCount() { return branchInfoCount; } + public int getCommitInfoCount() { return commitInfoCount; } + } + + /** + * 关闭缓存管理器(用于应用程序关闭时清理资源) + */ + public static void shutdown() { + cleanupExecutor.shutdown(); + try { + if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) { + cleanupExecutor.shutdownNow(); + } + } catch (InterruptedException e) { + cleanupExecutor.shutdownNow(); + Thread.currentThread().interrupt(); + } + clearCache(); + log.info("Git result cache shutdown completed"); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/mybatis/export/MyBatisExportService.java b/src/main/java/com/example/workutils/biz/mybatis/export/MyBatisExportService.java new file mode 100644 index 0000000..3e98e6b --- /dev/null +++ b/src/main/java/com/example/workutils/biz/mybatis/export/MyBatisExportService.java @@ -0,0 +1,411 @@ +package com.example.workutils.biz.mybatis.export; + +import com.example.workutils.config.ScanConfigBO; +import com.example.workutils.config.WorkUtilsProperties; +import com.example.workutils.biz.mybatis.model.MybatisTagBO; +import com.example.workutils.biz.output.OutputService; +import com.example.workutils.biz.scanner.service.FileOperationService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * MyBatis SQL导出服务 + * 负责从MyBatis XML文件中提取SQL并按各种条件导出 + * + * @author system + * @version 1.0 + */ +@Service +@Slf4j +public class MyBatisExportService { + + // ==================== 依赖注入 ==================== + + @Autowired + private FileOperationService fileOperationService; + + @Autowired + private OutputService outputService; + + @Autowired + private WorkUtilsProperties properties; + + // ==================== 常量定义 ==================== + + /** SQL标签类型 */ + private static final String[] SQL_TAG_TYPES = {"select", "insert", "update", "delete"}; + + /** 导出文件的表头 */ + private static final String[] EXPORT_HEADERS = { + "项目名", "文件路径", "SQL ID", "SQL类型", "原始SQL文本" + }; + + /** 表名匹配模式 - 匹配FROM、JOIN、UPDATE、INSERT INTO、DELETE FROM等SQL语句中的表名 */ + private static final String[] TABLE_PATTERNS = { + "(?i)\\bFROM\\s+([`\"']?)(\\w+)\\1", + "(?i)\\bJOIN\\s+([`\"']?)(\\w+)\\1", + "(?i)\\bUPDATE\\s+([`\"']?)(\\w+)\\1", + "(?i)\\bINSERT\\s+INTO\\s+([`\"']?)(\\w+)\\1", + "(?i)\\bDELETE\\s+FROM\\s+([`\"']?)(\\w+)\\1", + "(?i)\\bMERGE\\s+INTO\\s+([`\"']?)(\\w+)\\1" + }; + + // ==================== 公共接口方法 ==================== + + /** + * 导出所有SQL + * + * @throws Exception 导出异常 + */ + public void exportSQL() throws Exception { + exportWithFilter("SQL导出", null, null); + } + + /** + * 按表名模式导出SQL + * + * @param tableNamePattern 表名正则表达式模式 + * @throws Exception 导出异常 + */ + public void exportSQLByTable(String tableNamePattern) throws Exception { + exportWithFilter("表名SQL清单_" + tableNamePattern, + new TableFilter(tableNamePattern), null); + } + + /** + * 按多个表名模式导出SQL + * + * @param tableNamePatterns 表名模式列表 + * @throws Exception 导出异常 + */ + public void exportSQLByTables(List tableNamePatterns) throws Exception { + if (tableNamePatterns == null || tableNamePatterns.isEmpty()) { + log.warn("未提供表名模式"); + return; + } + + // 将多个表名模式合并为一个正则表达式 + String combinedPattern = String.join("|", tableNamePatterns.stream() + .map(pattern -> "(" + pattern + ")") + .toArray(String[]::new)); + + exportWithFilter("多表SQL清单_" + String.join("_", tableNamePatterns), + new TableFilter(combinedPattern), null); + } + + /** + * 按关键字导出SQL + * + * @param keywordPattern 关键字正则表达式模式 + * @throws Exception 导出异常 + */ + public void exportSQLByKeyword(String keywordPattern) throws Exception { + exportWithFilter("关键字SQL清单_" + keywordPattern, + new KeywordFilter(keywordPattern), null); + } + + /** + * 按SQL标签ID导出SQL + * + * @param tagIdPattern 标签ID正则表达式模式 + * @throws Exception 导出异常 + */ + public void exportSQLByTagId(String tagIdPattern) throws Exception { + exportWithFilter("标签ID SQL清单_" + tagIdPattern, + new TagIdFilter(tagIdPattern), null); + } + + // ==================== 核心导出逻辑 ==================== + + /** + * 使用过滤器导出SQL + * + * @param fileNamePrefix 文件名前缀 + * @param filter SQL过滤器 + * @param processStrategy 处理策略 + * @throws Exception 导出异常 + */ + private void exportWithFilter(String fileNamePrefix, SqlFilter filter, + ProcessStrategy processStrategy) throws Exception { + // 获取所有项目路径 + List projectPaths = fileOperationService.getProjectPaths(); + if (projectPaths.isEmpty()) { + log.warn("未找到项目路径"); + return; + } + + // 处理所有项目 + List allRecords = new ArrayList<>(); + AtomicInteger processedCount = new AtomicInteger(0); + + for (String projectPath : projectPaths) { + try { + log.info("正在处理项目: {}", projectPath); + List projectRecords = processProject(projectPath, filter, processStrategy); + allRecords.addAll(projectRecords); + processedCount.incrementAndGet(); + } catch (Exception e) { + log.error("处理项目时出错: " + projectPath, e); + } + } + + // 输出到文件 + String fileName = outputService.generateOutputFileName(fileNamePrefix, "csv"); + outputService.writeRecordsToCsv(allRecords, fileName, Arrays.asList(EXPORT_HEADERS)); + + log.info("导出完成。已处理 {} 个项目,共 {} 条记录", + processedCount.get(), allRecords.size()); + } + + /** + * 处理单个项目 + * + * @param projectPath 项目路径 + * @param filter SQL过滤器 + * @param processStrategy 处理策略 + * @return 处理结果记录列表 + * @throws Exception 处理异常 + */ + private List processProject(String projectPath, SqlFilter filter, + ProcessStrategy processStrategy) throws Exception { + List records = new ArrayList<>(); + String projectName = fileOperationService.extractProjectName(projectPath); + + // 扫描XML文件 + ScanConfigBO config = ScanConfigBO.forXmlFiles(); + List xmlFiles = fileOperationService.scanFiles(projectPath, config); + + for (Path xmlFile : xmlFiles) { + try { + List tags = parseXmlFile(xmlFile); + for (MybatisTagBO tag : tags) { + // 应用过滤器 + if (filter == null || filter.matches(tag)) { + // 应用处理策略 + if (processStrategy == null || processStrategy.process(tag)) { + String[] record = buildRecord(tag, projectName, xmlFile); + records.add(record); + } + } + } + } catch (Exception e) { + log.warn("解析XML文件时出错: " + xmlFile, e); + } + } + + return records; + } + + // ==================== XML解析相关方法 ==================== + + /** + * 解析XML文件,提取SQL标签 + * + * @param xmlFile XML文件路径 + * @return SQL标签列表 + * @throws Exception 解析异常 + */ + private List parseXmlFile(Path xmlFile) throws Exception { + List tags = new ArrayList<>(); + String content = fileOperationService.readFileContent(xmlFile); + + // 解析各种SQL标签 + for (String tagType : SQL_TAG_TYPES) { + tags.addAll(parseSqlTags(content, tagType)); + } + + return tags; + } + + /** + * 解析指定类型的SQL标签 + * + * @param content XML内容 + * @param tagName 标签名称 + * @return 解析到的标签列表 + */ + private List parseSqlTags(String content, String tagName) { + List tags = new ArrayList<>(); + + String regex = "<" + tagName + "\\b[^>]*>(.*?)"; + Pattern pattern = Pattern.compile(regex, Pattern.DOTALL | Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(content); + + while (matcher.find()) { + MybatisTagBO tag = new MybatisTagBO(); + String tagContent = matcher.group(0); + + // 设置基本信息 + tag.setTagName(tagName); + tag.setTagOriginalXmlText(tagContent); + + // 提取ID + extractTagAttribute(tagContent, "id", tag::setTagId); + + // 提取parameterType + extractTagAttribute(tagContent, "parameterType", tag::setParameterType); + + tags.add(tag); + } + + return tags; + } + + /** + * 从标签内容中提取指定属性 + * + * @param tagContent 标签内容 + * @param attributeName 属性名称 + * @param setter 属性设置器 + */ + private void extractTagAttribute(String tagContent, String attributeName, + java.util.function.Consumer setter) { + String regex = attributeName + "\\s*=\\s*[\"']([^\"']+)[\"']"; + Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(tagContent); + if (matcher.find()) { + setter.accept(matcher.group(1)); + } + } + + // ==================== 记录构建 ==================== + + /** + * 构建导出记录 + * + * @param tag SQL标签 + * @param projectName 项目名称 + * @param xmlFile XML文件路径 + * @return 导出记录数组 + */ + private String[] buildRecord(MybatisTagBO tag, String projectName, Path xmlFile) { + return new String[] { + projectName, + xmlFile.toString(), + tag.getTagId() != null ? tag.getTagId() : "", + tag.getTagName(), + tag.getTagOriginalXmlText() != null ? tag.getTagOriginalXmlText() : "" + }; + } + + // ==================== 过滤器接口和实现 ==================== + + /** + * SQL过滤器接口 + */ + private interface SqlFilter { + boolean matches(MybatisTagBO tag); + } + + /** + * 表名过滤器 + */ + private static class TableFilter implements SqlFilter { + private final Pattern tablePattern; + + public TableFilter(String pattern) { + this.tablePattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); + } + + @Override + public boolean matches(MybatisTagBO tag) { + if (tag == null || tag.getTagOriginalXmlText() == null) { + return false; + } + + String sqlText = removeSqlComments(tag.getTagOriginalXmlText()); + + for (String pattern : TABLE_PATTERNS) { + Pattern p = Pattern.compile(pattern); + Matcher m = p.matcher(sqlText); + while (m.find()) { + String tableName = m.group(2); // 获取表名组 + if (tableName != null && tablePattern.matcher(tableName).matches()) { + return true; + } + } + } + + return false; + } + + /** + * 移除SQL注释 + * + * @param sqlText SQL文本 + * @return 移除注释后的SQL文本 + */ + private String removeSqlComments(String sqlText) { + String cleanText = sqlText.replaceAll("--.*", ""); + cleanText = cleanText.replaceAll("/\\*.*?\\*/", ""); + return cleanText; + } + } + + /** + * 关键字过滤器 + */ + private static class KeywordFilter implements SqlFilter { + private final Pattern keywordPattern; + + public KeywordFilter(String pattern) { + this.keywordPattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); + } + + @Override + public boolean matches(MybatisTagBO tag) { + if (tag == null || tag.getTagOriginalXmlText() == null) { + return false; + } + + String cleanSqlText = removeSqlComments(tag.getTagOriginalXmlText()); + Matcher matcher = keywordPattern.matcher(cleanSqlText); + return matcher.find(); + } + + private String removeSqlComments(String sqlText) { + String cleanText = sqlText.replaceAll("--.*", ""); + cleanText = cleanText.replaceAll("/\\*.*?\\*/", ""); + return cleanText; + } + } + + /** + * 标签ID过滤器 + */ + private static class TagIdFilter implements SqlFilter { + private final Pattern tagIdPattern; + + public TagIdFilter(String pattern) { + this.tagIdPattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); + } + + @Override + public boolean matches(MybatisTagBO tag) { + if (tag == null || tag.getTagId() == null) { + return false; + } + + Matcher matcher = tagIdPattern.matcher(tag.getTagId()); + return matcher.matches(); + } + } + + // ==================== 处理策略接口 ==================== + + /** + * 处理策略接口(预留扩展) + */ + private interface ProcessStrategy { + boolean process(MybatisTagBO tag); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/mybatis/model/MybatisTagBO.java b/src/main/java/com/example/workutils/biz/mybatis/model/MybatisTagBO.java new file mode 100644 index 0000000..2e4c866 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/mybatis/model/MybatisTagBO.java @@ -0,0 +1,63 @@ +package com.example.workutils.biz.mybatis.model; + +import lombok.Data; + +/** + * MyBatis标签数据传输对象 + * 用于封装从MyBatis XML文件中解析的SQL标签信息 + * + * @author system + * @version 1.0 + */ +@Data +public class MybatisTagBO { + + /** + * 标签ID + * 对应MyBatis XML中SQL标签的id属性,用于唯一标识一个SQL语句 + */ + private String tagId; + + /** + * 标签名称 + * SQL标签类型,如select、insert、update、delete等 + */ + private String tagName; + + /** + * 参数类型 + * 对应MyBatis XML中SQL标签的parameterType属性,指定传入参数的Java类型 + */ + private String parameterType; + + /** + * 结果集映射ID + * 对应MyBatis XML中SQL标签的resultMap属性,指定结果集的映射规则 + */ + private String resultMap; + + /** + * 标签原始XML文本 + * 保留SQL标签的完整XML内容,包括标签、属性和SQL语句 + * 注意:保留原始格式,包括换行和缩进 + */ + private String tagOriginalXmlText; + + /** + * Include标签原始XML文本 + * 存储包含的SQL片段引用信息 + */ + private String includeTagOriginalXmlText; + + /** + * 命名空间 + * MyBatis Mapper接口的全限定名,对应XML的namespace属性 + */ + private String namespace; + + /** + * 简化命名空间 + * 命名空间的简化版本,通常只包含类名 + */ + private String simpleNamespace; +} diff --git a/src/main/java/com/example/workutils/biz/mybatis/service/XMLMapperChecker.java b/src/main/java/com/example/workutils/biz/mybatis/service/XMLMapperChecker.java new file mode 100644 index 0000000..e6c5d51 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/mybatis/service/XMLMapperChecker.java @@ -0,0 +1,49 @@ +package com.example.workutils.service; + +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import java.io.File; + +public class XMLMapperChecker { + + public static String getErrorText(String filePath) { + try { + checkXMLMapper(filePath); + } catch (Throwable e) { + return e.getMessage(); + } + return null; + } + + private static void checkXMLMapper(String filePath) { + File xmlFile = new File(filePath); + if (xmlFile.length() == 0) { + throw new RuntimeException("文件为空"); + } + + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + try { + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + Document doc = dBuilder.parse(xmlFile); + doc.getDocumentElement().normalize(); + + System.out.println(filePath + "标签检查完毕,未发现问题"); + } catch (Exception e) { + throw new RuntimeException("标签缺失"); + } + } + + // 检查单个标签是否存在且唯一 + private static void checkSingleTag(Document doc, String tagName) { + NodeList nodeList = doc.getElementsByTagName(tagName); + if (nodeList.getLength() == 0) { + throw new RuntimeException("缺少<" + tagName + ">标签"); + } else if (nodeList.getLength() > 1) { + new RuntimeException("存在多个<" + tagName + ">标签"); + } + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/output/OutputService.java b/src/main/java/com/example/workutils/biz/output/OutputService.java new file mode 100644 index 0000000..e9f8463 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/output/OutputService.java @@ -0,0 +1,249 @@ +package com.example.workutils.biz.output; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.ExcelWriter; +import com.alibaba.excel.write.metadata.WriteSheet; +import com.example.workutils.config.WorkUtilsProperties; +import com.example.workutils.biz.scanner.service.FileOperationService; +import com.opencsv.CSVWriter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; + +/** + * 输出服务 + * 负责将数据输出到CSV、Excel等格式的文件 + * + * @author system + * @version 1.0 + */ +@Service +@Slf4j +public class OutputService { + + // ==================== 依赖注入 ==================== + + @Autowired + private FileOperationService fileOperationService; + + @Autowired + private WorkUtilsProperties properties; + + // ==================== CSV输出相关方法 ==================== + + /** + * 将记录列表写入CSV文件 + * + * @param records 记录列表 + * @param outputPath 输出文件路径 + * @param headers 表头数组 + * @throws IOException 写入异常 + */ + public void writeRecordsToCsv(List records, String outputPath, String... headers) throws IOException { + Path filePath; + // 如果 outputPath 已经是完整路径(包含导出根目录),直接使用 + if (outputPath.startsWith(properties.getExportBasePath().toString())) { + filePath = java.nio.file.Paths.get(outputPath); + } else { + filePath = properties.resolveExportPath(outputPath); + } + fileOperationService.ensureDirectoryExists(filePath.getParent()); + + try (CSVWriter writer = new CSVWriter(new FileWriter(filePath.toFile()))) { + if (headers.length > 0) { + writer.writeNext(headers); + } + writer.writeAll(records); + writer.flush(); + } + + log.info("CSV文件写入成功: {}", filePath); + } + + /** + * 将记录列表写入CSV文件(重载方法) + * + * @param records 记录列表 + * @param outputPath 输出文件路径 + * @param headers 表头列表 + * @throws IOException 写入异常 + */ + public void writeRecordsToCsv(List records, String outputPath, List headers) throws IOException { + writeRecordsToCsv(records, outputPath, headers.toArray(new String[0])); + } + + /** + * 将单条记录写入CSV文件 + * + * @param record 记录 + * @param outputPath 输出文件路径 + * @param headers 表头数组 + * @throws IOException 写入异常 + */ + public void writeSingleRecordToCsv(String[] record, String outputPath, String... headers) throws IOException { + writeRecordsToCsv(Collections.singletonList(record), outputPath, headers); + } + + /** + * 追加记录到CSV文件 + * 如果文件不存在,则创建文件并写入表头 + * + * @param record 记录 + * @param outputPath 输出文件路径 + * @param headers 表头数组 + * @throws IOException 写入异常 + */ + public void appendToCsv(String[] record, String outputPath, String... headers) throws IOException { + Path filePath = properties.resolveExportPath(outputPath); + fileOperationService.ensureDirectoryExists(filePath.getParent()); + + boolean fileExists = fileOperationService.fileExists(filePath); + boolean shouldWriteHeaders = !fileExists && headers.length > 0; + + try (CSVWriter writer = new CSVWriter(new FileWriter(filePath.toFile(), true))) { + if (shouldWriteHeaders) { + writer.writeNext(headers); + } + writer.writeNext(record); + writer.flush(); + } + + log.debug("记录追加到CSV文件成功: {}", filePath); + } + + // ==================== Excel输出相关方法 ==================== + + /** + * 将数据写入Excel文件(单Sheet) + * + * @param data 数据列表 + * @param outputPath 输出文件路径 + * @param modelClass 数据模型类 + * @param 数据类型 + * @throws IOException 写入异常 + */ + public void writeToExcel(List data, String outputPath, Class modelClass) throws IOException { + Path filePath = properties.resolveExportPath(outputPath); + fileOperationService.ensureDirectoryExists(filePath.getParent()); + + EasyExcel.write(filePath.toFile(), modelClass).sheet().doWrite(data); + + log.info("Excel文件写入成功: {}", filePath); + } + + /** + * 将数据写入Excel文件(指定Sheet名称) + * + * @param data 数据列表 + * @param outputPath 输出文件路径 + * @param sheetName Sheet名称 + * @param modelClass 数据模型类 + * @param 数据类型 + * @throws IOException 写入异常 + */ + public void writeToExcel(List data, String outputPath, String sheetName, Class modelClass) throws IOException { + Path filePath = properties.resolveExportPath(outputPath); + fileOperationService.ensureDirectoryExists(filePath.getParent()); + + EasyExcel.write(filePath.toFile(), modelClass).sheet(sheetName).doWrite(data); + + log.info("Excel文件写入成功: {}", filePath); + } + + /** + * 将数据写入Excel文件(多Sheet) + * + * @param sheetsData Sheet数据列表 + * @param outputPath 输出文件路径 + * @param 数据类型 + * @throws IOException 写入异常 + */ + public void writeToExcelWithMultipleSheets(List> sheetsData, String outputPath) throws IOException { + Path filePath = properties.resolveExportPath(outputPath); + fileOperationService.ensureDirectoryExists(filePath.getParent()); + + try (ExcelWriter excelWriter = EasyExcel.write(filePath.toFile()).build()) { + for (ExcelSheetData sheetData : sheetsData) { + WriteSheet writeSheet = EasyExcel.writerSheet(sheetData.getSheetName()) + .head(sheetData.getModelClass()) + .build(); + excelWriter.write(sheetData.getData(), writeSheet); + } + } + + log.info("多Sheet Excel文件写入成功: {}", filePath); + } + + // ==================== 文件名生成相关方法 ==================== + + /** + * 生成输出文件名 + * + * @param prefix 文件名前缀 + * @param extension 文件扩展名 + * @return 生成的文件名 + */ + public String generateOutputFileName(String prefix, String extension) { + return fileOperationService.generateFileName(prefix, extension); + } + + // ==================== 内部数据类 ==================== + + /** + * Excel Sheet数据封装类 + * 用于封装单个Sheet的数据、名称和模型类信息 + * + * @param 数据类型 + */ + public static class ExcelSheetData { + private final List data; // 数据列表 + private final String sheetName; // Sheet名称 + private final Class modelClass; // 数据模型类 + + /** + * 构造函数 + * + * @param data 数据列表 + * @param sheetName Sheet名称 + * @param modelClass 数据模型类 + */ + public ExcelSheetData(List data, String sheetName, Class modelClass) { + this.data = data; + this.sheetName = sheetName; + this.modelClass = modelClass; + } + + /** + * 获取数据列表 + * + * @return 数据列表 + */ + public List getData() { + return data; + } + + /** + * 获取Sheet名称 + * + * @return Sheet名称 + */ + public String getSheetName() { + return sheetName; + } + + /** + * 获取数据模型类 + * + * @return 数据模型类 + */ + public Class getModelClass() { + return modelClass; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/scanner/config/ScanConfigBO.java b/src/main/java/com/example/workutils/biz/scanner/config/ScanConfigBO.java new file mode 100644 index 0000000..08da993 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/scanner/config/ScanConfigBO.java @@ -0,0 +1,60 @@ +package com.example.workutils.config; + +import lombok.Data; + +/** + * 扫描配置数据对象 + * 封装文件扫描过程中的各种配置参数 + * + * @author system + * @version 1.0 + */ +@Data +public class ScanConfigBO { + + /** 是否启用扫描 */ + private boolean enabled = true; + + /** 是否包含子目录 */ + private boolean includeSubdirectories = true; + + /** 扫描的文件扩展名,默认扫描Java、XML和属性文件 */ + private String[] fileExtensions = {".java", ".xml", ".properties"}; + + /** 排除的目录名称,默认排除target、git和node_modules目录 */ + private String[] excludeDirectories = {"target", ".git", "node_modules"}; + + /** 最大文件大小限制,默认10MB */ + private int maxFileSize = 10 * 1024 * 1024; + + /** + * 创建默认扫描配置 + * + * @return 默认配置实例 + */ + public static ScanConfigBO defaultConfig() { + return new ScanConfigBO(); + } + + /** + * 创建XML文件扫描配置 + * + * @return XML文件扫描配置 + */ + public static ScanConfigBO forXmlFiles() { + ScanConfigBO config = new ScanConfigBO(); + config.setFileExtensions(new String[]{".xml"}); + return config; + } + + /** + * 创建Java文件扫描配置 + * + * @return Java文件扫描配置 + */ + public static ScanConfigBO forJavaFiles() { + ScanConfigBO config = new ScanConfigBO(); + config.setFileExtensions(new String[]{".java"}); + return config; + } +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/scanner/service/FileOperationService.java b/src/main/java/com/example/workutils/biz/scanner/service/FileOperationService.java new file mode 100644 index 0000000..973afc1 --- /dev/null +++ b/src/main/java/com/example/workutils/biz/scanner/service/FileOperationService.java @@ -0,0 +1,496 @@ +package com.example.workutils.biz.scanner.service; + +import com.example.workutils.config.ScanConfigBO; +import com.example.workutils.config.WorkUtilsProperties; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * 文件操作服务 + * 负责处理文件和目录的读取、扫描等操作 + * + * @author system + * @version 1.0 + */ +@Service +@Slf4j +public class FileOperationService { + + // ==================== 依赖注入 ==================== + + @Autowired + private WorkUtilsProperties properties; + + // ==================== 常量定义 ==================== + + /** UTF-8字符集 */ + private static final java.nio.charset.Charset UTF8_CHARSET = java.nio.charset.StandardCharsets.UTF_8; + + /** 默认文件大小限制:10MB */ + private static final long DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024; + + /** 系统行分隔符 */ + private static final String LINE_SEPARATOR = System.lineSeparator(); + + // ==================== 文件读取相关方法 ==================== + + /** + * 按行读取文件内容 + * + * @param filePath 文件路径 + * @return 文件行列表 + * @throws IOException 读取异常 + */ + public List readLinesFromFile(String filePath) throws IOException { + Path path = Paths.get(filePath); + return readLinesFromFile(path); + } + + /** + * 按行读取文件内容 + * + * @param filePath 文件路径 + * @return 文件行列表 + * @throws IOException 读取异常 + */ + public List readLinesFromFile(Path filePath) throws IOException { + try { + return Files.readAllLines(filePath, UTF8_CHARSET); + } catch (java.nio.charset.MalformedInputException e) { + log.warn("使用UTF-8解码文件失败: {}, 尝试使用默认字符集", filePath); + try { + return Files.readAllLines(filePath); + } catch (java.nio.charset.MalformedInputException e2) { + log.warn("使用默认字符集解码文件也失败: {}, 返回空列表", filePath); + return new ArrayList<>(); + } + } + } + + /** + * 读取文件完整内容(保留换行符) + * + * @param filePath 文件路径 + * @return 文件内容字符串 + * @throws IOException 读取异常 + */ + public String readFileContent(Path filePath) throws IOException { + try { + // 读取文件行并使用系统行分隔符重新组合,保持原始格式 + List lines = Files.readAllLines(filePath, UTF8_CHARSET); + return String.join(LINE_SEPARATOR, lines); + } catch (java.nio.charset.MalformedInputException e) { + log.warn("使用UTF-8解码文件失败: {}, 尝试使用默认字符集", filePath); + try { + List lines = Files.readAllLines(filePath); + return String.join(LINE_SEPARATOR, lines); + } catch (java.nio.charset.MalformedInputException e2) { + log.warn("使用默认字符集解码文件也失败: {}, 返回空字符串", filePath); + return ""; + } + } + } + + // ==================== 文件名生成相关方法 ==================== + + /** + * 生成带时间戳的文件名 + * + * @param baseDirectory 基础目录 + * @param prefix 文件名前缀 + * @param extension 文件扩展名 + * @return 生成的文件名 + */ + public String generateFileName(String baseDirectory, String prefix, String extension) { + String timestamp = java.time.LocalDateTime.now() + .format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss")); + return String.format("%s/%s_%s.%s", baseDirectory, prefix, timestamp, extension); + } + + /** + * 生成带时间戳的文件名(使用配置的导出基础路径) + * 格式:导出根目录/yyyyMMdd/prefix_yyyyMMddHHmmss.extension + * + * @param prefix 文件名前缀 + * @param extension 文件扩展名 + * @return 生成的文件完整路径 + */ + public String generateFileName(String prefix, String extension) { + return properties.resolveExportPath(prefix, extension).toString(); + } + + /** + * 提取项目名称 + * + * @param projectPath 项目路径 + * @return 项目名称 + */ + public String extractProjectName(String projectPath) { + if (projectPath == null || projectPath.isEmpty()) { + return "unknown"; + } + + Path path = Paths.get(projectPath); + String fileName = path.getFileName().toString(); + + // 移除路径末尾的分隔符 + if (fileName.endsWith("/") || fileName.endsWith("\\")) { + fileName = fileName.substring(0, fileName.length() - 1); + } + + return fileName.isEmpty() ? "unknown" : fileName; + } + + // ==================== 文件扫描相关方法 ==================== + + /** + * 扫描指定路径下的文件 + * + * @param rootPath 根路径 + * @param config 扫描配置 + * @return 文件路径列表 + * @throws IOException 扫描异常 + */ + public List scanFiles(String rootPath, ScanConfigBO config) throws IOException { + List result = new ArrayList<>(); + Path root = Paths.get(rootPath); + + if (!Files.exists(root) || !Files.isDirectory(root)) { + log.warn("路径不存在或不是目录: {}", rootPath); + return result; + } + + if (config.isIncludeSubdirectories()) { + scanFilesRecursively(root, config, result); + } else { + scanFilesNonRecursively(root, config, result); + } + + return result; + } + + /** + * 递归扫描文件 + * + * @param root 根目录 + * @param config 扫描配置 + * @param result 结果列表 + * @throws IOException 扫描异常 + */ + private void scanFilesRecursively(Path root, ScanConfigBO config, List result) throws IOException { + List excludeDirs = Arrays.asList(config.getExcludeDirectories()); + List extensions = Arrays.asList(config.getFileExtensions()); + + Files.walkFileTree(root, new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) { + String dirName = dir.getFileName().toString(); + if (excludeDirs.contains(dirName)) { + log.debug("跳过排除目录: {}", dirName); + return FileVisitResult.SKIP_SUBTREE; + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { + if (attrs.isRegularFile()) { + try { + if (shouldIncludeFile(file, extensions, config.getMaxFileSize())) { + result.add(file); + } + } catch (Exception e) { + log.warn("处理文件时出错: " + file, e); + } + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) { + log.warn("访问文件失败: " + file, exc); + return FileVisitResult.CONTINUE; + } + }); + } + + /** + * 非递归扫描文件(只扫描直接子目录) + * + * @param root 根目录 + * @param config 扫描配置 + * @param result 结果列表 + * @throws IOException 扫描异常 + */ + private void scanFilesNonRecursively(Path root, ScanConfigBO config, List result) throws IOException { + if (!Files.isDirectory(root)) { + return; + } + + try (Stream stream = Files.list(root)) { + stream.filter(file -> { + try { + return shouldIncludeFile(file, + Arrays.asList(config.getFileExtensions()), + config.getMaxFileSize()); + } catch (Exception e) { + log.warn("检查文件时出错: " + file, e); + return false; + } + }).forEach(result::add); + } catch (IOException e) { + log.warn("列出文件时出错: " + root, e); + } + } + + /** + * 判断文件是否应该包含在扫描结果中 + * + * @param file 文件路径 + * @param extensions 允许的文件扩展名列表 + * @param maxSize 最大文件大小限制 + * @return 是否包含 + * @throws IOException 检查异常 + */ + private boolean shouldIncludeFile(Path file, List extensions, long maxSize) throws IOException { + // 检查文件大小 + if (Files.size(file) > maxSize) { + log.debug("文件过大,跳过: {} (大小: {} bytes)", file, Files.size(file)); + return false; + } + + // 检查文件扩展名 + String fileName = file.getFileName().toString(); + if (extensions.isEmpty()) { + // 如果没有指定扩展名限制,则包含所有文件 + return true; + } + + return extensions.stream().anyMatch(ext -> + fileName.toLowerCase().endsWith(ext.toLowerCase())); + } + + // ==================== 项目路径相关方法 ==================== + + /** + * 项目状态枚举 + */ + public enum ProjectStatus { + NORMAL, + CORRUPTED + } + + /** + * 项目路径信息(含状态) + */ + public static class ProjectPathInfo { + private String path; + private ProjectStatus status; + private String corruptionReason; + + public ProjectPathInfo(String path, ProjectStatus status) { + this.path = path; + this.status = status; + } + + public ProjectPathInfo(String path, ProjectStatus status, String corruptionReason) { + this.path = path; + this.status = status; + this.corruptionReason = corruptionReason; + } + + public String getPath() { return path; } + public void setPath(String path) { this.path = path; } + public ProjectStatus getStatus() { return status; } + public void setStatus(ProjectStatus status) { this.status = status; } + public String getCorruptionReason() { return corruptionReason; } + public void setCorruptionReason(String corruptionReason) { this.corruptionReason = corruptionReason; } + + /** + * 转换为配置文件格式 + */ + public String toConfigLine() { + return path + "|" + status.name(); + } + } + + /** + * 获取所有项目路径(兼容旧格式,无状态) + * + * @return 项目路径列表 + * @throws IOException 读取异常 + */ + public List getProjectPaths() throws IOException { + Path projectPathsFile = properties.getProjectPathsFilePath(); + if (!Files.exists(projectPathsFile)) { + log.warn("项目路径文件不存在: {}", projectPathsFile); + return new ArrayList<>(); + } + + return readLinesFromFile(projectPathsFile).stream() + .map(String::trim) + .filter(line -> !line.isEmpty() && !line.startsWith("#")) + .map(line -> { + // 兼容旧格式:只包含路径,没有状态 + if (!line.contains("|")) { + return line; + } + // 新格式:path|status + return line.split("\\|")[0]; + }) + .collect(Collectors.toList()); + } + + /** + * 获取所有项目路径(含状态) + * + * @return 项目路径信息列表 + * @throws IOException 读取异常 + */ + public List getProjectPathsWithStatus() throws IOException { + Path projectPathsFile = properties.getProjectPathsFilePath(); + if (!Files.exists(projectPathsFile)) { + log.warn("项目路径文件不存在: {}", projectPathsFile); + return new ArrayList<>(); + } + + return readLinesFromFile(projectPathsFile).stream() + .map(String::trim) + .filter(line -> !line.isEmpty() && !line.startsWith("#")) + .map(line -> { + // 兼容旧格式:只包含路径,没有状态 + if (!line.contains("|")) { + return new ProjectPathInfo(line, ProjectStatus.NORMAL); + } + // 新格式:path|status + String[] parts = line.split("\\|"); + String path = parts[0].trim(); + ProjectStatus status; + try { + status = ProjectStatus.valueOf(parts[1].trim().toUpperCase()); + } catch (Exception e) { + status = ProjectStatus.NORMAL; + } + return new ProjectPathInfo(path, status); + }) + .collect(Collectors.toList()); + } + + /** + * 更新项目状态 + * + * @param path 项目路径 + * @param status 新状态 + * @throws IOException 写入异常 + */ + public void updateProjectStatus(String path, ProjectStatus status) throws IOException { + List projects = getProjectPathsWithStatus(); + for (ProjectPathInfo project : projects) { + if (project.getPath().equals(path)) { + project.setStatus(status); + break; + } + } + saveProjectPaths(projects); + } + + /** + * 保存项目路径列表到文件 + * + * @param projects 项目列表 + * @throws IOException 写入异常 + */ + public void saveProjectPaths(List projects) throws IOException { + String content = projects.stream() + .map(ProjectPathInfo::toConfigLine) + .collect(Collectors.joining("\n")); + writeToFile(properties.getProjectPathsFilePath(), content); + } + + // ==================== 文件写入相关方法 ==================== + + /** + * 写入内容到文件 + * + * @param path 文件路径 + * @param content 内容 + * @throws IOException 写入异常 + */ + public void writeToFile(Path path, String content) throws IOException { + // 确保父目录存在 + Files.createDirectories(path.getParent()); + + // 写入文件 + Files.write(path, content.getBytes(), + StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + + log.debug("文件写入成功: {}", path); + } + + /** + * 写入内容到文件 + * + * @param filePath 文件路径字符串 + * @param content 内容 + * @throws IOException 写入异常 + */ + public void writeToFile(String filePath, String content) throws IOException { + writeToFile(Paths.get(filePath), content); + } + + // ==================== 文件检查相关方法 ==================== + + /** + * 检查文件是否存在 + * + * @param filePath 文件路径字符串 + * @return 是否存在 + */ + public boolean fileExists(String filePath) { + return Files.exists(Paths.get(filePath)); + } + + /** + * 检查文件是否存在 + * + * @param path 文件路径 + * @return 是否存在 + */ + public boolean fileExists(Path path) { + return Files.exists(path); + } + + /** + * 确保目录存在,如果不存在则创建 + * + * @param path 目录路径 + * @throws IOException 创建异常 + */ + public void ensureDirectoryExists(Path path) throws IOException { + if (!Files.exists(path)) { + Files.createDirectories(path); + log.debug("目录创建成功: {}", path); + } + } + + /** + * 确保目录存在,如果不存在则创建 + * + * @param path 目录路径字符串 + * @throws IOException 创建异常 + */ + public void ensureDirectoryExists(String path) throws IOException { + ensureDirectoryExists(Paths.get(path)); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/biz/scanner/service/ScanService.java b/src/main/java/com/example/workutils/biz/scanner/service/ScanService.java new file mode 100644 index 0000000..61451da --- /dev/null +++ b/src/main/java/com/example/workutils/biz/scanner/service/ScanService.java @@ -0,0 +1,349 @@ +package com.example.workutils.biz.scanner.service; + +import com.example.workutils.config.ScanConfigBO; +import com.example.workutils.config.WorkUtilsProperties; +import com.example.workutils.biz.output.OutputService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +@Service +@Slf4j +public class ScanService { + + private final ExecutorService executorService = Executors.newFixedThreadPool(10); + @Autowired + private FileOperationService fileOperationService; + @Autowired + private OutputService outputService; + @Autowired + private WorkUtilsProperties properties; + + public ScanResult scanAllProjects(ScanType scanType, String pattern) throws Exception { + List projectPaths = fileOperationService.getProjectPaths(); + if (projectPaths.isEmpty()) { + log.warn("No project paths found"); + return new ScanResult(); + } + + ScanResult result = new ScanResult(); + List> futures = new ArrayList<>(); + + for (String projectPath : projectPaths) { + CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + return scanProject(projectPath, scanType, pattern); + } catch (Exception e) { + log.error("Error scanning project: " + projectPath, e); + return new ProjectScanResult(projectPath, new ArrayList<>()); + } + }, executorService); + + futures.add(future); + } + + // 等待所有扫描完成 + for (CompletableFuture future : futures) { + ProjectScanResult projectResult = future.get(); + result.addProjectResult(projectResult); + } + + return result; + } + + private ProjectScanResult scanProject(String projectPath, ScanType scanType, String pattern) throws Exception { + ProjectScanResult projectResult = new ProjectScanResult(projectPath, new ArrayList<>()); + String projectName = fileOperationService.extractProjectName(projectPath); + + ScanConfigBO config = getConfigForScanType(scanType); + List files = fileOperationService.scanFiles(projectPath, config); + + for (Path file : files) { + try { + List matches = scanFile(file, scanType, pattern); + if (!matches.isEmpty()) { + for (MatchResult match : matches) { + match.setProjectName(projectName); + match.setFilePath(file.toString()); + } + projectResult.getMatches().addAll(matches); + } + } catch (Exception e) { + log.warn("Error scanning file: " + file, e); + } + } + + return projectResult; + } + + private List scanFile(Path file, ScanType scanType, String pattern) throws IOException { + List matches = new ArrayList<>(); + List lines; + try { + lines = fileOperationService.readLinesFromFile(file); + } catch (Exception e) { + log.warn("Failed to read file: {}, skipping this file", file, e); + return matches; + } + + Pattern regexPattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); + + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + Matcher matcher = regexPattern.matcher(line); + + if (matcher.find()) { + MatchResult match = new MatchResult(); + match.setLineNumber(i + 1); + match.setLineContent(line.trim()); + match.setMatchText(matcher.group()); + match.setScanType(scanType); + + // 根据扫描类型提取额外信息 + extractExtraInfo(match, line, scanType); + + matches.add(match); + } + } + + return matches; + } + + private void extractExtraInfo(MatchResult match, String line, ScanType scanType) { + switch (scanType) { + case DUBBO: + extractDubboInfo(match, line); + break; + case REDIS_KEY: + extractRedisInfo(match, line); + break; + case KEYWORD: + extractKeywordInfo(match, line); + break; + } + } + + private void extractDubboInfo(MatchResult match, String line) { + // 提取Dubbo服务信息 + if (line.contains("@Reference")) { + match.setAdditionalInfo("Dubbo Reference"); + } else if (line.contains("@Service")) { + match.setAdditionalInfo("Dubbo Service"); + } else if (line.contains(" records = new ArrayList<>(); + + // 添加表头 + String[] headers = {"项目名", "文件路径", "行号", "匹配内容", "完整行内容", "附加信息", "扫描类型"}; + records.add(headers); + + // 添加数据 + for (ProjectScanResult projectResult : result.getProjectResults()) { + for (MatchResult match : projectResult.getMatches()) { + String[] record = { + match.getProjectName(), + match.getFilePath(), + String.valueOf(match.getLineNumber()), + match.getMatchText(), + match.getLineContent(), + match.getAdditionalInfo() != null ? match.getAdditionalInfo() : "", + match.getScanType().name() + }; + records.add(record); + } + } + + // 输出到文件,确保目录存在 + fileOperationService.ensureDirectoryExists(properties.getTodayExportPath()); + outputService.writeRecordsToCsv(records, outputFileName); + + log.info("Scan results exported to: {}", outputFileName); + } + + // 兼容旧版本的便捷方法 + public void scanKeywords(String pattern, String outputFile) throws Exception { + ScanResult result = scanAllProjects(ScanType.KEYWORD, pattern); + exportScanResults(result, outputFile); + } + + public void scanRedisKeys(String pattern, String outputFile) throws Exception { + ScanResult result = scanAllProjects(ScanType.REDIS_KEY, pattern); + exportScanResults(result, outputFile); + } + + public void scanDubboInterfaces(String pattern, String outputFile) throws Exception { + ScanResult result = scanAllProjects(ScanType.DUBBO, pattern); + exportScanResults(result, outputFile); + } + + // 新增方法:使用默认文件名进行关键词扫描 + public void scanKeywordsWithDefaultName(String pattern) throws Exception { + ScanResult result = scanAllProjects(ScanType.KEYWORD, pattern); + String defaultFileName = outputService.generateOutputFileName("keyword_scan_result", "csv"); + exportScanResults(result, defaultFileName); + } + + // 新增方法:使用默认文件名进行Redis Key扫描 + public void scanRedisKeysWithDefaultName(String pattern) throws Exception { + ScanResult result = scanAllProjects(ScanType.REDIS_KEY, pattern); + String defaultFileName = outputService.generateOutputFileName("redis_key_scan_result", "csv"); + exportScanResults(result, defaultFileName); + } + + // 新增方法:使用默认文件名进行Dubbo接口扫描 + public void scanDubboInterfacesWithDefaultName(String pattern) throws Exception { + ScanResult result = scanAllProjects(ScanType.DUBBO, pattern); + String defaultFileName = outputService.generateOutputFileName("dubbo_scan_result", "csv"); + exportScanResults(result, defaultFileName); + } + + public enum ScanType { KEYWORD, // 关键词扫描 + REDIS_KEY, // Redis Key扫描 + DUBBO // Dubbo接口扫描 + } + + // 结果数据模型 + public static class ScanResult { + private List projectResults = new ArrayList<>(); + + public void addProjectResult(ProjectScanResult result) { + projectResults.add(result); + } + + public List getProjectResults() { + return projectResults; + } + + public int getTotalMatches() { + return projectResults.stream() + .mapToInt(p -> p.getMatches().size()) + .sum(); + } + } + + public static class ProjectScanResult { + private String projectPath; + private List matches; + + public ProjectScanResult(String projectPath, List matches) { + this.projectPath = projectPath; + this.matches = matches; + } + + public String getProjectPath() { + return projectPath; + } + + public List getMatches() { + return matches; + } + } + + public static class MatchResult { + private String projectName; + private String filePath; + private int lineNumber; + private String lineContent; + private String matchText; + private String additionalInfo; + private ScanType scanType; + + // Getters and Setters + public String getProjectName() { + return projectName; + } + + public void setProjectName(String projectName) { + this.projectName = projectName; + } + + public String getFilePath() { + return filePath; + } + + public void setFilePath(String filePath) { + this.filePath = filePath; + } + + public int getLineNumber() { + return lineNumber; + } + + public void setLineNumber(int lineNumber) { + this.lineNumber = lineNumber; + } + + public String getLineContent() { + return lineContent; + } + + public void setLineContent(String lineContent) { + this.lineContent = lineContent; + } + + public String getMatchText() { + return matchText; + } + + public void setMatchText(String matchText) { + this.matchText = matchText; + } + + public String getAdditionalInfo() { + return additionalInfo; + } + + public void setAdditionalInfo(String additionalInfo) { + this.additionalInfo = additionalInfo; + } + + public ScanType getScanType() { + return scanType; + } + + public void setScanType(ScanType scanType) { + this.scanType = scanType; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/workutils/config/WorkUtilsProperties.java b/src/main/java/com/example/workutils/config/WorkUtilsProperties.java new file mode 100644 index 0000000..7b6c12f --- /dev/null +++ b/src/main/java/com/example/workutils/config/WorkUtilsProperties.java @@ -0,0 +1,108 @@ +package com.example.workutils.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +/** + * 工具集配置属性 + * 绑定application.yml或application.properties中以workutils为前缀的配置项 + * + * @author system + * @version 1.0 + */ +@Component +@ConfigurationProperties(prefix = "workutils") +@Data +public class WorkUtilsProperties { + + /** 基础目录,默认为D:/workspace/workutils */ + private String baseDirectory = "D:/workspace/workutils"; + + /** 配置文件目录,相对于基础目录 */ + private String configDirectory = "src/main/resources/config"; + + /** 导出文件根目录 */ + private String exportDirectory = "D:/workspace/workutils/export"; + + /** 项目路径配置文件名 */ + private String projectPathsFile = "代码仓库项目路径.txt"; + + /** 日期格式:yyyyMMdd */ + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd"); + + /** 时间格式:yyyyMMddHHmmss */ + private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + + /** + * 获取项目路径配置文件的完整路径 + * + * @return 项目路径配置文件路径 + */ + public Path getProjectPathsFilePath() { + return Paths.get(baseDirectory, configDirectory, projectPathsFile); + } + + /** + * 获取导出根目录 + * + * @return 导出根目录路径 + */ + public Path getExportBasePath() { + return Paths.get(exportDirectory); + } + + /** + * 获取当天的导出目录 + * 格式:导出根目录/yyyyMMdd/ + * + * @return 当天日期目录路径 + */ + public Path getTodayExportPath() { + String dateDir = LocalDate.now().format(DATE_FORMATTER); + return getExportBasePath().resolve(dateDir); + } + + /** + * 生成带时间戳的导出文件名 + * 格式:prefix_yyyyMMddHHmmss.extension + * + * @param prefix 文件名前缀 + * @param extension 文件扩展名 + * @return 带时间戳的文件名 + */ + public String generateExportFileName(String prefix, String extension) { + String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMATTER); + return String.format("%s_%s.%s", prefix, timestamp, extension); + } + + /** + * 解析导出文件的完整路径 + * 格式:导出根目录/yyyyMMdd/prefix_yyyyMMddHHmmss.extension + * + * @param fileName 文件名 + * @return 文件的完整路径 + */ + public Path resolveExportPath(String fileName) { + return getTodayExportPath().resolve(fileName); + } + + /** + * 解析导出文件的完整路径(带时间戳) + * 格式:导出根目录/yyyyMMdd/prefix_yyyyMMddHHmmss.extension + * + * @param prefix 文件名前缀 + * @param extension 文件扩展名 + * @return 文件的完整路径 + */ + public Path resolveExportPath(String prefix, String extension) { + String fileName = generateExportFileName(prefix, extension); + return getTodayExportPath().resolve(fileName); + } +} diff --git a/src/main/java/com/example/workutils/menu/api/MenuRunner.java b/src/main/java/com/example/workutils/menu/api/MenuRunner.java new file mode 100644 index 0000000..f17ec5a --- /dev/null +++ b/src/main/java/com/example/workutils/menu/api/MenuRunner.java @@ -0,0 +1,171 @@ +package com.example.workutils.menu.api; + +import com.example.workutils.menu.handler.AbstractMenuHandler; +import com.example.workutils.menu.handler.BatchHandler; +import com.example.workutils.menu.handler.GitHealthHandler; +import com.example.workutils.menu.handler.MyBatisHandler; +import com.example.workutils.menu.handler.ScanHandler; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Scanner; + +/** + * 菜单运行器 + * 统一管理菜单注册和执行 + */ +@Slf4j +@Component +public class MenuRunner { + + /** + * 主菜单选项 + */ + private static final String[] MAIN_MENU_OPTIONS = { + "1. 关键词扫描", + "2. MyBatis导出", + "3. 批量处理", + "4. Git仓库健康检测", + "0. 退出系统" + }; + + /** + * 菜单处理器映射 + */ + private final Map handlerMap = new HashMap<>(); + + private final ScanHandler scanHandler; + private final MyBatisHandler myBatisHandler; + private final BatchHandler batchHandler; + private final GitHealthHandler gitHealthHandler; + + public MenuRunner(ScanHandler scanHandler, + MyBatisHandler myBatisHandler, + BatchHandler batchHandler, + GitHealthHandler gitHealthHandler) { + this.scanHandler = scanHandler; + this.myBatisHandler = myBatisHandler; + this.batchHandler = batchHandler; + this.gitHealthHandler = gitHealthHandler; + + // 注册菜单处理器 + registerHandlers(); + } + + /** + * 注册菜单处理器 + */ + private void registerHandlers() { + handlerMap.put(1, scanHandler); + handlerMap.put(2, myBatisHandler); + handlerMap.put(3, batchHandler); + handlerMap.put(4, gitHealthHandler); + + info("菜单处理器注册完成"); + } + + /** + * 运行主菜单 + * + * @param scanner 控制台输入扫描器 + */ + public void run(Scanner scanner) { + info("启动主菜单"); + while (true) { + printMenuTitle("主菜单"); + displayMenu(MAIN_MENU_OPTIONS); + + int choice = readChoice(scanner); + scanner.nextLine(); + + if (choice == 0) { + info("用户退出系统"); + System.out.println("感谢使用,再见!"); + break; + } + + handleChoice(choice, scanner); + } + } + + /** + * 处理用户选择 + */ + private void handleChoice(int choice, Scanner scanner) { + AbstractMenuHandler handler = handlerMap.get(choice); + if (handler != null) { + try { + handler.execute(scanner); + } catch (Exception e) { + error("菜单执行异常", e); + printError(e.getMessage()); + } + } else { + printInvalidOption(); + } + } + + /** + * 显示菜单标题 + */ + private void printMenuTitle(String title) { + System.out.println(); + System.out.println("========== " + title + " =========="); + } + + /** + * 显示菜单选项 + */ + private void displayMenu(String[] options) { + System.out.println("请选择操作:"); + Arrays.stream(options).forEach(System.out::println); + } + + /** + * 读取用户选择 + */ + private int readChoice(Scanner scanner) { + try { + System.out.print("请输入选项编号: "); + return scanner.nextInt(); + } catch (Exception e) { + return -1; + } + } + + /** + * 打印无效选项提示 + */ + private void printInvalidOption() { + System.out.println("无效选项"); + } + + /** + * 打印错误信息 + */ + private void printError(String message) { + System.out.println("操作失败: " + message); + } + + /** + * 记录信息日志 + */ + private void info(String message, Object... args) { + if (args.length > 0) { + log.info(message, args); + } else { + log.info(message); + } + } + + /** + * 记录错误日志 + */ + private void error(String message, Exception e) { + log.error(message, e); + } +} diff --git a/src/main/java/com/example/workutils/menu/handler/AbstractMenuHandler.java b/src/main/java/com/example/workutils/menu/handler/AbstractMenuHandler.java new file mode 100644 index 0000000..eea056a --- /dev/null +++ b/src/main/java/com/example/workutils/menu/handler/AbstractMenuHandler.java @@ -0,0 +1,116 @@ +package com.example.workutils.menu.handler; + +import lombok.extern.slf4j.Slf4j; + +import java.util.Scanner; + +/** + * 菜单处理器抽象基类 + * 所有菜单处理器需继承此类 + */ +@Slf4j +public abstract class AbstractMenuHandler { + + /** + * 执行菜单处理 + * 子类需实现此方法定义具体的菜单逻辑 + * + * @param scanner 控制台输入扫描器 + */ + public abstract void execute(Scanner scanner); + + /** + * 获取菜单组名称 + * + * @return 菜单组名称 + */ + public abstract String getMenuName(); + + /** + * 获取菜单选项数组 + * 子类需实现此方法返回菜单选项 + * + * @return 菜单选项字符串数组 + */ + public abstract String[] getMenuOptions(); + + /** + * 打印分隔线 + */ + protected void printSeparator() { + System.out.println(); + } + + /** + * 打印菜单标题 + * + * @param title 菜单标题 + */ + protected void printMenuTitle(String title) { + printSeparator(); + System.out.println("========== " + title + " =========="); + } + + /** + * 打印操作成功信息 + * + * @param operation 操作名称 + */ + protected void printSuccess(String operation) { + System.out.println(operation + "完成!"); + } + + /** + * 打印错误信息 + * + * @param message 错误消息 + */ + protected void printError(String message) { + System.out.println("操作失败: " + message); + } + + /** + * 打印无效选项提示 + */ + protected void printInvalidOption() { + System.out.println("无效选项"); + } + + /** + * 记录信息日志 + * + * @param message 日志消息 + */ + protected void info(String message) { + log.info(message); + } + + /** + * 记录信息日志(带参数) + * + * @param message 日志消息模板 + * @param args 参数 + */ + protected void info(String message, Object... args) { + log.info(message, args); + } + + /** + * 记录错误日志 + * + * @param message 错误消息 + */ + protected void error(String message) { + log.error(message); + } + + /** + * 记录错误日志及异常 + * + * @param message 错误消息 + * @param e 异常 + */ + protected void error(String message, Exception e) { + log.error(message, e); + } +} diff --git a/src/main/java/com/example/workutils/menu/handler/BatchHandler.java b/src/main/java/com/example/workutils/menu/handler/BatchHandler.java new file mode 100644 index 0000000..33a9165 --- /dev/null +++ b/src/main/java/com/example/workutils/menu/handler/BatchHandler.java @@ -0,0 +1,197 @@ +package com.example.workutils.menu.handler; + +import com.example.workutils.biz.mybatis.export.MyBatisExportService; +import com.example.workutils.biz.scanner.service.ScanService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +/** + * 批量处理功能处理器 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class BatchHandler extends AbstractMenuHandler { + + /** + * 可执行异常的操作接口 + */ + @FunctionalInterface + public interface ExceptionRunnable { + void run() throws Exception; + } + + private static final String[] MENU_OPTIONS = { + "1. 执行所有检查", + "2. 自定义组合检查", + "0. 返回上级菜单" + }; + + private static final String[] CUSTOM_CHECK_OPTIONS = { + "1. SQL导出", + "2. Dubbo接口扫描", + "3. Redis操作扫描", + "4. 关键词扫描" + }; + + private final ScanService scanService; + private final MyBatisExportService myBatisExportService; + + @Override + public String getMenuName() { + return "批量处理"; + } + + @Override + public String[] getMenuOptions() { + return MENU_OPTIONS; + } + + @Override + public void execute(Scanner scanner) { + while (true) { + printMenuTitle(getMenuName()); + displayMenu(getMenuOptions()); + + int choice = readChoice(scanner); + scanner.nextLine(); + + if (choice == 0) { + info("返回上级菜单"); + return; + } + + try { + switch (choice) { + case 1: + executeAllChecks(); + break; + case 2: + executeCustomChecks(scanner); + break; + default: + printInvalidOption(); + break; + } + } catch (Exception e) { + error("批量处理操作失败", e); + printError(e.getMessage()); + } + } + } + + /** + * 执行所有检查 + */ + private void executeAllChecks() throws Exception { + printMenuTitle("执行所有检查"); + info("开始执行所有检查"); + + executeStep("1. 导出SQL...", () -> myBatisExportService.exportSQL()); + executeStep("2. 扫描Dubbo接口...", + () -> scanService.scanDubboInterfacesWithDefaultName("@Reference|@Service| scanService.scanRedisKeysWithDefaultName("redis\\.opsFor")); + + printSuccess("所有检查"); + info("所有检查执行完成"); + } + + /** + * 执行自定义检查 + */ + private void executeCustomChecks(Scanner scanner) throws Exception { + printMenuTitle("自定义组合检查"); + displayMenu(CUSTOM_CHECK_OPTIONS); + + System.out.print("请选择要执行的检查(多选用逗号分隔,如1,2,3): "); + String choices = scanner.nextLine().trim(); + List choiceList = parseCommaSeparatedInput(choices); + + info("用户选择执行检查项: {}", choiceList); + + for (String choice : choiceList) { + try { + switch (choice) { + case "1": + executeStep("执行SQL导出...", () -> myBatisExportService.exportSQL()); + break; + case "2": + executeStep("执行Dubbo接口扫描...", + () -> scanService.scanDubboInterfacesWithDefaultName("@Reference|@Service")); + break; + case "3": + executeStep("执行Redis操作扫描...", + () -> scanService.scanRedisKeysWithDefaultName("redis\\.opsFor")); + break; + case "4": + System.out.print("请输入关键词: "); + String keyword = scanner.nextLine().trim(); + executeStep("执行关键词扫描...", + () -> scanService.scanKeywordsWithDefaultName(keyword)); + break; + default: + System.out.println("跳过无效选项: " + choice); + break; + } + } catch (Exception e) { + error("执行检查项失败: " + choice, e); + System.out.println("检查项 " + choice + " 执行失败: " + e.getMessage()); + } + } + printSuccess("自定义检查"); + } + + /** + * 执行单个步骤 + */ + private void executeStep(String stepName, ExceptionRunnable action) throws Exception { + System.out.println(stepName); + action.run(); + } + + /** + * 解析逗号分隔的输入 + */ + private List parseCommaSeparatedInput(String input) { + List result = new ArrayList<>(); + if (input == null || input.trim().isEmpty()) { + return result; + } + String[] parts = input.split(","); + for (String part : parts) { + String trimmed = part.trim(); + if (!trimmed.isEmpty()) { + result.add(trimmed); + } + } + return result; + } + + /** + * 显示菜单选项 + */ + private void displayMenu(String[] options) { + System.out.println("请选择操作:"); + for (String option : options) { + System.out.println(option); + } + } + + /** + * 读取用户选择 + */ + private int readChoice(Scanner scanner) { + try { + System.out.print("请输入选项编号: "); + return scanner.nextInt(); + } catch (Exception e) { + return -1; + } + } +} diff --git a/src/main/java/com/example/workutils/menu/handler/GitHealthHandler.java b/src/main/java/com/example/workutils/menu/handler/GitHealthHandler.java new file mode 100644 index 0000000..9b953b6 --- /dev/null +++ b/src/main/java/com/example/workutils/menu/handler/GitHealthHandler.java @@ -0,0 +1,240 @@ +package com.example.workutils.menu.handler; + +import com.example.workutils.biz.git.service.GitService; +import com.example.workutils.biz.git.util.GitCommandExecutor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.stream.Collectors; + +/** + * Git仓库健康检测处理器 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class GitHealthHandler extends AbstractMenuHandler { + + private static final String[] MENU_OPTIONS = { + "1. 检测所有仓库健康状态", + "2. 修复损坏仓库", + "0. 返回上级菜单" + }; + + private final GitService gitService; + + @Override + public String getMenuName() { + return "Git仓库健康检测"; + } + + @Override + public String[] getMenuOptions() { + return MENU_OPTIONS; + } + + @Override + public void execute(Scanner scanner) { + while (true) { + printMenuTitle(getMenuName()); + displayMenu(getMenuOptions()); + + int choice = readChoice(scanner); + scanner.nextLine(); + + if (choice == 0) { + info("返回上级菜单"); + return; + } + + try { + switch (choice) { + case 1: + checkAllReposHealth(scanner); + break; + case 2: + fixCorruptedRepos(scanner); + break; + default: + printInvalidOption(); + break; + } + } catch (Exception e) { + error("Git仓库健康检测操作失败", e); + printError(e.getMessage()); + } + } + } + + /** + * 检测所有仓库健康状态 + */ + private void checkAllReposHealth(Scanner scanner) { + printMenuTitle("开始检测所有仓库"); + info("开始检测所有仓库健康状态"); + + List results = gitService.checkCorruptedRepos(scanner); + + if (results.isEmpty()) { + System.out.println("所有仓库状态正常!"); + info("所有仓库状态正常"); + } else { + System.out.println("\n发现 " + results.size() + " 个损坏的仓库:"); + for (int i = 0; i < results.size(); i++) { + GitCommandExecutor.RepoHealthResult result = results.get(i); + System.out.printf("[%d] %s - %s%n", i + 1, + extractProjectName(result.getRepoPath()), + result.getCorruptionReason()); + } + info("检测完成,发现 {} 个损坏仓库", results.size()); + } + } + + /** + * 修复损坏仓库 + */ + private void fixCorruptedRepos(Scanner scanner) { + List corruptedRepos = gitService.listCorruptedRepos(); + + if (corruptedRepos.isEmpty()) { + System.out.println("没有损坏的仓库需要修复。"); + System.out.println("请先执行'检测所有仓库健康状态'来检测仓库。"); + return; + } + + // 显示损坏仓库列表 + printMenuTitle("损坏仓库列表"); + for (int i = 0; i < corruptedRepos.size(); i++) { + System.out.printf("[%d] %s%n", i + 1, corruptedRepos.get(i)); + } + + // 获取用户输入 + System.out.print("\n请输入要修复的仓库序号(多个用逗号分隔,如1,2,3,输入a修复全部): "); + String input = scanner.nextLine().trim(); + + List selectedIndices = parseSelectedIndices(input, corruptedRepos.size()); + + if (selectedIndices.isEmpty()) { + System.out.println("未选择任何仓库。"); + return; + } + + // 确认修复 + System.out.print("确认修复 " + selectedIndices.size() + " 个仓库?(y/n): "); + String confirm = scanner.nextLine().trim(); + if (!"y".equalsIgnoreCase(confirm)) { + System.out.println("已取消修复操作。"); + info("用户取消修复操作"); + return; + } + + // 执行修复 + info("开始修复 {} 个仓库", selectedIndices.size()); + int successCount = 0; + int failCount = 0; + for (Integer index : selectedIndices) { + String repoPath = corruptedRepos.get(index - 1); + System.out.println("\n正在修复: " + extractProjectName(repoPath)); + + boolean success = gitService.fixCorruptedRepo(repoPath); + if (success) { + System.out.println("修复成功: " + repoPath); + successCount++; + } else { + System.out.println("修复失败: " + repoPath); + failCount++; + } + } + + printMenuTitle("修复完成"); + System.out.println("成功: " + successCount); + System.out.println("失败: " + failCount); + info("修复完成,成功: {},失败: {}", successCount, failCount); + } + + /** + * 解析用户选择的仓库序号 + * + * @param input 用户输入 + * @param repoCount 仓库总数 + * @return 选择的序号列表 + */ + private List parseSelectedIndices(String input, int repoCount) { + if ("a".equalsIgnoreCase(input)) { + List indices = new ArrayList<>(); + for (int i = 1; i <= repoCount; i++) { + indices.add(i); + } + return indices; + } + + return parseCommaSeparatedInput(input).stream() + .map(s -> { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + return -1; + } + }) + .filter(i -> i >= 1 && i <= repoCount) + .collect(Collectors.toList()); + } + + /** + * 解析逗号分隔的输入 + * + * @param input 逗号分隔的字符串 + * @return 解析后的字符串列表 + */ + private List parseCommaSeparatedInput(String input) { + List result = new ArrayList<>(); + if (input == null || input.trim().isEmpty()) { + return result; + } + String[] parts = input.split(","); + for (String part : parts) { + String trimmed = part.trim(); + if (!trimmed.isEmpty()) { + result.add(trimmed); + } + } + return result; + } + + /** + * 从路径中提取项目名称 + */ + private String extractProjectName(String projectPath) { + if (projectPath == null || projectPath.isEmpty()) { + return "unknown"; + } + int lastSep = Math.max(projectPath.lastIndexOf('/'), projectPath.lastIndexOf('\\')); + return lastSep >= 0 ? projectPath.substring(lastSep + 1) : projectPath; + } + + /** + * 显示菜单选项 + */ + private void displayMenu(String[] options) { + System.out.println("请选择操作:"); + for (String option : options) { + System.out.println(option); + } + } + + /** + * 读取用户选择 + */ + private int readChoice(Scanner scanner) { + try { + System.out.print("请输入选项编号: "); + return scanner.nextInt(); + } catch (Exception e) { + return -1; + } + } +} diff --git a/src/main/java/com/example/workutils/menu/handler/MyBatisHandler.java b/src/main/java/com/example/workutils/menu/handler/MyBatisHandler.java new file mode 100644 index 0000000..0930309 --- /dev/null +++ b/src/main/java/com/example/workutils/menu/handler/MyBatisHandler.java @@ -0,0 +1,173 @@ +package com.example.workutils.menu.handler; + +import com.example.workutils.biz.mybatis.export.MyBatisExportService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Scanner; + +/** + * MyBatis导出功能处理器 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class MyBatisHandler extends AbstractMenuHandler { + + private static final String[] MENU_OPTIONS = { + "1. 导出所有SQL", + "2. 导出指定表名的所有SQL", + "3. 导出多个指定表名的所有SQL", + "4. 导出包含指定关键字的SQL", + "5. 导出指定SQL标签ID的SQL", + "0. 返回上级菜单" + }; + + private final MyBatisExportService myBatisExportService; + + @Override + public String getMenuName() { + return "MyBatis SQL导出"; + } + + @Override + public String[] getMenuOptions() { + return MENU_OPTIONS; + } + + @Override + public void execute(Scanner scanner) { + while (true) { + printMenuTitle(getMenuName()); + displayMenu(getMenuOptions()); + + int choice = readChoice(scanner); + scanner.nextLine(); + + if (choice == 0) { + info("返回上级菜单"); + return; + } + + try { + switch (choice) { + case 1: + exportAllSql(); + break; + case 2: + exportByTableName(scanner); + break; + case 3: + exportByTableNames(scanner); + break; + case 4: + exportByKeyword(scanner); + break; + case 5: + exportByTagId(scanner); + break; + default: + printInvalidOption(); + break; + } + } catch (Exception e) { + error("MyBatis导出执行失败", e); + printError(e.getMessage()); + } + } + } + + /** + * 导出所有SQL + */ + private void exportAllSql() throws Exception { + printMenuTitle("导出所有SQL"); + info("开始导出所有SQL"); + myBatisExportService.exportSQL(); + printSuccess("导出所有SQL"); + } + + /** + * 按表名导出 + */ + private void exportByTableName(Scanner scanner) throws Exception { + System.out.print("请输入表名: "); + String tableName = scanner.nextLine().trim(); + if (tableName.isEmpty()) { + System.out.println("表名不能为空"); + return; + } + info("按表名导出SQL: {}", tableName); + myBatisExportService.exportSQLByTable(tableName); + printSuccess("按表名导出SQL"); + } + + /** + * 按多个表名导出 + */ + private void exportByTableNames(Scanner scanner) throws Exception { + System.out.print("请输入表名(多个用逗号分隔): "); + String tableNames = scanner.nextLine().trim(); + if (tableNames.isEmpty()) { + System.out.println("表名不能为空"); + return; + } + info("按多个表名导出SQL: {}", tableNames); + myBatisExportService.exportSQLByTables(Arrays.asList(tableNames.split(","))); + printSuccess("按多个表名导出SQL"); + } + + /** + * 按关键字导出 + */ + private void exportByKeyword(Scanner scanner) throws Exception { + System.out.print("请输入关键字: "); + String keyword = scanner.nextLine().trim(); + if (keyword.isEmpty()) { + System.out.println("关键字不能为空"); + return; + } + info("按关键字导出SQL: {}", keyword); + myBatisExportService.exportSQLByKeyword(keyword); + printSuccess("按关键字导出SQL"); + } + + /** + * 按标签ID导出 + */ + private void exportByTagId(Scanner scanner) throws Exception { + System.out.print("请输入SQL标签ID: "); + String tagId = scanner.nextLine().trim(); + if (tagId.isEmpty()) { + System.out.println("标签ID不能为空"); + return; + } + info("按标签ID导出SQL: {}", tagId); + myBatisExportService.exportSQLByTagId(tagId); + printSuccess("按标签ID导出SQL"); + } + + /** + * 显示菜单选项 + */ + private void displayMenu(String[] options) { + System.out.println("请选择操作:"); + for (String option : options) { + System.out.println(option); + } + } + + /** + * 读取用户选择 + */ + private int readChoice(Scanner scanner) { + try { + System.out.print("请输入选项编号: "); + return scanner.nextInt(); + } catch (Exception e) { + return -1; + } + } +} diff --git a/src/main/java/com/example/workutils/menu/handler/ScanHandler.java b/src/main/java/com/example/workutils/menu/handler/ScanHandler.java new file mode 100644 index 0000000..4ed8ee2 --- /dev/null +++ b/src/main/java/com/example/workutils/menu/handler/ScanHandler.java @@ -0,0 +1,107 @@ +package com.example.workutils.menu.handler; + +import com.example.workutils.biz.scanner.service.ScanService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Scanner; + +/** + * 扫描功能处理器 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ScanHandler extends AbstractMenuHandler { + + private static final String[] MENU_OPTIONS = { + "1. 关键词扫描", + "0. 返回上级菜单" + }; + + private final ScanService scanService; + + @Override + public String getMenuName() { + return "扫描功能"; + } + + @Override + public String[] getMenuOptions() { + return MENU_OPTIONS; + } + + @Override + public void execute(Scanner scanner) { + while (true) { + printMenuTitle(getMenuName()); + displayMenu(getMenuOptions()); + + int choice = readChoice(scanner); + scanner.nextLine(); + + if (choice == 0) { + info("返回上级菜单"); + return; + } + + try { + switch (choice) { + case 1: + scanByKeyword(scanner); + break; + default: + printInvalidOption(); + break; + } + } catch (Exception e) { + error("扫描功能执行失败", e); + printError(e.getMessage()); + } + } + } + + /** + * 关键词扫描 + */ + private void scanByKeyword(Scanner scanner) throws Exception { + printMenuTitle("关键词扫描"); + info("开始关键词扫描"); + + System.out.print("请输入关键词: "); + String keyword = scanner.nextLine().trim(); + + if (keyword.isEmpty()) { + System.out.println("关键词不能为空"); + return; + } + + info("扫描关键词: {}", keyword); + scanService.scanKeywordsWithDefaultName(keyword); + + printSuccess("关键词扫描"); + } + + /** + * 显示菜单选项 + */ + private void displayMenu(String[] options) { + System.out.println("请选择操作:"); + for (String option : options) { + System.out.println(option); + } + } + + /** + * 读取用户选择 + */ + private int readChoice(Scanner scanner) { + try { + System.out.print("请输入选项编号: "); + return scanner.nextInt(); + } catch (Exception e) { + return -1; + } + } +} diff --git a/src/main/java/com/example/workutils/menu/model/MenuGroup.java b/src/main/java/com/example/workutils/menu/model/MenuGroup.java new file mode 100644 index 0000000..e8058a6 --- /dev/null +++ b/src/main/java/com/example/workutils/menu/model/MenuGroup.java @@ -0,0 +1,68 @@ +package com.example.workutils.menu.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 菜单组 + * 定义一组相关的菜单选项 + */ +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class MenuGroup { + + /** + * 菜单组名称 + */ + private String name; + + /** + * 菜单选项列表 + */ + private List items; + + /** + * 静态工厂方法:从字符串数组创建菜单组 + * + * @param name 菜单组名称 + * @param lines 菜单选项字符串数组,如 "1. 选项一", "0. 返回" + */ + public static MenuGroup of(String name, String[] lines) { + List items = Arrays.stream(lines) + .filter(line -> line != null && !line.trim().isEmpty()) + .map(line -> { + String[] parts = line.trim().split("\\.", 2); + Integer code = Integer.parseInt(parts[0].trim()); + String description = parts.length > 1 ? parts[1].trim() : ""; + return new MenuItem(code, description, null); + }) + .collect(Collectors.toList()); + return new MenuGroup(name, items); + } + + /** + * 获取菜单选项数量 + */ + public int size() { + return items != null ? items.size() : 0; + } + + /** + * 根据选项编号获取菜单项 + */ + public MenuItem getItem(Integer code) { + if (items == null || code == null) { + return null; + } + return items.stream() + .filter(item -> code.equals(item.getCode())) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/com/example/workutils/menu/model/MenuItem.java b/src/main/java/com/example/workutils/menu/model/MenuItem.java new file mode 100644 index 0000000..a791483 --- /dev/null +++ b/src/main/java/com/example/workutils/menu/model/MenuItem.java @@ -0,0 +1,30 @@ +package com.example.workutils.menu.model; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 菜单项 + * 表示菜单中的一个选项 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class MenuItem { + + /** + * 菜单选项编号 + */ + private Integer code; + + /** + * 菜单选项描述 + */ + private String description; + + /** + * 菜单选项处理器类名 + */ + private String handlerClass; +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..e7a77cb --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,16 @@ +spring: + application: + name: workutils + +workutils: + base-directory: "D:/workspace/workutils" + config-directory: "src/main/resources/config" + export-directory: "D:/workspace/workutils/export" + project-paths-file: "代码仓库项目路径.txt" + +logging: + level: + com.example.workutils: INFO + org.springframework: WARN + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" \ No newline at end of file diff --git a/src/main/resources/config/代码仓库项目路径.txt b/src/main/resources/config/代码仓库项目路径.txt new file mode 100644 index 0000000..3988060 --- /dev/null +++ b/src/main/resources/config/代码仓库项目路径.txt @@ -0,0 +1,72 @@ +D:/bestpay-srdcloud/ft-compensatory-core|NORMAL +D:/bestpay-srdcloud/ft-creative-prod|NORMAL +D:/bestpay-srdcloud/ft-credit-prod|NORMAL +D:/bestpay-srdcloud/ft-customer-core|NORMAL +D:/bestpay-srdcloud/ft-file-core|NORMAL +D:/bestpay-srdcloud/ft-finflux-gw-adapter|NORMAL +D:/bestpay-srdcloud/ft-lending-management-core|NORMAL +D:/bestpay-srdcloud/ft-loancapital-core|NORMAL +D:/bestpay-srdcloud/ft-loan-core|NORMAL +D:/bestpay-srdcloud/ft-organmng-core|NORMAL +D:/bestpay-srdcloud/ft-orgsupport-weixin|NORMAL +D:/bestpay-srdcloud/ft-protocol-core|NORMAL +D:/bestpay-srdcloud/ft-repay-core|NORMAL +D:/bestpay-srdcloud/ft-risk-control-core|NORMAL +D:/bestpay-srdcloud/ft-settlement-core|NORMAL +D:/bestpay-srdcloud/ft-wechatstage-prod|NORMAL +D:/bestpay-srdcloud/cf-agent-admin|NORMAL +D:/bestpay-srdcloud/cf-asset-core|NORMAL +D:/bestpay-srdcloud/cf-batch-creditpayment|NORMAL +D:/bestpay-srdcloud/cf-batch-creditpayment-plus|NORMAL +D:/bestpay-srdcloud/cf-blockchain-prod|CORRUPTED +D:/bestpay-srdcloud/cf-boss|CORRUPTED +D:/bestpay-srdcloud/cf-clrstl-fee-core|NORMAL +D:/bestpay-srdcloud/cf-clrstl-report-core|NORMAL +D:/bestpay-srdcloud/cf-codeutils|NORMAL +D:/bestpay-srdcloud/cf-codeutils(1)|CORRUPTED +D:/bestpay-srdcloud/cf-collateral-core|NORMAL +D:/bestpay-srdcloud/cf-commodity-core|NORMAL +D:/bestpay-srdcloud/cf-commonservice|NORMAL +D:/bestpay-srdcloud/cf-consumestage-core|CORRUPTED +D:/bestpay-srdcloud/cf-consumestage-mall|NORMAL +D:/bestpay-srdcloud/cf-credit-bizfront|CORRUPTED +D:/bestpay-srdcloud/cf-credit-bizfront-plus|NORMAL +D:/bestpay-srdcloud/cf-credit-front|NORMAL +D:/bestpay-srdcloud/cf-credit-prod|CORRUPTED +D:/bestpay-srdcloud/cf-credit-prod-batch|NORMAL +D:/bestpay-srdcloud/cf-credit-prod-manage|NORMAL +D:/bestpay-srdcloud/cf-creditstagescore|NORMAL +D:/bestpay-srdcloud/cf-credituse-core|NORMAL +D:/bestpay-srdcloud/cf-ctcorp-core|NORMAL +D:/bestpay-srdcloud/cf-customer-core|NORMAL +D:/bestpay-srdcloud/cf-file-core|CORRUPTED +D:/bestpay-srdcloud/cf-govent-prod|CORRUPTED +D:/bestpay-srdcloud/cf-grant-core|CORRUPTED +D:/bestpay-srdcloud/cf-info-collection|NORMAL +D:/bestpay-srdcloud/cf-instalment-admin|CORRUPTED +D:/bestpay-srdcloud/cf-instalment-batch|CORRUPTED +D:/bestpay-srdcloud/cf-instalment-mix|CORRUPTED +D:/bestpay-srdcloud/cf-invoice-core|NORMAL +D:/bestpay-srdcloud/cf-lending-management-core|NORMAL +D:/bestpay-srdcloud/cf-loancapital-core|NORMAL +D:/bestpay-srdcloud/cf-marketing-manage|CORRUPTED +D:/bestpay-srdcloud/cf-marketing-manage-plus|NORMAL +D:/bestpay-srdcloud/cf-onlinestage-prod|NORMAL +D:/bestpay-srdcloud/cf-organmng-core|NORMAL +D:/bestpay-srdcloud/cf-precredit-core|NORMAL +D:/bestpay-srdcloud/cf-productcfg-core-plus|NORMAL +D:/bestpay-srdcloud/cf-protocol-core|NORMAL +D:/bestpay-srdcloud/cf-risk-control-core|CORRUPTED +D:/bestpay-srdcloud/cf-risktool|CORRUPTED +D:/bestpay-srdcloud/cf-scheduler|NORMAL +D:/bestpay-srdcloud/cf-search|NORMAL +D:/bestpay-srdcloud/cf-settlement-core|NORMAL +D:/bestpay-srdcloud/cf-thirdpartybiz-prod|CORRUPTED +D:/bestpay-srdcloud/cf-trade-core|CORRUPTED +D:/bestpay-srdcloud/cf-wechatstage-prod|NORMAL +D:/bestpay-srdcloud/ft-approval-core|NORMAL +D:/bestpay-srdcloud/ft-asset-core|NORMAL +D:/bestpay-srdcloud/ft-blockchain-prod|NORMAL +D:/bestpay-srdcloud/ft-collateral-core|NORMAL +D:/bestpay-srdcloud/ft-commodity-core|NORMAL +D:/bestpay-srdcloud/ft-commonservice|NORMAL \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..51cecd8 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + \ No newline at end of file diff --git a/项目说明.md b/项目说明.md new file mode 100644 index 0000000..c1125fa --- /dev/null +++ b/项目说明.md @@ -0,0 +1,166 @@ +# 项目说明 + +## 项目概述 + +**项目名称**: workutils +**技术栈**: Spring Boot 3.2.3 + Java 21 + Maven +**项目类型**: Java工作工具集项目 +**主要用途**: 金融科技(FinTech)系统的开发和维护工具集 + +## 主要功能模块 + +### 1. SQL代码分析工具 + +- MyBatis XML文件解析和SQL提取 +- SQL语法检查和质量分析 +- 数据库表引用扫描 +- 支持跨项目批量扫描 +- 问题SQL检测和报告生成 + +### 2. 代码扫描工具 + +- 关键字搜索和目录扫描 +- Dubbo服务接口解析 +- Redis键值对扫描 +- 配置文件对比分析 +- 项目依赖关系分析 + +### 3. 文件处理工具 + +- Markdown转Excel转换器 +- CSV/Excel文件批量处理 +- 文件批量操作和过滤 +- 数据格式转换 + +### 4. Git操作工具 + +- 分支管理和对比 +- 提交历史分析 +- 版本控制自动化 +- 代码差异检测 + +### 5. 配置管理工具 + +- Apollo配置对比 +- 环境配置分析 +- 数据库表清单管理 +- 跨环境配置差异检查 + +## 核心功能说明 + +### MyBatis SQL分析模块 + +- 解析MyBatis XML映射文件 +- 提取SQL语句并进行语法检查 +- 识别潜在的性能问题 +- 生成标准化的SQL报告 + +### Dubbo服务扫描 + +- 自动解析Dubbo注解和XML配置 +- 提取服务提供者和消费者信息 +- 生成服务接口清单 +- 支持多项目并行扫描 + +### 关键字检索系统 + +- 支持全目录递归搜索 +- 多种关键字匹配模式 +- 生成详细的检索报告 +- 支持正则表达式搜索 + +## 应用场景 + +主要用于**金融系统迁移、数据库重构、代码审查**等场景,包括: + +- **系统架构升级**: 代码审计和依赖分析 +- **数据库迁移**: SQL质量检查和优化 +- **微服务架构**: 服务依赖关系梳理 +- **生产环境**: 配置管理和对比分析 +- **代码审查**: 自动化代码质量检查 + +## 技术特点 + +### 企业级特性 + +- 针对金融系统特点定制 +- 支持大规模代码库扫描 +- 高并发处理能力 +- 企业级日志和错误处理 + +### 批量处理能力 + +- 支持多项目并行扫描 +- 自动化的批量操作 +- 性能优化的数据处理 + +### 标准化输出 + +- 生成标准格式的报告文件(CSV/Excel) +- 结构化的数据输出 +- 易于集成到CI/CD流程 + +## 项目结构 + +``` +workutils/ +├── src/main/java/com/example/workutils/ +│ ├── advanceinspect/ # 高级检查工具 +│ ├── dubboscan/ # Dubbo扫描工具 +│ ├── keywordscan/ # 关键字扫描工具 +│ ├── model/ # 数据模型 +│ ├── utils/ # 工具类 +│ └── deprecated/ # 弃用代码 +├── src/main/resources/ +│ ├── config/ # 配置文件 +│ ├── export/ # 输出文件目录 +│ └── compare/ # 对比文件 +└── pom.xml # Maven配置 +``` + +## 依赖说明 + +项目使用的主要依赖包括: + +- **Spring Boot**: 基础框架 +- **MyBatis**: SQL解析 +- **EasyExcel**: Excel文件处理 +- **JGit**: Git操作 +- **Dubbo**: 服务扫描 +- **Apache Commons**: 工具库 + +## 使用方式 + +### 快速开始 + +```bash +# 编译项目 +mvn clean compile + +# 运行特定工具 +java -cp target/classes com.example.workutils.advanceinspect.AllProjectSimpleXmlSQLExportorV5 +``` + +### 配置说明 + +- 项目路径配置在 `src/main/resources/config/代码仓库项目路径.txt` +- 输出文件默认生成在 `src/main/resources/export/` 目录 +- 支持自定义扫描规则和过滤器 + +## 输出文件格式 + +项目生成的标准输出格式包括: + +- **CSV文件**: 适用于数据分析和导入 +- **Excel文件**: 提供可视化报告 +- **日志文件**: 详细的操作记录 + +## 注意事项 + +- 项目需要访问实际的代码仓库路径 +- 部分功能需要配置相应的环境变量 +- 建议在生产环境使用前进行充分测试 + +--- + +*这是一个高度专业化的内部工具集,主要服务于金融科技公司的开发和运维团队。* \ No newline at end of file