commit e281b21bfd0f598f57283a718a33117f576bf834 Author: 张家训 Date: Thu Feb 26 18:13:37 2026 +0800 提交代码 diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..cb28b0e Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ 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