Commit f3eace43b81d305ca3d1e9a373ab3367d614a5eb
1 parent
3af94dd2
1111
Showing
99 changed files
with
10891 additions
and
0 deletions
Too many changes to show.
To preserve performance only 63 of 99 files are displayed.
.gitignore
0 → 100644
| 1 | +HELP.md | |
| 2 | +target/ | |
| 3 | +.mvn/wrapper/maven-wrapper.jar | |
| 4 | +!**/src/main/**/target/ | |
| 5 | +!**/src/test/**/target/ | |
| 6 | + | |
| 7 | +### STS ### | |
| 8 | +.apt_generated | |
| 9 | +.classpath | |
| 10 | +.factorypath | |
| 11 | +.project | |
| 12 | +.settings | |
| 13 | +.springBeans | |
| 14 | +.sts4-cache | |
| 15 | + | |
| 16 | +### IntelliJ IDEA ### | |
| 17 | +.idea | |
| 18 | +*.iws | |
| 19 | +*.iml | |
| 20 | +*.ipr | |
| 21 | + | |
| 22 | +### NetBeans ### | |
| 23 | +/nbproject/private/ | |
| 24 | +/nbbuild/ | |
| 25 | +/dist/ | |
| 26 | +/nbdist/ | |
| 27 | +/.nb-gradle/ | |
| 28 | +build/ | |
| 29 | +!**/src/main/**/build/ | |
| 30 | +!**/src/test/**/build/ | |
| 31 | + | |
| 32 | +### VS Code ### | |
| 33 | +.vscode/ | ... | ... |
.mvn/wrapper/maven-wrapper.properties
0 → 100644
logPath_IS_UNDEFINED/2026-02-09/2026-02-09.debug-0.log
0 → 100644
logPath_IS_UNDEFINED/2026-02-09/2026-02-09.error-0.log
0 → 100644
logPath_IS_UNDEFINED/2026-02-09/2026-02-09.info-0.log
0 → 100644
mvnw
0 → 100644
| 1 | +#!/bin/sh | |
| 2 | +# ---------------------------------------------------------------------------- | |
| 3 | +# Licensed to the Apache Software Foundation (ASF) under one | |
| 4 | +# or more contributor license agreements. See the NOTICE file | |
| 5 | +# distributed with this work for additional information | |
| 6 | +# regarding copyright ownership. The ASF licenses this file | |
| 7 | +# to you under the Apache License, Version 2.0 (the | |
| 8 | +# "License"); you may not use this file except in compliance | |
| 9 | +# with the License. You may obtain a copy of the License at | |
| 10 | +# | |
| 11 | +# http://www.apache.org/licenses/LICENSE-2.0 | |
| 12 | +# | |
| 13 | +# Unless required by applicable law or agreed to in writing, | |
| 14 | +# software distributed under the License is distributed on an | |
| 15 | +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| 16 | +# KIND, either express or implied. See the License for the | |
| 17 | +# specific language governing permissions and limitations | |
| 18 | +# under the License. | |
| 19 | +# ---------------------------------------------------------------------------- | |
| 20 | + | |
| 21 | +# ---------------------------------------------------------------------------- | |
| 22 | +# Apache Maven Wrapper startup batch script, version 3.3.4 | |
| 23 | +# | |
| 24 | +# Optional ENV vars | |
| 25 | +# ----------------- | |
| 26 | +# JAVA_HOME - location of a JDK home dir, required when download maven via java source | |
| 27 | +# MVNW_REPOURL - repo url base for downloading maven distribution | |
| 28 | +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven | |
| 29 | +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output | |
| 30 | +# ---------------------------------------------------------------------------- | |
| 31 | + | |
| 32 | +set -euf | |
| 33 | +[ "${MVNW_VERBOSE-}" != debug ] || set -x | |
| 34 | + | |
| 35 | +# OS specific support. | |
| 36 | +native_path() { printf %s\\n "$1"; } | |
| 37 | +case "$(uname)" in | |
| 38 | +CYGWIN* | MINGW*) | |
| 39 | + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" | |
| 40 | + native_path() { cygpath --path --windows "$1"; } | |
| 41 | + ;; | |
| 42 | +esac | |
| 43 | + | |
| 44 | +# set JAVACMD and JAVACCMD | |
| 45 | +set_java_home() { | |
| 46 | + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched | |
| 47 | + if [ -n "${JAVA_HOME-}" ]; then | |
| 48 | + if [ -x "$JAVA_HOME/jre/sh/java" ]; then | |
| 49 | + # IBM's JDK on AIX uses strange locations for the executables | |
| 50 | + JAVACMD="$JAVA_HOME/jre/sh/java" | |
| 51 | + JAVACCMD="$JAVA_HOME/jre/sh/javac" | |
| 52 | + else | |
| 53 | + JAVACMD="$JAVA_HOME/bin/java" | |
| 54 | + JAVACCMD="$JAVA_HOME/bin/javac" | |
| 55 | + | |
| 56 | + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then | |
| 57 | + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 | |
| 58 | + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 | |
| 59 | + return 1 | |
| 60 | + fi | |
| 61 | + fi | |
| 62 | + else | |
| 63 | + JAVACMD="$( | |
| 64 | + 'set' +e | |
| 65 | + 'unset' -f command 2>/dev/null | |
| 66 | + 'command' -v java | |
| 67 | + )" || : | |
| 68 | + JAVACCMD="$( | |
| 69 | + 'set' +e | |
| 70 | + 'unset' -f command 2>/dev/null | |
| 71 | + 'command' -v javac | |
| 72 | + )" || : | |
| 73 | + | |
| 74 | + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then | |
| 75 | + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 | |
| 76 | + return 1 | |
| 77 | + fi | |
| 78 | + fi | |
| 79 | +} | |
| 80 | + | |
| 81 | +# hash string like Java String::hashCode | |
| 82 | +hash_string() { | |
| 83 | + str="${1:-}" h=0 | |
| 84 | + while [ -n "$str" ]; do | |
| 85 | + char="${str%"${str#?}"}" | |
| 86 | + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) | |
| 87 | + str="${str#?}" | |
| 88 | + done | |
| 89 | + printf %x\\n $h | |
| 90 | +} | |
| 91 | + | |
| 92 | +verbose() { :; } | |
| 93 | +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } | |
| 94 | + | |
| 95 | +die() { | |
| 96 | + printf %s\\n "$1" >&2 | |
| 97 | + exit 1 | |
| 98 | +} | |
| 99 | + | |
| 100 | +trim() { | |
| 101 | + # MWRAPPER-139: | |
| 102 | + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. | |
| 103 | + # Needed for removing poorly interpreted newline sequences when running in more | |
| 104 | + # exotic environments such as mingw bash on Windows. | |
| 105 | + printf "%s" "${1}" | tr -d '[:space:]' | |
| 106 | +} | |
| 107 | + | |
| 108 | +scriptDir="$(dirname "$0")" | |
| 109 | +scriptName="$(basename "$0")" | |
| 110 | + | |
| 111 | +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties | |
| 112 | +while IFS="=" read -r key value; do | |
| 113 | + case "${key-}" in | |
| 114 | + distributionUrl) distributionUrl=$(trim "${value-}") ;; | |
| 115 | + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; | |
| 116 | + esac | |
| 117 | +done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties" | |
| 118 | +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" | |
| 119 | + | |
| 120 | +case "${distributionUrl##*/}" in | |
| 121 | +maven-mvnd-*bin.*) | |
| 122 | + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ | |
| 123 | + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in | |
| 124 | + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; | |
| 125 | + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; | |
| 126 | + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; | |
| 127 | + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; | |
| 128 | + *) | |
| 129 | + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 | |
| 130 | + distributionPlatform=linux-amd64 | |
| 131 | + ;; | |
| 132 | + esac | |
| 133 | + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" | |
| 134 | + ;; | |
| 135 | +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; | |
| 136 | +*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; | |
| 137 | +esac | |
| 138 | + | |
| 139 | +# apply MVNW_REPOURL and calculate MAVEN_HOME | |
| 140 | +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash> | |
| 141 | +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" | |
| 142 | +distributionUrlName="${distributionUrl##*/}" | |
| 143 | +distributionUrlNameMain="${distributionUrlName%.*}" | |
| 144 | +distributionUrlNameMain="${distributionUrlNameMain%-bin}" | |
| 145 | +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" | |
| 146 | +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" | |
| 147 | + | |
| 148 | +exec_maven() { | |
| 149 | + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : | |
| 150 | + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" | |
| 151 | +} | |
| 152 | + | |
| 153 | +if [ -d "$MAVEN_HOME" ]; then | |
| 154 | + verbose "found existing MAVEN_HOME at $MAVEN_HOME" | |
| 155 | + exec_maven "$@" | |
| 156 | +fi | |
| 157 | + | |
| 158 | +case "${distributionUrl-}" in | |
| 159 | +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; | |
| 160 | +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; | |
| 161 | +esac | |
| 162 | + | |
| 163 | +# prepare tmp dir | |
| 164 | +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then | |
| 165 | + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } | |
| 166 | + trap clean HUP INT TERM EXIT | |
| 167 | +else | |
| 168 | + die "cannot create temp dir" | |
| 169 | +fi | |
| 170 | + | |
| 171 | +mkdir -p -- "${MAVEN_HOME%/*}" | |
| 172 | + | |
| 173 | +# Download and Install Apache Maven | |
| 174 | +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." | |
| 175 | +verbose "Downloading from: $distributionUrl" | |
| 176 | +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" | |
| 177 | + | |
| 178 | +# select .zip or .tar.gz | |
| 179 | +if ! command -v unzip >/dev/null; then | |
| 180 | + distributionUrl="${distributionUrl%.zip}.tar.gz" | |
| 181 | + distributionUrlName="${distributionUrl##*/}" | |
| 182 | +fi | |
| 183 | + | |
| 184 | +# verbose opt | |
| 185 | +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' | |
| 186 | +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v | |
| 187 | + | |
| 188 | +# normalize http auth | |
| 189 | +case "${MVNW_PASSWORD:+has-password}" in | |
| 190 | +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; | |
| 191 | +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; | |
| 192 | +esac | |
| 193 | + | |
| 194 | +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then | |
| 195 | + verbose "Found wget ... using wget" | |
| 196 | + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" | |
| 197 | +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then | |
| 198 | + verbose "Found curl ... using curl" | |
| 199 | + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" | |
| 200 | +elif set_java_home; then | |
| 201 | + verbose "Falling back to use Java to download" | |
| 202 | + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" | |
| 203 | + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" | |
| 204 | + cat >"$javaSource" <<-END | |
| 205 | + public class Downloader extends java.net.Authenticator | |
| 206 | + { | |
| 207 | + protected java.net.PasswordAuthentication getPasswordAuthentication() | |
| 208 | + { | |
| 209 | + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); | |
| 210 | + } | |
| 211 | + public static void main( String[] args ) throws Exception | |
| 212 | + { | |
| 213 | + setDefault( new Downloader() ); | |
| 214 | + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); | |
| 215 | + } | |
| 216 | + } | |
| 217 | + END | |
| 218 | + # For Cygwin/MinGW, switch paths to Windows format before running javac and java | |
| 219 | + verbose " - Compiling Downloader.java ..." | |
| 220 | + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" | |
| 221 | + verbose " - Running Downloader.java ..." | |
| 222 | + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" | |
| 223 | +fi | |
| 224 | + | |
| 225 | +# If specified, validate the SHA-256 sum of the Maven distribution zip file | |
| 226 | +if [ -n "${distributionSha256Sum-}" ]; then | |
| 227 | + distributionSha256Result=false | |
| 228 | + if [ "$MVN_CMD" = mvnd.sh ]; then | |
| 229 | + echo "Checksum validation is not supported for maven-mvnd." >&2 | |
| 230 | + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 | |
| 231 | + exit 1 | |
| 232 | + elif command -v sha256sum >/dev/null; then | |
| 233 | + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then | |
| 234 | + distributionSha256Result=true | |
| 235 | + fi | |
| 236 | + elif command -v shasum >/dev/null; then | |
| 237 | + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then | |
| 238 | + distributionSha256Result=true | |
| 239 | + fi | |
| 240 | + else | |
| 241 | + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 | |
| 242 | + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 | |
| 243 | + exit 1 | |
| 244 | + fi | |
| 245 | + if [ $distributionSha256Result = false ]; then | |
| 246 | + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 | |
| 247 | + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 | |
| 248 | + exit 1 | |
| 249 | + fi | |
| 250 | +fi | |
| 251 | + | |
| 252 | +# unzip and move | |
| 253 | +if command -v unzip >/dev/null; then | |
| 254 | + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" | |
| 255 | +else | |
| 256 | + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" | |
| 257 | +fi | |
| 258 | + | |
| 259 | +# Find the actual extracted directory name (handles snapshots where filename != directory name) | |
| 260 | +actualDistributionDir="" | |
| 261 | + | |
| 262 | +# First try the expected directory name (for regular distributions) | |
| 263 | +if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then | |
| 264 | + if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then | |
| 265 | + actualDistributionDir="$distributionUrlNameMain" | |
| 266 | + fi | |
| 267 | +fi | |
| 268 | + | |
| 269 | +# If not found, search for any directory with the Maven executable (for snapshots) | |
| 270 | +if [ -z "$actualDistributionDir" ]; then | |
| 271 | + # enable globbing to iterate over items | |
| 272 | + set +f | |
| 273 | + for dir in "$TMP_DOWNLOAD_DIR"/*; do | |
| 274 | + if [ -d "$dir" ]; then | |
| 275 | + if [ -f "$dir/bin/$MVN_CMD" ]; then | |
| 276 | + actualDistributionDir="$(basename "$dir")" | |
| 277 | + break | |
| 278 | + fi | |
| 279 | + fi | |
| 280 | + done | |
| 281 | + set -f | |
| 282 | +fi | |
| 283 | + | |
| 284 | +if [ -z "$actualDistributionDir" ]; then | |
| 285 | + verbose "Contents of $TMP_DOWNLOAD_DIR:" | |
| 286 | + verbose "$(ls -la "$TMP_DOWNLOAD_DIR")" | |
| 287 | + die "Could not find Maven distribution directory in extracted archive" | |
| 288 | +fi | |
| 289 | + | |
| 290 | +verbose "Found extracted Maven distribution directory: $actualDistributionDir" | |
| 291 | +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url" | |
| 292 | +mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" | |
| 293 | + | |
| 294 | +clean || : | |
| 295 | +exec_maven "$@" | ... | ... |
mvnw.cmd
0 → 100644
| 1 | +<# : batch portion | |
| 2 | +@REM ---------------------------------------------------------------------------- | |
| 3 | +@REM Licensed to the Apache Software Foundation (ASF) under one | |
| 4 | +@REM or more contributor license agreements. See the NOTICE file | |
| 5 | +@REM distributed with this work for additional information | |
| 6 | +@REM regarding copyright ownership. The ASF licenses this file | |
| 7 | +@REM to you under the Apache License, Version 2.0 (the | |
| 8 | +@REM "License"); you may not use this file except in compliance | |
| 9 | +@REM with the License. You may obtain a copy of the License at | |
| 10 | +@REM | |
| 11 | +@REM http://www.apache.org/licenses/LICENSE-2.0 | |
| 12 | +@REM | |
| 13 | +@REM Unless required by applicable law or agreed to in writing, | |
| 14 | +@REM software distributed under the License is distributed on an | |
| 15 | +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| 16 | +@REM KIND, either express or implied. See the License for the | |
| 17 | +@REM specific language governing permissions and limitations | |
| 18 | +@REM under the License. | |
| 19 | +@REM ---------------------------------------------------------------------------- | |
| 20 | + | |
| 21 | +@REM ---------------------------------------------------------------------------- | |
| 22 | +@REM Apache Maven Wrapper startup batch script, version 3.3.4 | |
| 23 | +@REM | |
| 24 | +@REM Optional ENV vars | |
| 25 | +@REM MVNW_REPOURL - repo url base for downloading maven distribution | |
| 26 | +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven | |
| 27 | +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output | |
| 28 | +@REM ---------------------------------------------------------------------------- | |
| 29 | + | |
| 30 | +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) | |
| 31 | +@SET __MVNW_CMD__= | |
| 32 | +@SET __MVNW_ERROR__= | |
| 33 | +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% | |
| 34 | +@SET PSModulePath= | |
| 35 | +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( | |
| 36 | + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) | |
| 37 | +) | |
| 38 | +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% | |
| 39 | +@SET __MVNW_PSMODULEP_SAVE= | |
| 40 | +@SET __MVNW_ARG0_NAME__= | |
| 41 | +@SET MVNW_USERNAME= | |
| 42 | +@SET MVNW_PASSWORD= | |
| 43 | +@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*) | |
| 44 | +@echo Cannot start maven from wrapper >&2 && exit /b 1 | |
| 45 | +@GOTO :EOF | |
| 46 | +: end batch / begin powershell #> | |
| 47 | + | |
| 48 | +$ErrorActionPreference = "Stop" | |
| 49 | +if ($env:MVNW_VERBOSE -eq "true") { | |
| 50 | + $VerbosePreference = "Continue" | |
| 51 | +} | |
| 52 | + | |
| 53 | +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties | |
| 54 | +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl | |
| 55 | +if (!$distributionUrl) { | |
| 56 | + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" | |
| 57 | +} | |
| 58 | + | |
| 59 | +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { | |
| 60 | + "maven-mvnd-*" { | |
| 61 | + $USE_MVND = $true | |
| 62 | + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" | |
| 63 | + $MVN_CMD = "mvnd.cmd" | |
| 64 | + break | |
| 65 | + } | |
| 66 | + default { | |
| 67 | + $USE_MVND = $false | |
| 68 | + $MVN_CMD = $script -replace '^mvnw','mvn' | |
| 69 | + break | |
| 70 | + } | |
| 71 | +} | |
| 72 | + | |
| 73 | +# apply MVNW_REPOURL and calculate MAVEN_HOME | |
| 74 | +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash> | |
| 75 | +if ($env:MVNW_REPOURL) { | |
| 76 | + $MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" } | |
| 77 | + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')" | |
| 78 | +} | |
| 79 | +$distributionUrlName = $distributionUrl -replace '^.*/','' | |
| 80 | +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' | |
| 81 | + | |
| 82 | +$MAVEN_M2_PATH = "$HOME/.m2" | |
| 83 | +if ($env:MAVEN_USER_HOME) { | |
| 84 | + $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME" | |
| 85 | +} | |
| 86 | + | |
| 87 | +if (-not (Test-Path -Path $MAVEN_M2_PATH)) { | |
| 88 | + New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null | |
| 89 | +} | |
| 90 | + | |
| 91 | +$MAVEN_WRAPPER_DISTS = $null | |
| 92 | +if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) { | |
| 93 | + $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists" | |
| 94 | +} else { | |
| 95 | + $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists" | |
| 96 | +} | |
| 97 | + | |
| 98 | +$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain" | |
| 99 | +$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' | |
| 100 | +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" | |
| 101 | + | |
| 102 | +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { | |
| 103 | + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" | |
| 104 | + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" | |
| 105 | + exit $? | |
| 106 | +} | |
| 107 | + | |
| 108 | +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { | |
| 109 | + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" | |
| 110 | +} | |
| 111 | + | |
| 112 | +# prepare tmp dir | |
| 113 | +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile | |
| 114 | +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" | |
| 115 | +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null | |
| 116 | +trap { | |
| 117 | + if ($TMP_DOWNLOAD_DIR.Exists) { | |
| 118 | + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } | |
| 119 | + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } | |
| 120 | + } | |
| 121 | +} | |
| 122 | + | |
| 123 | +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null | |
| 124 | + | |
| 125 | +# Download and Install Apache Maven | |
| 126 | +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." | |
| 127 | +Write-Verbose "Downloading from: $distributionUrl" | |
| 128 | +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" | |
| 129 | + | |
| 130 | +$webclient = New-Object System.Net.WebClient | |
| 131 | +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { | |
| 132 | + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) | |
| 133 | +} | |
| 134 | +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 | |
| 135 | +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null | |
| 136 | + | |
| 137 | +# If specified, validate the SHA-256 sum of the Maven distribution zip file | |
| 138 | +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum | |
| 139 | +if ($distributionSha256Sum) { | |
| 140 | + if ($USE_MVND) { | |
| 141 | + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." | |
| 142 | + } | |
| 143 | + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash | |
| 144 | + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { | |
| 145 | + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." | |
| 146 | + } | |
| 147 | +} | |
| 148 | + | |
| 149 | +# unzip and move | |
| 150 | +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null | |
| 151 | + | |
| 152 | +# Find the actual extracted directory name (handles snapshots where filename != directory name) | |
| 153 | +$actualDistributionDir = "" | |
| 154 | + | |
| 155 | +# First try the expected directory name (for regular distributions) | |
| 156 | +$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain" | |
| 157 | +$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD" | |
| 158 | +if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) { | |
| 159 | + $actualDistributionDir = $distributionUrlNameMain | |
| 160 | +} | |
| 161 | + | |
| 162 | +# If not found, search for any directory with the Maven executable (for snapshots) | |
| 163 | +if (!$actualDistributionDir) { | |
| 164 | + Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object { | |
| 165 | + $testPath = Join-Path $_.FullName "bin/$MVN_CMD" | |
| 166 | + if (Test-Path -Path $testPath -PathType Leaf) { | |
| 167 | + $actualDistributionDir = $_.Name | |
| 168 | + } | |
| 169 | + } | |
| 170 | +} | |
| 171 | + | |
| 172 | +if (!$actualDistributionDir) { | |
| 173 | + Write-Error "Could not find Maven distribution directory in extracted archive" | |
| 174 | +} | |
| 175 | + | |
| 176 | +Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir" | |
| 177 | +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null | |
| 178 | +try { | |
| 179 | + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null | |
| 180 | +} catch { | |
| 181 | + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { | |
| 182 | + Write-Error "fail to move MAVEN_HOME" | |
| 183 | + } | |
| 184 | +} finally { | |
| 185 | + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } | |
| 186 | + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } | |
| 187 | +} | |
| 188 | + | |
| 189 | +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" | ... | ... |
src/main/java/com/xly/XlyAiApplication.java
0 → 100644
| 1 | +package com.xly; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.ObjectUtil; | |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 5 | +import org.mybatis.spring.annotation.MapperScan; | |
| 6 | +import org.slf4j.Logger; | |
| 7 | +import org.slf4j.LoggerFactory; | |
| 8 | +import org.springframework.boot.SpringApplication; | |
| 9 | +import org.springframework.boot.autoconfigure.SpringBootApplication; | |
| 10 | +import org.springframework.boot.builder.SpringApplicationBuilder; | |
| 11 | +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; | |
| 12 | +import org.springframework.cache.annotation.EnableCaching; | |
| 13 | +import org.springframework.scheduling.annotation.EnableAsync; | |
| 14 | +import org.springframework.scheduling.annotation.EnableScheduling; | |
| 15 | +import org.springframework.web.bind.annotation.PostMapping; | |
| 16 | +import org.springframework.web.bind.annotation.RequestBody; | |
| 17 | + | |
| 18 | +import java.util.HashMap; | |
| 19 | +import java.util.Map; | |
| 20 | + | |
| 21 | +@EnableAsync | |
| 22 | +@EnableScheduling | |
| 23 | +@EnableCaching | |
| 24 | +@SpringBootApplication | |
| 25 | +@MapperScan("com.xly.mapper") | |
| 26 | +public class XlyAiApplication extends SpringBootServletInitializer { // 关键:继承 SpringBootServletInitializer | |
| 27 | + | |
| 28 | + private static final Logger logger = LoggerFactory.getLogger(XlyAiApplication.class); | |
| 29 | + | |
| 30 | + @Override // 关键:重写 configure 方法 | |
| 31 | + protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { | |
| 32 | + return application.sources(XlyAiApplication.class); | |
| 33 | + } | |
| 34 | + | |
| 35 | + /** | |
| 36 | + * 提取报修结构化信息 | |
| 37 | + */ | |
| 38 | + @PostMapping("/") | |
| 39 | + public String home(@RequestBody Map<String,Object> sendMap) { | |
| 40 | + return "欢迎来到小羚羊AI系统"; | |
| 41 | + } | |
| 42 | + | |
| 43 | + public static void main(String[] args) { | |
| 44 | + SpringApplication.run(XlyAiApplication.class, args); | |
| 45 | + logger.info(" \n" + | |
| 46 | + " /$$ \n" + | |
| 47 | + " | $$ \n" + | |
| 48 | + " /$$ /$$ /$$ /$$ /$$ /$$$$$$$| $$ /$$$$$$$\n" + | |
| 49 | + "| $$ | $$ | $$| $$ | $$ /$$_____/| $$ /$$_____/\n" + | |
| 50 | + "| $$ | $$ | $$| $$ | $$| $$ | $$| $$$$$$ \n" + | |
| 51 | + "| $$ | $$ | $$| $$ | $$| $$ | $$ \\____ $$\n" + | |
| 52 | + "| $$$$$/$$$$/| $$$$$$$| $$$$$$$| $$ /$$$$$$$/\n" + | |
| 53 | + " \\_____/\\___/ \\____ $$ \\_______/|__/|_______/ \n" + | |
| 54 | + " /$$ | $$ \n" + | |
| 55 | + " | $$$$$$/ \n" + | |
| 56 | + " \\______/ "); | |
| 57 | + logger.info("I wish you a pleasant use of the system and never have any bugs"); | |
| 58 | + } | |
| 59 | +} | |
| 0 | 60 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/agent/ChatiAgent.java
0 → 100644
| 1 | +package com.xly.agent; | |
| 2 | + | |
| 3 | +import dev.langchain4j.service.MemoryId; | |
| 4 | +import dev.langchain4j.service.SystemMessage; | |
| 5 | +import dev.langchain4j.service.UserMessage; | |
| 6 | +import dev.langchain4j.service.V; | |
| 7 | + | |
| 8 | +public interface ChatiAgent { | |
| 9 | + @SystemMessage(""" | |
| 10 | + 你是一个轻松自然的聊天伙伴,语气亲切口语化,像朋友一样闲聊。 | |
| 11 | + 要求:1. 不生硬、不说教,避免书面化表达; | |
| 12 | + 2. 主动接梗,适当延伸话题,不一问一答; | |
| 13 | + 3. 偶尔带点小幽默,保持轻松无压力的氛围; | |
| 14 | + 4. 回答简洁,符合日常聊天的语气,不啰嗦。 | |
| 15 | + 5. 首次沟通时发现称呼不是“小羚羊”时,请回复“我不是..,我是小羚羊”,语气俏皮。 | |
| 16 | + """) | |
| 17 | + @UserMessage("用户说:{{userInput}}") | |
| 18 | + String chat(@MemoryId String userId, @V("userInput") String userInput); | |
| 19 | +} | ... | ... |
src/main/java/com/xly/agent/DynamicTableNl2SqlAiAgent.java
0 → 100644
| 1 | +package com.xly.agent; | |
| 2 | + | |
| 3 | +import dev.langchain4j.service.MemoryId; | |
| 4 | +import dev.langchain4j.service.SystemMessage; | |
| 5 | +import dev.langchain4j.service.UserMessage; | |
| 6 | +import dev.langchain4j.service.V; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * 适配动态表结构的NL2SQL AI服务 | |
| 10 | + * 核心:表结构作为动态参数传入,@SystemMessage仅保留通用规则 | |
| 11 | + */ | |
| 12 | + | |
| 13 | + | |
| 14 | +public interface DynamicTableNl2SqlAiAgent { | |
| 15 | + | |
| 16 | + /** | |
| 17 | + * 动态表结构:自然语言转MySQL SELECT语句 | |
| 18 | + * 入参:数据库名、表名(多表用,分隔)、表结构、用户查询 | |
| 19 | + */ | |
| 20 | + @SystemMessage(""" | |
| 21 | + 你是资深MySQL数据分析师,严格遵循以下**通用规则**生成SQL,适用于所有业务场景: | |
| 22 | + 1. 语法规范:仅生成符合MySQL8.0/5.7的标准SELECT语句,兼容低版本,多表关联用JOIN而非逗号; | |
| 23 | + 2. 输出格式:仅返回SQL语句本身,无任何解释、换行、```sql/```包裹、备注、多余空格,直接输出可执行SQL; | |
| 24 | + 3. 编写规范: | |
| 25 | + 3.1 多表关联必须使用 表名+字段名(如表名.字段名),严格按下面[涉及表名]中的表次序关联,聚合函数(SUM/COUNT/AVG/MIN/MAX)必须加业务化别名,日期过滤使用标准DATE格式(yyyy-MM-dd); | |
| 26 | + 3.2 SQL所有字段均采用 表名.字段名 方式生成,务必确保 字段名 在相应的 表名 描述的字段中存在,如果不存在重试其它方式,直到满足条件 | |
| 27 | + 3.3 SQL所有字段涉及的所有表名,都要**严格**按下面[涉及表名]中的表次序关联,没有关联不允许使用 | |
| 28 | + 3.4 SQL所有的查询条件,如果是字符类型的字段,均需要加不为空判断(如ifnull(customername,'')<>'') | |
| 29 | + 4. 安全约束:禁止生成任何DDL/DML语句(DROP/ALTER/INSERT/UPDATE/DELETE等),禁止使用子查询、存储过程、自定义函数、临时表; | |
| 30 | + 5. 精准性:严格按用户需求+传入的表结构生成,仅使用指定字段/表,无多余字段、无无效表关联、无冗余过滤条件; | |
| 31 | + 6. 关联规则:多表关联时,必须使用外键/业务唯一键关联,禁止无意义关联。 | |
| 32 | + """) | |
| 33 | + @UserMessage(""" | |
| 34 | + 【业务场景表结构信息】 | |
| 35 | + 涉及表名:{{tableNames}}(多表用,分隔,需关联时请按规范使用JOIN) | |
| 36 | + 表结构详情:{{tableStruct}}(多表请标注表名+字段,格式:表名(字段1:类型,字段2:类型,主键/外键)) | |
| 37 | + 【用户需求】 | |
| 38 | + {{userInput}} | |
| 39 | + 请根据上述表结构+通用规则,生成符合要求的MySQL SELECT语句: | |
| 40 | + """) | |
| 41 | + String generateMysqlSql(@MemoryId String userId, | |
| 42 | + @V("tableNames") String tableNames, | |
| 43 | + @V("tableStruct") String tableStruct, | |
| 44 | + @V("userInput") String userInput); | |
| 45 | + | |
| 46 | + /** | |
| 47 | + * 动态表结构:自然语言解释SQL执行结果 | |
| 48 | + * 入参:用户问题、执行的SQL、表结构、JSON格式结果 | |
| 49 | + */ | |
| 50 | + @SystemMessage(""" | |
| 51 | + 你是专业的业务数据分析师,严格遵循以下**通用规则**解释查询结果,适用于所有业务场景: | |
| 52 | + 1. 解释风格:贴合业务场景,无任何SQL专业术语,用口语化、简洁的商业语言说明,避免技术词汇; | |
| 53 | + 2. 数据准确:严格按照JSON执行结果解释,不夸大、不遗漏、不编造数据,数值与结果完全一致; | |
| 54 | + 3. 输出格式:仅返回解释内容,不要列出ID,无多余标题、换行、符号,结果为空时直接返回“未查询到相关数据”; | |
| 55 | + 4. 长度控制:单条解释不超过150字,条理清晰,重点突出核心数据/趋势; | |
| 56 | + 5. 禁止重复:不重复用户问题、不重复执行的SQL语句,仅针对结果做业务解读。 | |
| 57 | + """) | |
| 58 | + @UserMessage(""" | |
| 59 | + 【业务场景表结构信息】 | |
| 60 | + 表结构详情:{{tableStruct}} | |
| 61 | + 【查询相关信息】 | |
| 62 | + 用户原始查询:{{userInput}} | |
| 63 | + 执行的MySQL SQL:{{sql}} | |
| 64 | + SQL执行结果(JSON格式):{{result}} | |
| 65 | + 请根据上述信息+通用规则,对查询结果做业务解释: | |
| 66 | + """) | |
| 67 | + String explainSqlResult(@MemoryId String userId, | |
| 68 | + @V("userInput") String userInput, | |
| 69 | + @V("sql") String sql, | |
| 70 | + @V("tableStruct") String tableStruct, | |
| 71 | + @V("result") String result); | |
| 72 | +} | |
| 0 | 73 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/agent/ErpAiAgent.java
0 → 100644
| 1 | +package com.xly.agent; | |
| 2 | + | |
| 3 | + | |
| 4 | +import dev.langchain4j.service.MemoryId; | |
| 5 | +import dev.langchain4j.service.SystemMessage; | |
| 6 | +import dev.langchain4j.service.UserMessage; | |
| 7 | +import dev.langchain4j.service.V; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * 优化后:新增场景专属交互规则,大模型仅处理当前场景业务指令 | |
| 11 | + */ | |
| 12 | +public interface ErpAiAgent { | |
| 13 | + @SystemMessage(""" | |
| 14 | + 你是一个专业的 工具方法匹配与参数提取 助手,核心职责是根据用户输入(含历史对话)精准匹配工具方法、提取参数、判断缺失并生成交互式补全提示; | |
| 15 | + 按严格按以下步骤处理,无任何额外输出!规则如下: | |
| 16 | + 1. 方法匹配:先精准拆解用户查询的核心业务意图,再自动匹配唯一符合用户问题的工具方法(MethodNo),禁止自创; | |
| 17 | + 2. 参数提取:提取该工具的全部参数,与描述完全一致,严格按标注类型赋值,数字无引号,为空时禁止赋值0; | |
| 18 | + """) | |
| 19 | + @UserMessage("用户输入:{{userInput}}") | |
| 20 | + String chat(@MemoryId String userId, @V("userInput") String userInput); | |
| 21 | +} | |
| 0 | 22 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/agent/SceneSelectorAiAgent.java
0 → 100644
| 1 | +package com.xly.agent; | |
| 2 | + | |
| 3 | +import com.xly.entity.SceneIntentParseResp; | |
| 4 | +import dev.langchain4j.service.SystemMessage; | |
| 5 | +import dev.langchain4j.service.UserMessage; | |
| 6 | +import dev.langchain4j.service.V; | |
| 7 | +import org.springframework.stereotype.Component; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * 场景意图解析AI服务:专门让大模型解析用户输入的意图,匹配对应的业务场景 | |
| 11 | + * 基于LangChain4j AiServices构建,由大模型返回标准化的场景编码 | |
| 12 | + */ | |
| 13 | +public interface SceneSelectorAiAgent { | |
| 14 | + | |
| 15 | + /** | |
| 16 | + * 核心方法:解析用户意图,匹配业务场景 | |
| 17 | + * @param userInput 用户输入 | |
| 18 | + * @param authScenesDesc 可访问场景描述 | |
| 19 | + * @return 标准化的意图解析响应(仅返回sceneCode) | |
| 20 | + */ | |
| 21 | + @SystemMessage(""" | |
| 22 | + 你是专业的ERP系统**意图解析助理**,你的唯一职责是根据用户输入,匹配其意图对应的业务场景,严格遵循以下规则: | |
| 23 | + 1. 仅从用户提供的「可访问场景列表」中选择匹配的场景,绝对不允许虚构场景; | |
| 24 | + 2. 匹配规则:用户输入的意图与场景的「支持功能」高度相关,即匹配该场景; | |
| 25 | + 3. 输出格式:必须严格返回JSON格式,仅包含sceneCode字段,无任何多余文字、解释、换行; | |
| 26 | + - 匹配到一个场景:sceneCode为场景码(如salemange/purchasemange/productionmange),scene为场景名称(如销售管理/采购管理/生产管理); | |
| 27 | + - 匹配到多个场景,请列出并让客户选择场景; | |
| 28 | + - 无匹配场景/用户仅问候:sceneCode为NO_MATCH; | |
| 29 | + 4. 不允许添加任何额外字段,不允许返回JSON以外的内容,确保后端能直接解析; | |
| 30 | + 5. 忽略用户输入中的无关语气词(如“你好”“帮我”“麻烦”),提取核心业务意图。 | |
| 31 | + """) | |
| 32 | + @UserMessage(""" | |
| 33 | + 用户输入:{{userInput}} | |
| 34 | + 可访问场景列表:{{authScenesDesc}} | |
| 35 | + 请严格按指定格式返回匹配的场景编码! | |
| 36 | + """) | |
| 37 | + SceneIntentParseResp parseSceneIntent(@V("userInput") String userInput, | |
| 38 | + @V("authScenesDesc") String authScenesDesc); | |
| 39 | +} | |
| 0 | 40 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/config/BizExecuteUtil.java
0 → 100644
| 1 | +package com.xly.config; | |
| 2 | + | |
| 3 | +import groovy.lang.GroovyClassLoader; | |
| 4 | +import groovy.lang.GroovyObject; | |
| 5 | +import lombok.RequiredArgsConstructor; | |
| 6 | +import lombok.extern.slf4j.Slf4j; | |
| 7 | +import org.springframework.expression.Expression; | |
| 8 | +import org.springframework.expression.ExpressionParser; | |
| 9 | +import org.springframework.expression.spel.standard.SpelExpressionParser; | |
| 10 | +import org.springframework.expression.spel.support.StandardEvaluationContext; | |
| 11 | +import org.springframework.stereotype.Component; | |
| 12 | + | |
| 13 | +import java.util.Map; | |
| 14 | +import java.util.Objects; | |
| 15 | + | |
| 16 | +/** | |
| 17 | + * 业务逻辑执行工具:EL表达式 + Groovy脚本 | |
| 18 | + */ | |
| 19 | +@Slf4j | |
| 20 | +@Component | |
| 21 | +@RequiredArgsConstructor | |
| 22 | +public class BizExecuteUtil { | |
| 23 | + /** | |
| 24 | + * EL表达式解析器 | |
| 25 | + */ | |
| 26 | + private final ExpressionParser elParser = new SpelExpressionParser(); | |
| 27 | + | |
| 28 | + /** | |
| 29 | + * Groovy类加载器 | |
| 30 | + */ | |
| 31 | + private final GroovyClassLoader groovyClassLoader = new GroovyClassLoader(); | |
| 32 | + | |
| 33 | + /** | |
| 34 | + * 执行业务逻辑 | |
| 35 | + * @param bizType 1=EL,2=Groovy | |
| 36 | + * @param bizContent 逻辑内容 | |
| 37 | + * @param params JSON参数 | |
| 38 | + * @return 执行结果 | |
| 39 | + */ | |
| 40 | + public String execute(Integer bizType, String bizContent, Map<String, Object> params) { | |
| 41 | + if (Objects.isNull(bizType) || Objects.isNull(bizContent) || bizContent.isBlank()) { | |
| 42 | + throw new IllegalArgumentException("业务逻辑配置异常"); | |
| 43 | + } | |
| 44 | + // EL表达式执行 | |
| 45 | + if (1 == bizType) { | |
| 46 | + return executeEL(bizContent, params); | |
| 47 | + } | |
| 48 | + // Groovy脚本执行 | |
| 49 | + else if (2 == bizType) { | |
| 50 | + return executeGroovy(bizContent, params); | |
| 51 | + } | |
| 52 | + else { | |
| 53 | + throw new IllegalArgumentException("不支持的业务逻辑类型:" + bizType); | |
| 54 | + } | |
| 55 | + } | |
| 56 | + | |
| 57 | + /** | |
| 58 | + * 执行EL表达式 | |
| 59 | + */ | |
| 60 | + private String executeEL(String elContent, Map<String, Object> params) { | |
| 61 | + try { | |
| 62 | + StandardEvaluationContext context = new StandardEvaluationContext(); | |
| 63 | + context.setVariable("params", params); | |
| 64 | + Expression expression = elParser.parseExpression(elContent); | |
| 65 | + return expression.getValue(context, String.class); | |
| 66 | + } catch (Exception e) { | |
| 67 | + log.error("EL表达式执行失败:{}", elContent, e); | |
| 68 | + throw new IllegalArgumentException("EL表达式执行失败:" + e.getMessage()); | |
| 69 | + } | |
| 70 | + } | |
| 71 | + | |
| 72 | + /** | |
| 73 | + * 执行Groovy脚本 | |
| 74 | + */ | |
| 75 | + private String executeGroovy(String groovyContent, Map<String, Object> params) { | |
| 76 | + try { | |
| 77 | + // 构建Groovy脚本类 | |
| 78 | + String scriptCode = "class DynamicScript { def execute(Map params) { " + groovyContent + " } }"; | |
| 79 | + Class<?> groovyClass = groovyClassLoader.parseClass(scriptCode); | |
| 80 | + GroovyObject groovyObject = (GroovyObject) groovyClass.getDeclaredConstructor().newInstance(); | |
| 81 | + // 执行脚本并返回结果 | |
| 82 | + Object result = groovyObject.invokeMethod("execute", new Object[]{params}); | |
| 83 | + return Objects.isNull(result) ? "执行成功" : result.toString(); | |
| 84 | + } catch (Exception e) { | |
| 85 | + log.error("Groovy脚本执行失败:{}", groovyContent, e); | |
| 86 | + throw new IllegalArgumentException("Groovy脚本执行失败:" + e.getMessage()); | |
| 87 | + } | |
| 88 | + } | |
| 89 | +} | |
| 0 | 90 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/config/CorsConfig.java
0 → 100644
| 1 | +package com.xly.config; | |
| 2 | + | |
| 3 | +import org.springframework.context.annotation.Bean; | |
| 4 | +import org.springframework.context.annotation.Configuration; | |
| 5 | +import org.springframework.web.cors.CorsConfiguration; | |
| 6 | +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; | |
| 7 | +import org.springframework.web.filter.CorsFilter; | |
| 8 | +import org.springframework.web.servlet.config.annotation.CorsRegistry; | |
| 9 | +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | |
| 10 | + | |
| 11 | +/*** | |
| 12 | + * @Author 钱豹 | |
| 13 | + * @Date 22:40 2026/2/3 | |
| 14 | + * @Param | |
| 15 | + * @return | |
| 16 | + * @Description 跨域配置 | |
| 17 | + **/ | |
| 18 | +@Configuration | |
| 19 | +public class CorsConfig { | |
| 20 | + | |
| 21 | + /** | |
| 22 | + * 允许所有跨域请求 - CorsFilter方式 | |
| 23 | + */ | |
| 24 | + @Bean | |
| 25 | + public CorsFilter corsFilter() { | |
| 26 | + CorsConfiguration config = new CorsConfiguration(); | |
| 27 | + | |
| 28 | + // 允许所有域名 | |
| 29 | + config.addAllowedOriginPattern("*"); | |
| 30 | + | |
| 31 | + // 允许所有请求方法 | |
| 32 | + config.addAllowedMethod("*"); | |
| 33 | + | |
| 34 | + // 允许所有请求头 | |
| 35 | + config.addAllowedHeader("*"); | |
| 36 | + | |
| 37 | + // 允许携带凭证(如cookies) | |
| 38 | + config.setAllowCredentials(true); | |
| 39 | + | |
| 40 | + // 暴露所有响应头 | |
| 41 | + config.addExposedHeader("*"); | |
| 42 | + | |
| 43 | + // 预检请求缓存时间 | |
| 44 | + config.setMaxAge(3600L); | |
| 45 | + | |
| 46 | + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); | |
| 47 | + source.registerCorsConfiguration("/**", config); | |
| 48 | + | |
| 49 | + return new CorsFilter(source); | |
| 50 | + } | |
| 51 | + | |
| 52 | + /** | |
| 53 | + * 允许所有跨域请求 - WebMvcConfigurer方式 | |
| 54 | + */ | |
| 55 | + @Bean | |
| 56 | + public WebMvcConfigurer corsConfigurer() { | |
| 57 | + return new WebMvcConfigurer() { | |
| 58 | + @Override | |
| 59 | + public void addCorsMappings(CorsRegistry registry) { | |
| 60 | + registry.addMapping("/**") | |
| 61 | + .allowedOriginPatterns("*") // 使用 allowedOriginPatterns 代替 allowedOrigins | |
| 62 | + .allowedMethods("*") | |
| 63 | + .allowedHeaders("*") | |
| 64 | + .exposedHeaders("*") | |
| 65 | + .allowCredentials(true) | |
| 66 | + .maxAge(3600); | |
| 67 | + } | |
| 68 | + }; | |
| 69 | + } | |
| 70 | +} | |
| 0 | 71 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/config/JacksonConfig.java
0 → 100644
| 1 | +package com.xly.config; | |
| 2 | + | |
| 3 | +import com.fasterxml.jackson.databind.ObjectMapper; | |
| 4 | +import com.fasterxml.jackson.databind.SerializationFeature; | |
| 5 | +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; | |
| 6 | +import org.springframework.context.annotation.Bean; | |
| 7 | +import org.springframework.context.annotation.Configuration; | |
| 8 | +import org.springframework.context.annotation.Primary; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * 全局 ObjectMapper 配置:支持 LocalDate 序列化/反序列化 | |
| 12 | + */ | |
| 13 | +@Configuration | |
| 14 | +public class JacksonConfig { | |
| 15 | + | |
| 16 | + /** | |
| 17 | + * 配置好的 ObjectMapper:注册 JavaTimeModule,支持 Java 8 时间类型 | |
| 18 | + */ | |
| 19 | + @Bean | |
| 20 | + @Primary // 标记为默认实例,避免多 ObjectMapper 冲突 | |
| 21 | + public ObjectMapper objectMapper() { | |
| 22 | + ObjectMapper mapper = new ObjectMapper(); | |
| 23 | + // 1. 核心:注册 JSR310 模块,处理 LocalDate/LocalDateTime | |
| 24 | + mapper.registerModule(new JavaTimeModule()); | |
| 25 | + // 2. 关闭时间戳序列化,LocalDate 以 "yyyy-MM-dd" 字符串形式存储 | |
| 26 | + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); | |
| 27 | + // 3. 忽略未知字段(AI 返回多余字段时不报错) | |
| 28 | + mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | |
| 29 | + return mapper; | |
| 30 | + } | |
| 31 | +} | |
| 0 | 32 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/config/ModelConfig.java
0 → 100644
| 1 | +package com.xly.config; | |
| 2 | + | |
| 3 | +import com.fasterxml.jackson.databind.ObjectMapper; | |
| 4 | +import com.fasterxml.jackson.databind.SerializationFeature; | |
| 5 | +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; | |
| 6 | +import com.xly.agent.DynamicTableNl2SqlAiAgent; | |
| 7 | +import com.xly.agent.SceneSelectorAiAgent; | |
| 8 | +import dev.langchain4j.memory.chat.MessageWindowChatMemory; | |
| 9 | +import dev.langchain4j.model.chat.ChatLanguageModel; | |
| 10 | +import dev.langchain4j.model.chat.StreamingChatLanguageModel; | |
| 11 | +import dev.langchain4j.model.ollama.OllamaChatModel; | |
| 12 | +import dev.langchain4j.model.ollama.OllamaStreamingChatModel; | |
| 13 | +import dev.langchain4j.service.AiServices; | |
| 14 | +import org.springframework.beans.factory.annotation.Qualifier; | |
| 15 | +import org.springframework.beans.factory.annotation.Value; | |
| 16 | +import org.springframework.context.annotation.Bean; | |
| 17 | +import org.springframework.context.annotation.Configuration; | |
| 18 | +import org.springframework.context.annotation.Primary; | |
| 19 | + | |
| 20 | +import java.time.Duration; | |
| 21 | + | |
| 22 | +/** | |
| 23 | + * 大模型初始化配置(单例复用,避免重复创建) | |
| 24 | + */ | |
| 25 | +@Configuration | |
| 26 | +public class ModelConfig { | |
| 27 | + | |
| 28 | + | |
| 29 | + @Value("${langchain4j.ollama.base-url}") | |
| 30 | + private String chatModelUrl; | |
| 31 | + | |
| 32 | + @Value("${langchain4j.ollama.base-url}") | |
| 33 | + private String sqlModelUrl; | |
| 34 | + | |
| 35 | + @Value("${langchain4j.ollama.chat-model-name}") | |
| 36 | + private String chatModelName; | |
| 37 | + | |
| 38 | + @Value("${langchain4j.ollama.sql-model-name}") | |
| 39 | + private String sqlModelName; | |
| 40 | + | |
| 41 | + // 中文对话模型 qwen2.5:7b-instruct | |
| 42 | + @Bean | |
| 43 | + @Primary | |
| 44 | + public OllamaChatModel chatLanguageModel() { | |
| 45 | + return OllamaChatModel.builder() | |
| 46 | + .baseUrl(chatModelUrl) | |
| 47 | + .modelName(chatModelName) // 使用聊天模型名称 | |
| 48 | + .temperature(0.0) // 建议调整为0.0太确定 | |
| 49 | + .topP(0.9) | |
| 50 | +// .numPredict(2048) // 添加生成长度限制 | |
| 51 | + .timeout(Duration.ofSeconds(60)) // 缩短超时时间 | |
| 52 | + .maxRetries(2) | |
| 53 | + .build(); | |
| 54 | + } | |
| 55 | + | |
| 56 | + /*** | |
| 57 | + * @Author 钱豹 | |
| 58 | + * @Date 13:25 2026/2/6 | |
| 59 | + * @Param [] | |
| 60 | + * @return dev.langchain4j.model.ollama.OllamaChatModel | |
| 61 | + * @Description 聊天 | |
| 62 | + **/ | |
| 63 | + @Bean("chatiModel") | |
| 64 | + public ChatLanguageModel chatiModel() { | |
| 65 | + return OllamaChatModel.builder() | |
| 66 | + .baseUrl(chatModelUrl) | |
| 67 | + .modelName(chatModelName) // 使用聊天模型名称 | |
| 68 | + .temperature(0.8) // 建议调整为0.8太确定 | |
| 69 | + .topP(0.9) | |
| 70 | +// .numPredict(2048) // 添加生成长度限制 | |
| 71 | + .timeout(Duration.ofSeconds(60)) // 缩短超时时间 | |
| 72 | + .maxRetries(2) | |
| 73 | + .build(); | |
| 74 | + } | |
| 75 | + | |
| 76 | + // SQL/代码专用模型 qwen2.5-coder:14b | |
| 77 | + @Bean("sqlChatModel") // 明确指定bean名称 | |
| 78 | + public ChatLanguageModel sqlChatModel() { | |
| 79 | + return OllamaChatModel.builder() | |
| 80 | + .baseUrl(sqlModelUrl) | |
| 81 | + .modelName(sqlModelName) // 使用SQL模型名称 | |
| 82 | + .temperature(0.0) | |
| 83 | + .topP(0.95) | |
| 84 | + .numPredict(4096) // 代码生成需要更长 | |
| 85 | + .timeout(Duration.ofSeconds(120)) | |
| 86 | + .maxRetries(3) | |
| 87 | +// .repeatPenalty(1.1) // 减少重复 | |
| 88 | + .build(); | |
| 89 | + } | |
| 90 | + | |
| 91 | + /*** | |
| 92 | + * @Author 钱豹 | |
| 93 | + * @Date 22:53 2026/2/3 | |
| 94 | + * @Param [] | |
| 95 | + * @return dev.langchain4j.model.chat.StreamingChatLanguageModel | |
| 96 | + * @Description 流式聊天模型 - 使用 @Primary | |
| 97 | + **/ | |
| 98 | + @Bean("streamingChatModel") | |
| 99 | + @Primary | |
| 100 | + public StreamingChatLanguageModel streamingChatModel() { | |
| 101 | + return OllamaStreamingChatModel.builder() | |
| 102 | + .baseUrl(chatModelUrl) | |
| 103 | + .modelName(chatModelName) | |
| 104 | + .temperature(0.7) | |
| 105 | + .topP(0.9) | |
| 106 | + .numPredict(1024) | |
| 107 | + .timeout(Duration.ofSeconds(60)) | |
| 108 | + .build(); | |
| 109 | + } | |
| 110 | + | |
| 111 | + // 流式SQL/代码模型 - 指定名称 | |
| 112 | + @Bean("streamingSqlModel") | |
| 113 | + public StreamingChatLanguageModel streamingSqlModel() { | |
| 114 | + return OllamaStreamingChatModel.builder() | |
| 115 | + .baseUrl(sqlModelUrl) | |
| 116 | + .modelName(sqlModelName) | |
| 117 | + .temperature(0.3) | |
| 118 | + .topP(0.95) | |
| 119 | + .numPredict(2048) | |
| 120 | + .timeout(Duration.ofSeconds(120)) | |
| 121 | + .build(); | |
| 122 | + } | |
| 123 | + | |
| 124 | + /** | |
| 125 | + * 全局ObjectMapper:支持LocalDate序列化 | |
| 126 | + */ | |
| 127 | + @Bean | |
| 128 | + @Primary | |
| 129 | + public ObjectMapper objectMapper() { | |
| 130 | + ObjectMapper mapper = new ObjectMapper(); | |
| 131 | + mapper.registerModule(new JavaTimeModule()); | |
| 132 | + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); | |
| 133 | + mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); | |
| 134 | + return mapper; | |
| 135 | + } | |
| 136 | + | |
| 137 | + //动态SQL生成模型 | |
| 138 | + @Bean | |
| 139 | + public DynamicTableNl2SqlAiAgent dynamicTableNl2SqlAiAgent(@Qualifier("sqlChatModel") ChatLanguageModel sqlModel) { | |
| 140 | + return AiServices.builder(DynamicTableNl2SqlAiAgent.class) | |
| 141 | + .chatLanguageModel(sqlModel) | |
| 142 | + // 会话记忆:每个用户最多保留10轮对话,避免记忆溢出 | |
| 143 | + .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) | |
| 144 | + .build(); | |
| 145 | + } | |
| 146 | + | |
| 147 | + //场景意图解析AI服务 | |
| 148 | + @Bean | |
| 149 | + public SceneSelectorAiAgent sceneSelectorAiAgent(OllamaChatModel sqlModel) { | |
| 150 | + return AiServices.builder(SceneSelectorAiAgent.class) | |
| 151 | + .chatLanguageModel(sqlModel) | |
| 152 | + // 会话记忆:每个用户最多保留10轮对话,避免记忆溢出 | |
| 153 | + .chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(10)) | |
| 154 | + .build(); | |
| 155 | + } | |
| 156 | +} | |
| 0 | 157 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/config/MvcConfig.java
0 → 100644
| 1 | +package com.xly.config; | |
| 2 | + | |
| 3 | +import org.springframework.context.annotation.Configuration; | |
| 4 | +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; | |
| 5 | +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; | |
| 6 | + | |
| 7 | +@Configuration | |
| 8 | +public class MvcConfig implements WebMvcConfigurer { | |
| 9 | + | |
| 10 | + @Override | |
| 11 | + public void addViewControllers(ViewControllerRegistry registry) { | |
| 12 | + // 直接映射URL到视图 | |
| 13 | + registry.addViewController("/chat").setViewName("chat"); | |
| 14 | + registry.addViewController("/").setViewName("index"); | |
| 15 | + } | |
| 16 | +} | |
| 0 | 17 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/config/OperableChatMemoryProvider.java
0 → 100644
| 1 | +package com.xly.config; | |
| 2 | + | |
| 3 | + | |
| 4 | +import dev.langchain4j.memory.ChatMemory; | |
| 5 | +import dev.langchain4j.memory.chat.ChatMemoryProvider; | |
| 6 | +import dev.langchain4j.memory.chat.MessageWindowChatMemory; | |
| 7 | +import dev.langchain4j.data.message.ChatMessage; | |
| 8 | +import org.springframework.stereotype.Component; | |
| 9 | + | |
| 10 | +import java.util.List; | |
| 11 | +import java.util.Map; | |
| 12 | +import java.util.Objects; | |
| 13 | +import java.util.concurrent.ConcurrentHashMap; | |
| 14 | + | |
| 15 | +/** | |
| 16 | + * 可操作的ChatMemoryProvider:获取消息对象+清除记忆(指定/全量)+删除单条消息 | |
| 17 | + * 实现框架原生接口,无缝对接AiServices,线程安全适配生产环境 | |
| 18 | + */ | |
| 19 | +@Component | |
| 20 | +public class OperableChatMemoryProvider implements ChatMemoryProvider { | |
| 21 | + // 核心缓存:memoryId -> ChatMemory,保证一个会话/用户对应唯一记忆实例 | |
| 22 | + private final Map<Object, ChatMemory> memoryCache = new ConcurrentHashMap<>(); | |
| 23 | + // 记忆最大消息数,根据业务需求调整(原配置为10) | |
| 24 | + private static final int MAX_MESSAGE_SIZE = 100; | |
| 25 | + | |
| 26 | + /** | |
| 27 | + * 框架原生方法:获取【当前memoryId对应的ChatMemory实例(含消息对象)】 | |
| 28 | + * AiServices自动调用,也是手动操作记忆/消息的唯一入口 | |
| 29 | + */ | |
| 30 | + @Override | |
| 31 | + public ChatMemory get(Object memoryId) { | |
| 32 | + // 空memoryId兜底,避免空指针 | |
| 33 | + Object finalMemId = Objects.isNull(memoryId) ? "default_erp_chat_memory" : memoryId; | |
| 34 | + // 不存在则创建MessageWindowChatMemory,存在则复用 | |
| 35 | + return memoryCache.computeIfAbsent(finalMemId, k -> MessageWindowChatMemory.withMaxMessages(MAX_MESSAGE_SIZE)); | |
| 36 | + } | |
| 37 | + | |
| 38 | + // ===================== 1. 获取消息对象(当前memoryId) ===================== | |
| 39 | + /** | |
| 40 | + * 获取当前会话/用户的全部消息列表(含用户消息、AI消息,按对话顺序) | |
| 41 | + */ | |
| 42 | + public List<ChatMessage> getCurrentChatMessages(Object memoryId) { | |
| 43 | + if (Objects.isNull(memoryId)) { | |
| 44 | + return List.of(); | |
| 45 | + } | |
| 46 | + // 从ChatMemory中获取原生消息列表 | |
| 47 | + return this.get(memoryId).messages(); | |
| 48 | + } | |
| 49 | + | |
| 50 | + // ===================== 2. 清除记忆(核心需求) ===================== | |
| 51 | + /** | |
| 52 | + * 清空【指定memoryId】的全部记忆(最常用,如前端「清空对话」) | |
| 53 | + */ | |
| 54 | + public void clearSpecifiedMemory(Object memoryId) { | |
| 55 | + if (Objects.nonNull(memoryId)) { | |
| 56 | + // 调用ChatMemory原生clear(),清空该实例所有消息 | |
| 57 | + this.get(memoryId).clear(); | |
| 58 | + } | |
| 59 | + } | |
| 60 | + | |
| 61 | + /** | |
| 62 | + * 全量清除【所有memoryId】的记忆(系统级清理,如定时任务/后台操作) | |
| 63 | + */ | |
| 64 | + public void clearAllMemory() { | |
| 65 | + // 遍历所有记忆实例,逐个调用原生clear()清空 | |
| 66 | + memoryCache.values().forEach(ChatMemory::clear); | |
| 67 | + // 可选:彻底清空缓存,销毁所有实例(释放内存,后续会重新创建) | |
| 68 | + // memoryCache.clear(); | |
| 69 | + } | |
| 70 | + | |
| 71 | + // ===================== 3. 精准操作:删除单条消息 ===================== | |
| 72 | + /** | |
| 73 | + * 删除当前memoryId的指定单条消息(如删除错误消息) | |
| 74 | + * @param message 要删除的消息对象(从getCurrentChatMessages中获取) | |
| 75 | + */ | |
| 76 | + public void deleteSingleMessage(Object memoryId, ChatMessage message) { | |
| 77 | + if (Objects.nonNull(memoryId) && Objects.nonNull(message)) { | |
| 78 | + ChatMemory currentMemory = this.get(memoryId); | |
| 79 | + // 删除指定消息 | |
| 80 | + currentMemory.messages().remove(message); | |
| 81 | + // 刷新记忆,保证MessageWindowChatMemory的最大消息数限制生效 | |
| 82 | +// currentMemory.update(currentMemory.messages()); | |
| 83 | + } | |
| 84 | + } | |
| 85 | + | |
| 86 | + /** | |
| 87 | + * 移除并清除指定记忆(清空消息+从缓存删除实例,彻底释放资源,适用于过期会话) | |
| 88 | + */ | |
| 89 | + public void removeAndClearMemory(Object memoryId) { | |
| 90 | + if (Objects.nonNull(memoryId)) { | |
| 91 | + ChatMemory chatMemory = memoryCache.remove(memoryId); | |
| 92 | + if (Objects.nonNull(chatMemory)) { | |
| 93 | + chatMemory.clear(); | |
| 94 | + } | |
| 95 | + } | |
| 96 | + } | |
| 97 | +} | ... | ... |
src/main/java/com/xly/constant/BusinessCode.java
0 → 100644
| 1 | +package com.xly.constant; | |
| 2 | + | |
| 3 | +import lombok.Getter; | |
| 4 | + | |
| 5 | +/*** | |
| 6 | + * @Author 钱豹 | |
| 7 | + * @Date 9:53 2026/2/9 | |
| 8 | + * @Param | |
| 9 | + * @return | |
| 10 | + * @Description //TODO | |
| 11 | + **/ | |
| 12 | +@Getter | |
| 13 | +public enum BusinessCode { | |
| 14 | + | |
| 15 | + //报价确认 | |
| 16 | + QUOCONFIRM("quoconfirm", "报价确认"); | |
| 17 | + | |
| 18 | + private final String code; | |
| 19 | + private final String message; | |
| 20 | + | |
| 21 | + BusinessCode(String code, String message) { | |
| 22 | + this.code = code; | |
| 23 | + this.message = message; | |
| 24 | + } | |
| 25 | + | |
| 26 | + /** | |
| 27 | + * 根据code获取ErrorCode | |
| 28 | + */ | |
| 29 | + public static BusinessCode getByCode(Integer code) { | |
| 30 | + for (BusinessCode errorCode : values()) { | |
| 31 | + if (errorCode.getCode().equals(code)) { | |
| 32 | + return errorCode; | |
| 33 | + } | |
| 34 | + } | |
| 35 | + return QUOCONFIRM; | |
| 36 | + } | |
| 37 | +} | |
| 0 | 38 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/constant/CommonConstant.java
0 → 100644
| 1 | +package com.xly.constant; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * 全局通用常量 | |
| 5 | + * 包含大模型解析提示语、请求头、资源前缀等 | |
| 6 | + */ | |
| 7 | +public class CommonConstant { | |
| 8 | + //重置方法 | |
| 9 | + public static final String RESET = "<button \n" + | |
| 10 | + " data-action=\"reset\" \n" + | |
| 11 | + " style=\"\n" + | |
| 12 | + " background: linear-gradient(135deg, #1677ff, #4096ff);\n" + | |
| 13 | + " color: white;\n" + | |
| 14 | + " border: none;\n" + | |
| 15 | + " padding: 6px 14px;\n" + | |
| 16 | + " border-radius: 6px;\n" + | |
| 17 | + " cursor: pointer;\n" + | |
| 18 | + " font-size: 12px;\n" + | |
| 19 | + " font-weight: 500;\n" + | |
| 20 | + " transition: all 0.3s ease;\n" + | |
| 21 | + " \"\n" + | |
| 22 | + " data-text=\"重置\"\n" + | |
| 23 | + " onclick=\"reset('重置')\"\n" + | |
| 24 | + " onmouseover=\"this.style.opacity='0.8'\"\n" + | |
| 25 | + " onmouseout=\"this.style.opacity='1'\">\n" + | |
| 26 | + " 重置\n" + | |
| 27 | + "</button> 复位 \n\n"; | |
| 28 | + | |
| 29 | +} | ... | ... |
src/main/java/com/xly/constant/ErrorCode.java
0 → 100644
| 1 | +package com.xly.constant; | |
| 2 | + | |
| 3 | +import lombok.Getter; | |
| 4 | + | |
| 5 | +/*** | |
| 6 | + * @Author 钱豹 | |
| 7 | + * @Date 23:04 2026/1/30 | |
| 8 | + * @Param | |
| 9 | + * @return | |
| 10 | + * @Description 异常码枚举 | |
| 11 | + **/ | |
| 12 | +@Getter | |
| 13 | +public enum ErrorCode { | |
| 14 | + | |
| 15 | + // 成功 | |
| 16 | + SUCCESS(200, "操作成功"), | |
| 17 | + SUCCESSMSG(201, "成功"), | |
| 18 | + ERRORMSG(202, "失败"), | |
| 19 | + WFHYY(203, "未返回原因"), | |
| 20 | + | |
| 21 | + // 客户端错误 | |
| 22 | + BAD_REQUEST(400, "请求参数错误"), | |
| 23 | + UNAUTHORIZED(401, "未授权"), | |
| 24 | + FORBIDDEN(403, "禁止访问"), | |
| 25 | + NOT_FOUND(404, "资源不存在"), | |
| 26 | + | |
| 27 | + // 参数错误 | |
| 28 | + PARAM_ERROR(40001, "参数错误"), | |
| 29 | + PARAM_REQUIRED(40002, "参数缺失"), | |
| 30 | + PARAM_TYPE_ERROR(40003, "参数类型错误"), | |
| 31 | + PARAM_FORMAT_ERROR(40004, "参数格式错误"), | |
| 32 | + | |
| 33 | + // 业务错误 | |
| 34 | + BUSINESS_ERROR(50001, "业务异常"), | |
| 35 | + DATA_ERROR(50002, "数据异常"), | |
| 36 | + DATA_NOT_FOUND(50003, "数据不存在"), | |
| 37 | + DATA_EXISTS(50004, "数据已存在"), | |
| 38 | + DATA_STATE_ERROR(50005, "数据状态异常"), | |
| 39 | + | |
| 40 | + // 用户相关 | |
| 41 | + USER_NOT_FOUND(60001, "用户不存在"), | |
| 42 | + USER_DISABLED(60002, "用户已禁用"), | |
| 43 | + USER_PASSWORD_ERROR(60003, "密码错误"), | |
| 44 | + USER_NOT_LOGIN(60004, "用户未登录"), | |
| 45 | + | |
| 46 | + // 权限相关 | |
| 47 | + PERMISSION_DENIED(70001, "权限不足"), | |
| 48 | + ROLE_NOT_FOUND(70002, "角色不存在"), | |
| 49 | + | |
| 50 | + // 系统错误 | |
| 51 | + SYSTEM_ERROR(10000, "系统异常"), | |
| 52 | + SERVICE_UNAVAILABLE(10001, "服务不可用"), | |
| 53 | + DB_ERROR(10002, "数据库异常"), | |
| 54 | + NETWORK_ERROR(10003, "网络异常"), | |
| 55 | + THIRD_PARTY_ERROR(10004, "第三方服务异常"), | |
| 56 | + CONFIG_ERROR(10005, "配置错误"), | |
| 57 | + | |
| 58 | + // 文件相关 | |
| 59 | + FILE_UPLOAD_ERROR(80001, "文件上传失败"), | |
| 60 | + FILE_NOT_FOUND(80002, "文件不存在"), | |
| 61 | + FILE_TYPE_ERROR(80003, "文件类型错误"), | |
| 62 | + FILE_SIZE_ERROR(80004, "文件大小超限"), | |
| 63 | + | |
| 64 | + PYTHON_ERROR(9001, "Python脚本执行失败"); | |
| 65 | + | |
| 66 | + private final Integer code; | |
| 67 | + private final String message; | |
| 68 | + | |
| 69 | + ErrorCode(Integer code, String message) { | |
| 70 | + this.code = code; | |
| 71 | + this.message = message; | |
| 72 | + } | |
| 73 | + | |
| 74 | + /** | |
| 75 | + * 根据code获取ErrorCode | |
| 76 | + */ | |
| 77 | + public static ErrorCode getByCode(Integer code) { | |
| 78 | + for (ErrorCode errorCode : values()) { | |
| 79 | + if (errorCode.getCode().equals(code)) { | |
| 80 | + return errorCode; | |
| 81 | + } | |
| 82 | + } | |
| 83 | + return SYSTEM_ERROR; | |
| 84 | + } | |
| 85 | +} | |
| 0 | 86 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/constant/ProcedureConstant.java
0 → 100644
| 1 | +package com.xly.constant; | |
| 2 | + | |
| 3 | +import java.util.ArrayList; | |
| 4 | +import java.util.HashMap; | |
| 5 | +import java.util.List; | |
| 6 | +import java.util.Map; | |
| 7 | + | |
| 8 | +/*** | |
| 9 | + * @Author 钱豹 | |
| 10 | + * @Date 22:41 2026/2/3 | |
| 11 | + * @Param | |
| 12 | + * @return | |
| 13 | + * @Description 调用过程的常量 | |
| 14 | + **/ | |
| 15 | +public class ProcedureConstant { | |
| 16 | + | |
| 17 | + public static final String PROTYPESTRING = "proc"; | |
| 18 | + public static final String CONFTYPESTRING = "sType"; | |
| 19 | + public static final String SSQLSTRSTRING = "sSqlStr"; | |
| 20 | + public static final String SRETURN = "sReturn"; | |
| 21 | + public static final String SCODE = "sCode"; | |
| 22 | + public static final String OUTSETSTRING = "outSet"; | |
| 23 | + public static final String SDEFAULT = "sDefault"; | |
| 24 | + public static final String IN = "IN"; | |
| 25 | + public static final String OUT = "OUT"; | |
| 26 | + public static final String HEADER = "HEADER"; | |
| 27 | + public static final String OUTLIST = "outList"; | |
| 28 | + public static final String OUTMAP = "outMap"; | |
| 29 | + | |
| 30 | + public static Map<String, Object> getRetMap(List<Map<String, Object>> proList, Map<String, Object> outMap) { | |
| 31 | + Map<String, Object> retMap = new HashMap<>(8); | |
| 32 | + Map<String, Object> proMap = new HashMap<>(4); | |
| 33 | + List<Map<String, Object>> outList = new ArrayList<>(1); | |
| 34 | + outList.add(outMap); | |
| 35 | + proMap.put("proData", proList); | |
| 36 | + proMap.put("outData", outList); | |
| 37 | + retMap.put("dataSet", proMap); | |
| 38 | + retMap.put(SCODE, outMap.get(SCODE)); | |
| 39 | + retMap.put(SRETURN, outMap.get(SRETURN)); | |
| 40 | + return retMap; | |
| 41 | + } | |
| 42 | + | |
| 43 | +} | ... | ... |
src/main/java/com/xly/constant/ReturnTypeCode.java
0 → 100644
| 1 | +package com.xly.constant; | |
| 2 | + | |
| 3 | +import lombok.Getter; | |
| 4 | + | |
| 5 | +/*** | |
| 6 | + * @Author 钱豹 | |
| 7 | + * @Date 23:04 2026/1/30 | |
| 8 | + * @Param | |
| 9 | + * @return | |
| 10 | + * @Description 异常码枚举 | |
| 11 | + **/ | |
| 12 | +@Getter | |
| 13 | +public enum ReturnTypeCode { | |
| 14 | + | |
| 15 | + // 成功 | |
| 16 | + HTML("html", "html"), | |
| 17 | + MAKEDOWN("makedown", "makedown"); | |
| 18 | + | |
| 19 | + | |
| 20 | + private final String code; | |
| 21 | + private final String message; | |
| 22 | + | |
| 23 | + ReturnTypeCode(String code, String message) { | |
| 24 | + this.code = code; | |
| 25 | + this.message = message; | |
| 26 | + } | |
| 27 | + | |
| 28 | + /** | |
| 29 | + * 根据code获取ErrorCode | |
| 30 | + */ | |
| 31 | + public static ReturnTypeCode getByCode(String code) { | |
| 32 | + for (ReturnTypeCode errorCode : values()) { | |
| 33 | + if (errorCode.getCode().equals(code)) { | |
| 34 | + return errorCode; | |
| 35 | + } | |
| 36 | + } | |
| 37 | + return MAKEDOWN; | |
| 38 | + } | |
| 39 | +} | |
| 0 | 40 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/constant/RuleCode.java
0 → 100644
| 1 | +package com.xly.constant; | |
| 2 | + | |
| 3 | +import lombok.Getter; | |
| 4 | + | |
| 5 | +/*** | |
| 6 | + * @Author 钱豹 | |
| 7 | + * @Date 23:04 2026/1/30 | |
| 8 | + * @Param | |
| 9 | + * @return | |
| 10 | + * @Description 异常码枚举 | |
| 11 | + **/ | |
| 12 | +@Getter | |
| 13 | +public enum RuleCode { | |
| 14 | +// {"sql":"SQL","pro":"过程","const":"常量"} | |
| 15 | + // 成功 | |
| 16 | + SQL("sql", "SQL"), | |
| 17 | + // 客户端错误 | |
| 18 | + PRO("pro", "过程"), | |
| 19 | + CONST("const", "常量"); | |
| 20 | + | |
| 21 | + private final String code; | |
| 22 | + private final String message; | |
| 23 | + | |
| 24 | + RuleCode(String code, String message) { | |
| 25 | + this.code = code; | |
| 26 | + this.message = message; | |
| 27 | + } | |
| 28 | + | |
| 29 | + /** | |
| 30 | + * 根据code获取ErrorCode | |
| 31 | + */ | |
| 32 | + public static RuleCode getByCode(Integer code) { | |
| 33 | + for (RuleCode errorCode : values()) { | |
| 34 | + if (errorCode.getCode().equals(code)) { | |
| 35 | + return errorCode; | |
| 36 | + } | |
| 37 | + } | |
| 38 | + return CONST; | |
| 39 | + } | |
| 40 | +} | |
| 0 | 41 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/constant/UrlErpConstant.java
0 → 100644
| 1 | +package com.xly.constant; | |
| 2 | + | |
| 3 | +/*** | |
| 4 | + * @Author 钱豹 | |
| 5 | + * @Date 0:37 2026/2/6 | |
| 6 | + * @Param | |
| 7 | + * @return | |
| 8 | + * @Description ERP URL后缀 | |
| 9 | + **/ | |
| 10 | +public class UrlErpConstant { | |
| 11 | + | |
| 12 | + public static final String getBusinessDataByFormcustomId = "/business/getBusinessDataByFormcustomId/{}?sModelsId={}&sName="; | |
| 13 | + | |
| 14 | +} | ... | ... |
src/main/java/com/xly/entity/AiResponseDTO.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 4 | +import com.xly.constant.ErrorCode; | |
| 5 | +import com.xly.constant.ReturnTypeCode; | |
| 6 | +import lombok.AllArgsConstructor; | |
| 7 | +import lombok.Builder; | |
| 8 | +import lombok.Data; | |
| 9 | +import lombok.NoArgsConstructor; | |
| 10 | + | |
| 11 | +import java.io.Serializable; | |
| 12 | + | |
| 13 | +/** | |
| 14 | + * TTS响应数据传输对象 | |
| 15 | + */ | |
| 16 | +@Data | |
| 17 | +@Builder | |
| 18 | +@NoArgsConstructor | |
| 19 | +@AllArgsConstructor | |
| 20 | +public class AiResponseDTO implements Serializable { | |
| 21 | + | |
| 22 | + private static final long serialVersionUID = 1L; | |
| 23 | + // AI文字部分 | |
| 24 | + private String aiText; | |
| 25 | + //系统拼接返回的文字部分 | |
| 26 | + private String systemText; | |
| 27 | + //业务场景名称 | |
| 28 | + private String sSceneName = StrUtil.EMPTY; | |
| 29 | + //业务方法名称 | |
| 30 | + private String sMethodName = StrUtil.EMPTY; | |
| 31 | + private String sReturnType = ReturnTypeCode.MAKEDOWN.getCode(); | |
| 32 | + | |
| 33 | +} | |
| 0 | 34 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/entity/ConfirmationData.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | + | |
| 5 | +import java.util.HashMap; | |
| 6 | +import java.util.Map; | |
| 7 | + | |
| 8 | +@Data | |
| 9 | +public class ConfirmationData { | |
| 10 | + private final String requestId; | |
| 11 | + private final String toolName; | |
| 12 | + private final Map<String, Object> parameters; | |
| 13 | + private final String initialResult; | |
| 14 | + private final long expiryTimestamp; // 超时时间戳 | |
| 15 | + | |
| 16 | + public ConfirmationData(String requestId, String toolName, | |
| 17 | + Map<String, Object> parameters, | |
| 18 | + String initialResult, | |
| 19 | + long timeoutMillis) { | |
| 20 | + this.requestId = requestId; | |
| 21 | + this.toolName = toolName; | |
| 22 | + this.parameters = parameters != null ? new HashMap<>(parameters) : new HashMap<>(); | |
| 23 | + this.initialResult = initialResult; | |
| 24 | + this.expiryTimestamp = System.currentTimeMillis() + timeoutMillis; | |
| 25 | + } | |
| 26 | + | |
| 27 | + // Getters 和 检查方法 | |
| 28 | + public boolean isExpired() { | |
| 29 | + return System.currentTimeMillis() > expiryTimestamp; | |
| 30 | + } | |
| 31 | + | |
| 32 | + public String getRequestId() { return requestId; } | |
| 33 | + public String getToolName() { return toolName; } | |
| 34 | + public Map<String, Object> getParameters() { return new HashMap<>(parameters); } | |
| 35 | + public String getInitialResult() { return initialResult; } | |
| 36 | + public long getExpiryTimestamp() { return expiryTimestamp; } | |
| 37 | +} | ... | ... |
src/main/java/com/xly/entity/DynamicNl2SqlRequest.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | + | |
| 4 | +import lombok.AllArgsConstructor; | |
| 5 | +import lombok.Data; | |
| 6 | +import lombok.NoArgsConstructor; | |
| 7 | + | |
| 8 | +/** | |
| 9 | + * 动态表结构的NL2SQL请求实体 | |
| 10 | + * 适配多场景、任意表结构的NL2SQL查询 | |
| 11 | + */ | |
| 12 | +@Data | |
| 13 | +@NoArgsConstructor | |
| 14 | +@AllArgsConstructor | |
| 15 | +public class DynamicNl2SqlRequest { | |
| 16 | + /** 涉及表名(多表用,分隔,如product,sales、order,user) */ | |
| 17 | + private String tableNames; | |
| 18 | + /** 表结构详情(严格按格式:表名(字段1:类型,字段2:类型,主键/外键),多表换行/逗号分隔) */ | |
| 19 | + private String tableStruct; | |
| 20 | + /** 用户自然语言查询问题 */ | |
| 21 | + private String question; | |
| 22 | +} | |
| 0 | 23 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/entity/ErpDataset.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | + | |
| 5 | +import java.util.List; | |
| 6 | +import java.util.Map; | |
| 7 | + | |
| 8 | +/*** | |
| 9 | + * @Author 钱豹 | |
| 10 | + * @Date 1:22 2026/2/6 | |
| 11 | + * @Param | |
| 12 | + * @return | |
| 13 | + * @Description ERP 返回的结果集 | |
| 14 | + **/ | |
| 15 | +@Data | |
| 16 | +public class ErpDataset { | |
| 17 | + private Integer start; | |
| 18 | + private Integer pageSize; | |
| 19 | + private Integer totalCount; | |
| 20 | + private Integer billNum; | |
| 21 | + private Integer end; | |
| 22 | + private Integer totalPageCount; | |
| 23 | + private Integer previousPageNo; | |
| 24 | + private Integer currentPageNo; | |
| 25 | + private Integer nextPageNo; | |
| 26 | + private List<ErpRow> rows; | |
| 27 | +} | ... | ... |
src/main/java/com/xly/entity/ErpResult.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | + | |
| 5 | +/*** | |
| 6 | + * @Author 钱豹 | |
| 7 | + * @Date 22:42 2026/1/30 | |
| 8 | + * @Param | |
| 9 | + * @return | |
| 10 | + * @Description 参数实体类 | |
| 11 | + **/ | |
| 12 | +@Data | |
| 13 | +public class ErpResult { | |
| 14 | + | |
| 15 | + private Integer code; | |
| 16 | + private String msg; | |
| 17 | + private ErpDataset dataset; | |
| 18 | + | |
| 19 | +} | ... | ... |
src/main/java/com/xly/entity/ErpRow.java
0 → 100644
src/main/java/com/xly/entity/Nl2SqlRequest.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | +import lombok.AllArgsConstructor; | |
| 4 | +import lombok.Data; | |
| 5 | +import lombok.NoArgsConstructor; | |
| 6 | + | |
| 7 | +import java.util.List; | |
| 8 | +import java.util.Map; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * NL2SQL统一请求参数 | |
| 12 | + */ | |
| 13 | +@Data | |
| 14 | +@NoArgsConstructor | |
| 15 | +@AllArgsConstructor | |
| 16 | +public class Nl2SqlRequest { | |
| 17 | + /** 用户自然语言查询语句 */ | |
| 18 | + private String question; | |
| 19 | +} | |
| 20 | + | |
| 21 | + | |
| 22 | + | ... | ... |
src/main/java/com/xly/entity/Nl2SqlResult.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | +import lombok.AllArgsConstructor; | |
| 4 | +import lombok.Data; | |
| 5 | +import lombok.NoArgsConstructor; | |
| 6 | + | |
| 7 | +import java.util.List; | |
| 8 | +import java.util.Map; | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | +/** | |
| 13 | + * NL2SQL统一返回结果 | |
| 14 | + */ | |
| 15 | +@Data | |
| 16 | +@NoArgsConstructor | |
| 17 | +@AllArgsConstructor | |
| 18 | +public class Nl2SqlResult { | |
| 19 | + /** 生成并校验后的可执行MySQL SQL */ | |
| 20 | + private String generateSql; | |
| 21 | + /** SQL执行结构化结果 */ | |
| 22 | + private List<Map<String, Object>> sqlResult; | |
| 23 | + /** 结果自然语言解释 */ | |
| 24 | + private String resultExplain; | |
| 25 | +} | |
| 26 | + | ... | ... |
src/main/java/com/xly/entity/ParamRule.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | +import lombok.Data; | |
| 4 | + | |
| 5 | +import java.time.LocalDateTime; | |
| 6 | + | |
| 7 | +/*** | |
| 8 | + * @Author 钱豹 | |
| 9 | + * @Date 22:42 2026/1/30 | |
| 10 | + * @Param | |
| 11 | + * @return | |
| 12 | + * @Description 参数实体类 | |
| 13 | + **/ | |
| 14 | +@Data | |
| 15 | +public class ParamRule { | |
| 16 | + private Integer iOrder; | |
| 17 | + private String sParamValue; | |
| 18 | + private String sParentId; | |
| 19 | + private String sParam; | |
| 20 | + private String sRule; | |
| 21 | + private String sType; | |
| 22 | + private String sExampleValue; | |
| 23 | + private String sDefaultValue; | |
| 24 | + private String sParamMissMemo; | |
| 25 | + private String sParamConfig; | |
| 26 | + private String sCopyTo; | |
| 27 | + private String sRuleTs; | |
| 28 | + private Boolean bTipModel; | |
| 29 | + private Boolean bEmpty; | |
| 30 | + private Boolean bConfirmAfter; | |
| 31 | + | |
| 32 | +} | ... | ... |
src/main/java/com/xly/entity/SceneDto.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | +import jakarta.persistence.Column; | |
| 4 | +import jakarta.persistence.Id; | |
| 5 | +import lombok.Data; | |
| 6 | + | |
| 7 | +import java.time.LocalDateTime; | |
| 8 | + | |
| 9 | +/*** | |
| 10 | + * @Author 钱豹 | |
| 11 | + * @Date 22:41 2026/1/30 | |
| 12 | + * @Param | |
| 13 | + * @return | |
| 14 | + * @Description 模型实体 | |
| 15 | + **/ | |
| 16 | +@Data | |
| 17 | +public class SceneDto { | |
| 18 | + @Id | |
| 19 | +// @GeneratedValue(strategy = GenerationType.IDENTITY) | |
| 20 | + private String sId; | |
| 21 | + | |
| 22 | + @Column(name = "iOrder") | |
| 23 | + private Integer iOrder; | |
| 24 | + | |
| 25 | + @Column(name = "tCreateDate") | |
| 26 | + private LocalDateTime tCreateDate; | |
| 27 | + | |
| 28 | + @Column(name = "sMakePerson") | |
| 29 | + private String sMakePerson; | |
| 30 | + | |
| 31 | + private String sBrandsId; | |
| 32 | + | |
| 33 | + private String sSubsidiaryId; | |
| 34 | + | |
| 35 | + private String sBillNo; | |
| 36 | + | |
| 37 | + private String sSceneNo; | |
| 38 | + | |
| 39 | + private String sSceneName; | |
| 40 | + | |
| 41 | + private String sNickName; | |
| 42 | + | |
| 43 | + private String sSceneContext; | |
| 44 | + | |
| 45 | + private String sStatus; | |
| 46 | + | |
| 47 | + | |
| 48 | + private LocalDateTime tUpdateDate; | |
| 49 | + | |
| 50 | +} | ... | ... |
src/main/java/com/xly/entity/SceneIntentParseReq.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | +import lombok.Data; | |
| 3 | + | |
| 4 | +/** | |
| 5 | + * 意图解析请求参数:传给大模型的入参,包含用户输入+可访问场景列表 | |
| 6 | + */ | |
| 7 | +@Data | |
| 8 | +public class SceneIntentParseReq{ | |
| 9 | + /** | |
| 10 | + * 用户自然语言输入 | |
| 11 | + */ | |
| 12 | + private String userInput; | |
| 13 | + /** | |
| 14 | + * 该用户权限内的可访问场景(格式:场景编码-场景名称-支持功能,如ORDER_OPERATE-订单操作-下单、查订单、取消订单) | |
| 15 | + */ | |
| 16 | + private String authScenesDesc; | |
| 17 | +} | ... | ... |
src/main/java/com/xly/entity/SceneIntentParseResp.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | +import lombok.AllArgsConstructor; | |
| 4 | +import lombok.Data; | |
| 5 | +import lombok.NoArgsConstructor; | |
| 6 | + | |
| 7 | +import java.util.Map; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * 大模型意图解析响应DTO:强制大模型按此格式返回,方便后端解析 | |
| 11 | + * 核心:仅返回场景编码(如ORDER_OPERATE),无需多余描述 | |
| 12 | + */ | |
| 13 | +@Data | |
| 14 | +@NoArgsConstructor | |
| 15 | +@AllArgsConstructor | |
| 16 | +public class SceneIntentParseResp { | |
| 17 | + /** | |
| 18 | + * 匹配的业务场景编码,必须是BusinessScene的枚举名称(如ORDER_OPERATE/CUSTOMER_MANAGE/STOCK_QUERY) | |
| 19 | + * 无匹配场景时,返回:NO_MATCH | |
| 20 | + */ | |
| 21 | + private String sceneCode; | |
| 22 | + | |
| 23 | + | |
| 24 | + /** | |
| 25 | + * 业务场景名(如销量订单、送货单) | |
| 26 | + */ | |
| 27 | + private String scene; | |
| 28 | + | |
| 29 | + /** | |
| 30 | + * 操作方法名(如增加、修改、审核、物流跟踪) | |
| 31 | + */ | |
| 32 | + private String method; | |
| 33 | + | |
| 34 | + /** | |
| 35 | + * JSON结构化参数(键=参数名,值=参数值,类型与工具描述一致) | |
| 36 | + */ | |
| 37 | + private Map<String, Object> params; | |
| 38 | + | |
| 39 | + | |
| 40 | +} | |
| 41 | + | ... | ... |
src/main/java/com/xly/entity/SceneTemplate.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | +import jakarta.persistence.*; | |
| 4 | +import lombok.Data; | |
| 5 | + | |
| 6 | +/** | |
| 7 | + * 数据库存储的动态模板 | |
| 8 | + */ | |
| 9 | +@Entity | |
| 10 | +@Table(name = "ai_scene_template") | |
| 11 | +@Data | |
| 12 | +public class SceneTemplate { | |
| 13 | + | |
| 14 | + @Id | |
| 15 | + @GeneratedValue(strategy = GenerationType.IDENTITY) | |
| 16 | + private Long id; | |
| 17 | + | |
| 18 | + @Column(name = "scene_code") | |
| 19 | + private String sceneCode; // 场景编码 | |
| 20 | + | |
| 21 | + @Column(name = "scene_name") | |
| 22 | + private String sceneName; // 场景名称 | |
| 23 | + | |
| 24 | + @Column(name = "template_content", columnDefinition = "TEXT") | |
| 25 | + private String templateContent; // 模板内容 | |
| 26 | + | |
| 27 | + @Column(name = "variables_config", columnDefinition = "JSON") | |
| 28 | + private String variablesConfig; // 变量配置 | |
| 29 | + | |
| 30 | + @Column(name = "rules_config", columnDefinition = "JSON") | |
| 31 | + private String rulesConfig; // 规则配置 | |
| 32 | + | |
| 33 | + @Column(name = "is_active") | |
| 34 | + private Boolean active = true; | |
| 35 | + | |
| 36 | + public void setId(Long id) { | |
| 37 | + this.id = id; | |
| 38 | + } | |
| 39 | + | |
| 40 | + public Long getId() { | |
| 41 | + return id; | |
| 42 | + } | |
| 43 | +} | |
| 0 | 44 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/entity/ToolMeta.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | + | |
| 4 | +import lombok.Data; | |
| 5 | + | |
| 6 | +import java.time.LocalDateTime; | |
| 7 | +import java.util.List; | |
| 8 | +import java.util.Map; | |
| 9 | + | |
| 10 | +/*** | |
| 11 | + * @Author 钱豹 | |
| 12 | + * @Date 22:41 2026/1/30 | |
| 13 | + * @Param | |
| 14 | + * @return | |
| 15 | + * @Description 工具实体 | |
| 16 | + **/ | |
| 17 | +@Data | |
| 18 | +public class ToolMeta { | |
| 19 | + private String sId; | |
| 20 | + private Integer iOrder; | |
| 21 | + private LocalDateTime tCreateDate; | |
| 22 | + private String sBrandsId; | |
| 23 | + private String sSubsidiaryId; | |
| 24 | + private String sMakePerson; | |
| 25 | + private String sBillNo; | |
| 26 | + private String sFormId; | |
| 27 | + private String sStatus; | |
| 28 | + private String sceneName; | |
| 29 | + private String sMethodName; | |
| 30 | + private String stoolDesc; | |
| 31 | + private String sParamRules; | |
| 32 | + private Integer iBizType; | |
| 33 | + private LocalDateTime tUpdateDate; | |
| 34 | + private String sMethodNo; | |
| 35 | + private String sControlName; | |
| 36 | + private String sApiUrl; | |
| 37 | + private String sSrcFormId; | |
| 38 | + private String sBizContent; | |
| 39 | + private String sSceneId; | |
| 40 | + private String sInputTabelName; | |
| 41 | + private String sStructureMemo; | |
| 42 | + private String sAIshowfield; | |
| 43 | + private List<Map<String,Object>> sAIshowfieldShow; | |
| 44 | + private Integer iActionType; | |
| 45 | + private String sendUrl; | |
| 46 | + private List<ParamRule> paramRuleList;//模型 | |
| 47 | + private List<ParamRule> paramRuleListCheck;//需要校验 | |
| 48 | + private List<ParamRule> paramRuleListAll;//所有的 | |
| 49 | + private LocalDateTime tMakeDate; | |
| 50 | +} | ... | ... |
src/main/java/com/xly/entity/UserSceneSession.java
0 → 100644
| 1 | +package com.xly.entity; | |
| 2 | + | |
| 3 | + | |
| 4 | +import cn.hutool.core.util.ObjectUtil; | |
| 5 | +import lombok.Data; | |
| 6 | + | |
| 7 | +import java.util.List; | |
| 8 | +import java.util.Map; | |
| 9 | +import java.util.concurrent.atomic.AtomicInteger; | |
| 10 | +import java.util.stream.Collectors; | |
| 11 | + | |
| 12 | +/** | |
| 13 | + * 用户会话状态:记录每个用户的场景选择状态,实现场景会话持久化 | |
| 14 | + * 存储:用户ID、权限内场景、是否已选场景、当前选定场景 | |
| 15 | + */ | |
| 16 | +@Data | |
| 17 | +public class UserSceneSession { | |
| 18 | + /** | |
| 19 | + * 唯一标识:用户ID | |
| 20 | + */ | |
| 21 | + private String userId; | |
| 22 | + private String authorization; | |
| 23 | + /** | |
| 24 | + * 该用户权限内可访问的所有场景(从权限映射中获取) | |
| 25 | + */ | |
| 26 | + private List<SceneDto> authScenes; | |
| 27 | + | |
| 28 | + private List<ToolMeta> authTool; | |
| 29 | + /** | |
| 30 | + * 是否已选择场景(true=已选,进入专属场景交互;false=未选,先展示选择界面) | |
| 31 | + */ | |
| 32 | + private boolean sceneSelected = false; | |
| 33 | + /** | |
| 34 | + * 当前选定的业务场景(sceneSelected=true时才有值) | |
| 35 | + */ | |
| 36 | + private SceneDto currentScene; | |
| 37 | + | |
| 38 | + private ToolMeta currentTool; | |
| 39 | + /*** | |
| 40 | + * @Author 钱豹 | |
| 41 | + * @Date 10:07 2026/1/31 | |
| 42 | + * @Param | |
| 43 | + * @return | |
| 44 | + * @Description 当前已有参数 | |
| 45 | + **/ | |
| 46 | + private Map<String,Object> currentArgs; | |
| 47 | + | |
| 48 | + private String sFunPrompts; //方法返回的参数补全提示 | |
| 49 | + | |
| 50 | + private Boolean bCleanMemory = false; | |
| 51 | + /** | |
| 52 | + * 构建场景选择提示语:展示权限内场景,引导用户选择 | |
| 53 | + * @return 自然语言提示语 | |
| 54 | + */ | |
| 55 | + public String buildSceneSelectHint() { | |
| 56 | + if (authScenes == null || authScenes.isEmpty()) { | |
| 57 | + return "<p style='color:red;'>抱歉,你暂无任何业务场景的访问权限,请联系管理员开通!</p>"; | |
| 58 | + } | |
| 59 | + // 按名称分组 | |
| 60 | + StringBuilder sb = new StringBuilder("<div style='line-height:1.8;'>"); | |
| 61 | + sb.append("<span>请输入序号或点链接进入</span>"); | |
| 62 | + sb.append("<ul style='padding-left:10px;margin:0;list-style: none;'>"); | |
| 63 | + for(int i=0;i<authScenes.size();i++){ | |
| 64 | + SceneDto sceneDto = authScenes.get(i); | |
| 65 | + sb.append("<li>").append(i+1).append("、").append("<span style=\"text-decoration: underline;\" data-action=\"reset\" data-text=\"") .append(i+1).append("\" onclick=\"reset('").append(i+1).append("')\">") .append(sceneDto.getSNickName()).append("</span><font style='color:#666; font-size:14px;'>(").append(sceneDto.getSSceneContext()).append(")</font>").append("</li>"); | |
| 66 | + } | |
| 67 | + sb.append("</ul>"); | |
| 68 | + sb.append("<span style='color:red; font-size:14px;'>提示:可直接输入问题,系统会自动匹配智能体! </span>"); | |
| 69 | + sb.append("</div>"); | |
| 70 | + return sb.toString(); | |
| 71 | + | |
| 72 | + } | |
| 73 | + | |
| 74 | + | |
| 75 | + // 新增:将权限内场景转换为「大模型可识别的描述字符串」 | |
| 76 | + public String buildAuthScenesForLlm(List<ToolMeta> metasAll) { | |
| 77 | + if (authScenes == null || authScenes.isEmpty()) { | |
| 78 | + return "无可用场景"; | |
| 79 | + } | |
| 80 | + // 格式:场景编码-场景名称-支持功能,如ORDER_OPERATE-订单操作-下单、查订单、取消订单 | |
| 81 | + return authScenes.stream() | |
| 82 | + .map(scene -> "sceneCode:" + scene.getSSceneNo() + ",场景名称:" + scene.getSSceneName() + ",场景内容:\n" + getSceneContext(scene,metasAll)) | |
| 83 | + .collect(Collectors.joining("\n")); | |
| 84 | + } | |
| 85 | + /*** | |
| 86 | + * @Author 钱豹 | |
| 87 | + * @Date 14:12 2026/2/6 | |
| 88 | + * @Param [sceneDto, metasAll] | |
| 89 | + * @return java.lang.String | |
| 90 | + * @Description 方法名称拼接 | |
| 91 | + **/ | |
| 92 | + private String getSceneContext(SceneDto sceneDto,List<ToolMeta> metasAll){ | |
| 93 | + List<ToolMeta> metas = metasAll.stream().filter(m->m.getSSceneId().equals(sceneDto.getSId())).collect(Collectors.toUnmodifiableList()); | |
| 94 | + StringBuilder sb = new StringBuilder(); | |
| 95 | + AtomicInteger index = new AtomicInteger(1); | |
| 96 | + metas.forEach(m -> { | |
| 97 | + sb.append(index.getAndIncrement()) | |
| 98 | + .append(". 【") | |
| 99 | +// .append(m.getSMethodNo()) | |
| 100 | +// .append("-") | |
| 101 | + .append(m.getSMethodName()) | |
| 102 | + .append("】") | |
| 103 | + .append(m.getStoolDesc()) | |
| 104 | + .append("\n"); | |
| 105 | + }); | |
| 106 | + if(ObjectUtil.isEmpty(sb)){ | |
| 107 | + sb.append(sceneDto.getSSceneContext()); | |
| 108 | + } | |
| 109 | + return sb.toString(); | |
| 110 | + } | |
| 111 | + | |
| 112 | + /** | |
| 113 | + * 根据用户输入的序号,匹配选定场景 | |
| 114 | + * @param input 用户输入的序号(如1/2/3) | |
| 115 | + * @return true=匹配成功,false=匹配失败 | |
| 116 | + */ | |
| 117 | + public boolean selectSceneByInput(String input) { | |
| 118 | + try { | |
| 119 | + int index = Integer.parseInt(input) - 1; | |
| 120 | + if (index >= 0 && index < authScenes.size()) { | |
| 121 | + this.currentScene = authScenes.get(index); | |
| 122 | + this.sceneSelected = true; | |
| 123 | + return true; | |
| 124 | + } | |
| 125 | + } catch (NumberFormatException e) { | |
| 126 | + // 非数字输入,直接返回false | |
| 127 | + } | |
| 128 | + return false; | |
| 129 | + } | |
| 130 | + | |
| 131 | + | |
| 132 | +} | |
| 0 | 133 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/exception/GlobalExceptionHandler.java
0 → 100644
| 1 | +package com.xly.exception; | |
| 2 | + | |
| 3 | +import com.xly.constant.ErrorCode; | |
| 4 | +import com.xly.exception.dto.BusinessException; | |
| 5 | +import com.xly.tts.bean.TTSResponseDTO; | |
| 6 | +import jakarta.servlet.http.HttpServletRequest; | |
| 7 | +import jakarta.validation.ConstraintViolationException; | |
| 8 | +import lombok.extern.slf4j.Slf4j; | |
| 9 | +import org.junit.jupiter.api.Order; | |
| 10 | +import org.springframework.core.Ordered; | |
| 11 | +import org.springframework.dao.DataAccessException; | |
| 12 | +import org.springframework.dao.DataIntegrityViolationException; | |
| 13 | +import org.springframework.dao.DuplicateKeyException; | |
| 14 | +import org.springframework.http.converter.HttpMessageConversionException; | |
| 15 | +import org.springframework.jdbc.BadSqlGrammarException; | |
| 16 | +import org.springframework.web.HttpMediaTypeNotSupportedException; | |
| 17 | +import org.springframework.web.HttpRequestMethodNotSupportedException; | |
| 18 | +import org.springframework.web.bind.MethodArgumentNotValidException; | |
| 19 | +import org.springframework.web.bind.annotation.ExceptionHandler; | |
| 20 | +import org.springframework.web.bind.annotation.RestControllerAdvice; | |
| 21 | +import org.springframework.web.servlet.NoHandlerFoundException; | |
| 22 | + | |
| 23 | +import java.util.Date; | |
| 24 | +import java.util.HashMap; | |
| 25 | +import java.util.List; | |
| 26 | +import java.util.Map; | |
| 27 | +import java.util.stream.Collectors; | |
| 28 | + | |
| 29 | +@Slf4j | |
| 30 | +@RestControllerAdvice | |
| 31 | +@Order(Ordered.HIGHEST_PRECEDENCE) | |
| 32 | +public class GlobalExceptionHandler { | |
| 33 | + | |
| 34 | + | |
| 35 | + /** | |
| 36 | + * 处理所有异常 | |
| 37 | + */ | |
| 38 | + @ExceptionHandler(Exception.class) | |
| 39 | + public TTSResponseDTO handleException(HttpServletRequest request, Exception e) { | |
| 40 | + log.error("请求地址: {}, 全局异常: ", request.getRequestURI(), e); | |
| 41 | + | |
| 42 | + // 获取请求信息 | |
| 43 | + String method = request.getMethod(); | |
| 44 | + String uri = request.getRequestURI(); | |
| 45 | + String queryString = request.getQueryString(); | |
| 46 | + | |
| 47 | + // 记录异常日志(可存入数据库) | |
| 48 | +// ErrorLog errorLog = ErrorLog.builder() | |
| 49 | +// .method(method) | |
| 50 | +// .uri(uri) | |
| 51 | +// .queryString(queryString) | |
| 52 | +// .exceptionName(e.getClass().getName()) | |
| 53 | +// .exceptionMessage(e.getMessage()) | |
| 54 | +// .stackTrace(ExceptionUtils.getStackTrace(e)) | |
| 55 | +// .ip(IpUtil.getIpAddr(request)) | |
| 56 | +// .userAgent(request.getHeader("User-Agent")) | |
| 57 | +// .timestamp(new Date()) | |
| 58 | +// .build(); | |
| 59 | +// | |
| 60 | +// // 异步保存错误日志 | |
| 61 | +// saveErrorLogAsync(errorLog); | |
| 62 | + | |
| 63 | + // 生产环境隐藏详细错误信息 | |
| 64 | + if (isProduction()) { | |
| 65 | + return TTSResponseDTO.error(ErrorCode.SYSTEM_ERROR); | |
| 66 | + } else { | |
| 67 | + // 开发环境返回详细错误信息 | |
| 68 | + Map<String, Object> errorDetail = new HashMap<>(); | |
| 69 | + errorDetail.put("exception", e.getClass().getName()); | |
| 70 | + errorDetail.put("message", e.getMessage()); | |
| 71 | + errorDetail.put("path", uri); | |
| 72 | + errorDetail.put("method", method); | |
| 73 | + errorDetail.put("timestamp", new Date()); | |
| 74 | + return TTSResponseDTO.error(ErrorCode.SYSTEM_ERROR.getCode(), "系统异常: " + e.getMessage()); | |
| 75 | + } | |
| 76 | + } | |
| 77 | + | |
| 78 | + /** | |
| 79 | + * 处理业务异常 | |
| 80 | + */ | |
| 81 | + @ExceptionHandler(BusinessException.class) | |
| 82 | + public TTSResponseDTO handleBusinessException(BusinessException e) { | |
| 83 | + log.warn("业务异常: {}", e.getMessage()); | |
| 84 | + return TTSResponseDTO.error(e.getCode(), e.getMessage()); | |
| 85 | + } | |
| 86 | + | |
| 87 | + /** | |
| 88 | + * 处理数据校验异常 | |
| 89 | + */ | |
| 90 | + @ExceptionHandler(MethodArgumentNotValidException.class) | |
| 91 | + public TTSResponseDTO handleMethodArgumentNotValidException( | |
| 92 | + MethodArgumentNotValidException e) { | |
| 93 | + List<String> errors = e.getBindingResult().getFieldErrors() | |
| 94 | + .stream() | |
| 95 | + .map(error -> error.getField() + ": " + error.getDefaultMessage()) | |
| 96 | + .collect(Collectors.toList()); | |
| 97 | + | |
| 98 | + String message = String.join("; ", errors); | |
| 99 | + log.warn("参数校验失败: {}", message); | |
| 100 | + return TTSResponseDTO.error(ErrorCode.PARAM_ERROR.getCode(), message); | |
| 101 | + } | |
| 102 | + | |
| 103 | + /** | |
| 104 | + * 处理约束违反异常 | |
| 105 | + */ | |
| 106 | + @ExceptionHandler(ConstraintViolationException.class) | |
| 107 | + public TTSResponseDTO handleConstraintViolationException( | |
| 108 | + ConstraintViolationException e) { | |
| 109 | + List<String> errors = e.getConstraintViolations() | |
| 110 | + .stream() | |
| 111 | + .map(violation -> | |
| 112 | + violation.getPropertyPath() + ": " + violation.getMessage()) | |
| 113 | + .collect(Collectors.toList()); | |
| 114 | + | |
| 115 | + String message = String.join("; ", errors); | |
| 116 | + log.warn("参数约束违反: {}", message); | |
| 117 | + return TTSResponseDTO.error(ErrorCode.PARAM_ERROR.getCode(), message); | |
| 118 | + } | |
| 119 | + | |
| 120 | + /** | |
| 121 | + * 处理Http请求方法不支持异常 | |
| 122 | + */ | |
| 123 | + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) | |
| 124 | + public TTSResponseDTO handleHttpRequestMethodNotSupportedException( | |
| 125 | + HttpRequestMethodNotSupportedException e) { | |
| 126 | + log.warn("HTTP方法不支持: {} {}", e.getMethod(), e.getSupportedHttpMethods()); | |
| 127 | + return TTSResponseDTO.error(ErrorCode.BAD_REQUEST.getCode(), | |
| 128 | + "请求方法 '" + e.getMethod() + "' 不支持"); | |
| 129 | + } | |
| 130 | + | |
| 131 | + /** | |
| 132 | + * 处理媒体类型不支持异常 | |
| 133 | + */ | |
| 134 | + @ExceptionHandler(HttpMediaTypeNotSupportedException.class) | |
| 135 | + public TTSResponseDTO handleHttpMediaTypeNotSupportedException( | |
| 136 | + HttpMediaTypeNotSupportedException e) { | |
| 137 | + log.warn("媒体类型不支持: {}", e.getContentType()); | |
| 138 | + return TTSResponseDTO.error(ErrorCode.BAD_REQUEST.getCode(), | |
| 139 | + "媒体类型不支持: " + e.getContentType()); | |
| 140 | + } | |
| 141 | + | |
| 142 | + /** | |
| 143 | + * 处理404异常 | |
| 144 | + */ | |
| 145 | + @ExceptionHandler(NoHandlerFoundException.class) | |
| 146 | + public TTSResponseDTO handleNoHandlerFoundException( | |
| 147 | + NoHandlerFoundException e) { | |
| 148 | + log.warn("接口不存在: {} {}", e.getHttpMethod(), e.getRequestURL()); | |
| 149 | + return TTSResponseDTO.error(ErrorCode.NOT_FOUND.getCode(), | |
| 150 | + "接口不存在: " + e.getRequestURL()); | |
| 151 | + } | |
| 152 | + | |
| 153 | + /** | |
| 154 | + * 处理类型转换异常 | |
| 155 | + */ | |
| 156 | + @ExceptionHandler(HttpMessageConversionException.class) | |
| 157 | + public TTSResponseDTO handleHttpMessageConversionException( | |
| 158 | + HttpMessageConversionException e) { | |
| 159 | + log.warn("HTTP消息转换异常: {}", e.getMessage()); | |
| 160 | + return TTSResponseDTO.error(ErrorCode.PARAM_ERROR.getCode(), | |
| 161 | + "参数格式错误"); | |
| 162 | + } | |
| 163 | + | |
| 164 | + /** | |
| 165 | + * 处理数据库异常 | |
| 166 | + */ | |
| 167 | + @ExceptionHandler(DataAccessException.class) | |
| 168 | + public TTSResponseDTO handleDataAccessException(DataAccessException e) { | |
| 169 | + log.error("数据库异常: {}", e.getMessage(), e); | |
| 170 | + | |
| 171 | + // 根据具体异常类型细化处理 | |
| 172 | + if (e instanceof DuplicateKeyException) { | |
| 173 | + return TTSResponseDTO.error(ErrorCode.DATA_EXISTS.getCode(), | |
| 174 | + "数据已存在"); | |
| 175 | + } else if (e instanceof DataIntegrityViolationException) { | |
| 176 | + return TTSResponseDTO.error(ErrorCode.DATA_ERROR.getCode(), | |
| 177 | + "数据完整性违反"); | |
| 178 | + } else if (e instanceof BadSqlGrammarException) { | |
| 179 | + return TTSResponseDTO.error(ErrorCode.DB_ERROR.getCode(), | |
| 180 | + "SQL语法错误"); | |
| 181 | + } | |
| 182 | + | |
| 183 | + return TTSResponseDTO.error(ErrorCode.DB_ERROR); | |
| 184 | + } | |
| 185 | + | |
| 186 | + /** | |
| 187 | + * 处理JWT异常 | |
| 188 | + */ | |
| 189 | +// @ExceptionHandler(JwtException.class) | |
| 190 | +// public TTSResponseDTO handleJwtException(JwtException e) { | |
| 191 | +// log.warn("JWT异常: {}", e.getMessage()); | |
| 192 | +// return ApiResult.error(ErrorCode.UNAUTHORIZED.getCode(), | |
| 193 | +// "Token无效或已过期"); | |
| 194 | +// } | |
| 195 | + | |
| 196 | +// /** | |
| 197 | +// * 异步保存错误日志 | |
| 198 | +// */ | |
| 199 | +// @Async | |
| 200 | +// public void saveErrorLogAsync(ErrorLog errorLog) { | |
| 201 | +// try { | |
| 202 | +// // 保存到数据库或日志文件 | |
| 203 | +// errorLogService.save(errorLog); | |
| 204 | +// } catch (Exception ex) { | |
| 205 | +// log.error("保存错误日志失败", ex); | |
| 206 | +// } | |
| 207 | +// } | |
| 208 | + | |
| 209 | + /*** | |
| 210 | + * @Author 钱豹 | |
| 211 | + * @Date 23:21 2026/1/30 | |
| 212 | + * @Param [] | |
| 213 | + * @return boolean | |
| 214 | + * @Description 判断是否生产环境还是开发环境 应该根据yml 配置来 后期拓展 | |
| 215 | + **/ | |
| 216 | + private boolean isProduction() { | |
| 217 | + //TODO | |
| 218 | + String profile = "dev"; | |
| 219 | + return "prod".equals(profile); | |
| 220 | + } | |
| 221 | +} | |
| 0 | 222 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/exception/TtsExceptionFilter.java
0 → 100644
| 1 | +package com.xly.exception; | |
| 2 | + | |
| 3 | +import com.fasterxml.jackson.databind.ObjectMapper; | |
| 4 | +import com.xly.tts.bean.TTSResponseDTO; | |
| 5 | +import jakarta.servlet.*; | |
| 6 | +import jakarta.servlet.http.HttpServletRequest; | |
| 7 | +import jakarta.servlet.http.HttpServletResponse; | |
| 8 | +import lombok.RequiredArgsConstructor; | |
| 9 | +import lombok.extern.slf4j.Slf4j; | |
| 10 | +import org.springframework.core.annotation.Order; | |
| 11 | +import org.springframework.http.HttpStatus; | |
| 12 | +import org.springframework.http.MediaType; | |
| 13 | +import org.springframework.stereotype.Component; | |
| 14 | + | |
| 15 | +import java.io.IOException; | |
| 16 | + | |
| 17 | +@Slf4j | |
| 18 | +@Component | |
| 19 | +@Order(1) | |
| 20 | +@RequiredArgsConstructor | |
| 21 | +public class TtsExceptionFilter implements Filter { | |
| 22 | + | |
| 23 | + private final ObjectMapper objectMapper; | |
| 24 | + | |
| 25 | + @Override | |
| 26 | + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) | |
| 27 | + throws IOException, ServletException { | |
| 28 | + | |
| 29 | + HttpServletRequest httpRequest = (HttpServletRequest) request; | |
| 30 | + HttpServletResponse httpResponse = (HttpServletResponse) response; | |
| 31 | + | |
| 32 | + // 只处理 TTS 相关接口 | |
| 33 | + if (!httpRequest.getRequestURI().contains("/api/tts/")) { | |
| 34 | + chain.doFilter(request, response); | |
| 35 | + return; | |
| 36 | + } | |
| 37 | + | |
| 38 | + try { | |
| 39 | + chain.doFilter(request, response); | |
| 40 | + } catch (Exception ex) { | |
| 41 | + log.error("TTS接口异常: {}", httpRequest.getRequestURI(), ex); | |
| 42 | + | |
| 43 | + // 清除之前的响应 | |
| 44 | + if (httpResponse.isCommitted()) { | |
| 45 | + return; | |
| 46 | + } | |
| 47 | + | |
| 48 | + httpResponse.reset(); | |
| 49 | + httpResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); | |
| 50 | + httpResponse.setContentType(MediaType.APPLICATION_JSON_VALUE); | |
| 51 | + httpResponse.setCharacterEncoding("UTF-8"); | |
| 52 | + | |
| 53 | + TTSResponseDTO errorResult = TTSResponseDTO.error( | |
| 54 | + HttpStatus.INTERNAL_SERVER_ERROR.value(), | |
| 55 | + "语音合成服务异常: " + ex.getMessage() | |
| 56 | + ); | |
| 57 | + | |
| 58 | + String jsonResponse = objectMapper.writeValueAsString(errorResult); | |
| 59 | + httpResponse.getWriter().write(jsonResponse); | |
| 60 | + httpResponse.getWriter().flush(); | |
| 61 | + } | |
| 62 | + } | |
| 63 | +} | |
| 0 | 64 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/exception/dto/AuthenticationException.java
0 → 100644
| 1 | +package com.xly.exception.dto; | |
| 2 | + | |
| 3 | +import com.xly.constant.ErrorCode; | |
| 4 | + | |
| 5 | +/*** | |
| 6 | + * @Author 钱豹 | |
| 7 | + * @Date 23:13 2026/1/30 | |
| 8 | + * @Param | |
| 9 | + * @return | |
| 10 | + * @Description 认证异常 | |
| 11 | + **/ | |
| 12 | +public class AuthenticationException extends BaseException { | |
| 13 | + public AuthenticationException(ErrorCode errorCode) { | |
| 14 | + super(errorCode); | |
| 15 | + } | |
| 16 | + | |
| 17 | + public AuthenticationException(String message) { | |
| 18 | + super(ErrorCode.UNAUTHORIZED.getCode(), message); | |
| 19 | + } | |
| 20 | +} | |
| 0 | 21 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/exception/dto/AuthorizationException.java
0 → 100644
| 1 | +package com.xly.exception.dto; | |
| 2 | + | |
| 3 | + | |
| 4 | +import com.xly.constant.ErrorCode; | |
| 5 | + | |
| 6 | +/*** | |
| 7 | + * @Author 钱豹 | |
| 8 | + * @Date 23:14 2026/1/30 | |
| 9 | + * @Param | |
| 10 | + * @return | |
| 11 | + * @Description 授权异常 | |
| 12 | + **/ | |
| 13 | +public class AuthorizationException extends BaseException { | |
| 14 | + public AuthorizationException(ErrorCode errorCode) { | |
| 15 | + super(errorCode); | |
| 16 | + } | |
| 17 | + | |
| 18 | + public AuthorizationException(String message) { | |
| 19 | + super(ErrorCode.FORBIDDEN.getCode(), message); | |
| 20 | + } | |
| 21 | +} | |
| 0 | 22 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/exception/dto/BaseException.java
0 → 100644
| 1 | +package com.xly.exception.dto; | |
| 2 | + | |
| 3 | +import com.xly.constant.ErrorCode; | |
| 4 | +import lombok.Data; | |
| 5 | +import lombok.EqualsAndHashCode; | |
| 6 | + | |
| 7 | +/*** | |
| 8 | + * @Author 钱豹 | |
| 9 | + * @Date 23:13 2026/1/30 | |
| 10 | + * @Param | |
| 11 | + * @return | |
| 12 | + * @Description 基础异常定义 | |
| 13 | + **/ | |
| 14 | +@Data | |
| 15 | +@EqualsAndHashCode(callSuper = true) | |
| 16 | +public class BaseException extends RuntimeException { | |
| 17 | + private final Integer code; | |
| 18 | + private final String message; | |
| 19 | + | |
| 20 | + public BaseException(ErrorCode errorCode) { | |
| 21 | + super(errorCode.getMessage()); | |
| 22 | + this.code = errorCode.getCode(); | |
| 23 | + this.message = errorCode.getMessage(); | |
| 24 | + } | |
| 25 | + | |
| 26 | + public BaseException(ErrorCode errorCode, String message) { | |
| 27 | + super(message); | |
| 28 | + this.code = errorCode.getCode(); | |
| 29 | + this.message = message; | |
| 30 | + } | |
| 31 | + | |
| 32 | + public BaseException(Integer code, String message) { | |
| 33 | + super(message); | |
| 34 | + this.code = code; | |
| 35 | + this.message = message; | |
| 36 | + } | |
| 37 | +} | |
| 0 | 38 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/exception/dto/BusinessException.java
0 → 100644
| 1 | +package com.xly.exception.dto; | |
| 2 | + | |
| 3 | +import com.xly.constant.ErrorCode; | |
| 4 | + | |
| 5 | +/*** | |
| 6 | + * @Author 钱豹 | |
| 7 | + * @Date 23:13 2026/1/30 | |
| 8 | + * @Param | |
| 9 | + * @return | |
| 10 | + * @Description 业务异常 | |
| 11 | + **/ | |
| 12 | +public class BusinessException extends BaseException { | |
| 13 | + public BusinessException(ErrorCode errorCode) { | |
| 14 | + super(errorCode); | |
| 15 | + } | |
| 16 | + | |
| 17 | + public BusinessException(ErrorCode errorCode, String message) { | |
| 18 | + super(errorCode, message); | |
| 19 | + } | |
| 20 | + | |
| 21 | + public BusinessException(Integer code, String message) { | |
| 22 | + super(code, message); | |
| 23 | + } | |
| 24 | +} | |
| 25 | + | |
| 26 | + | |
| 27 | + | ... | ... |
src/main/java/com/xly/exception/dto/DataException.java
0 → 100644
| 1 | +package com.xly.exception.dto; | |
| 2 | + | |
| 3 | +import com.xly.constant.ErrorCode; | |
| 4 | + | |
| 5 | +/*** | |
| 6 | + * @Author 钱豹 | |
| 7 | + * @Date 23:13 2026/1/30 | |
| 8 | + * @Param | |
| 9 | + * @return | |
| 10 | + * @Description 数据异常 | |
| 11 | + **/ | |
| 12 | +public class DataException extends BaseException { | |
| 13 | + public DataException(ErrorCode errorCode) { | |
| 14 | + super(errorCode); | |
| 15 | + } | |
| 16 | + | |
| 17 | + public DataException(String message) { | |
| 18 | + super(ErrorCode.DATA_ERROR.getCode(), message); | |
| 19 | + } | |
| 20 | +} | |
| 0 | 21 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/exception/sqlexception/Nl2SqlBaseException.java
0 → 100644
| 1 | +package com.xly.exception.sqlexception; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * NL2SQL根异常 | |
| 5 | + */ | |
| 6 | +public class Nl2SqlBaseException extends RuntimeException { | |
| 7 | + public Nl2SqlBaseException(String message) { | |
| 8 | + super(message); | |
| 9 | + } | |
| 10 | + | |
| 11 | + public Nl2SqlBaseException(String message, Throwable cause) { | |
| 12 | + super(message, cause); | |
| 13 | + } | |
| 14 | +} | ... | ... |
src/main/java/com/xly/exception/sqlexception/SqlExecuteException.java
0 → 100644
| 1 | +package com.xly.exception.sqlexception; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * SQL执行异常(数据库执行/结果解析失败) | |
| 5 | + */ | |
| 6 | +public class SqlExecuteException extends Nl2SqlBaseException { | |
| 7 | + public SqlExecuteException(String message) { | |
| 8 | + super(message); | |
| 9 | + } | |
| 10 | + | |
| 11 | + public SqlExecuteException(String message, Throwable cause) { | |
| 12 | + super(message, cause); | |
| 13 | + } | |
| 14 | +} | |
| 0 | 15 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/exception/sqlexception/SqlGenerateException.java
0 → 100644
| 1 | +package com.xly.exception.sqlexception; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * SQL生成异常(@AiService调用失败/生成空SQL) | |
| 5 | + */ | |
| 6 | +public class SqlGenerateException extends Nl2SqlBaseException { | |
| 7 | + public SqlGenerateException(String message) { | |
| 8 | + super(message); | |
| 9 | + } | |
| 10 | + | |
| 11 | + public SqlGenerateException(String message, Throwable cause) { | |
| 12 | + super(message, cause); | |
| 13 | + } | |
| 14 | +} | |
| 0 | 15 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/exception/sqlexception/SqlValidateException.java
0 → 100644
| 1 | +package com.xly.exception.sqlexception; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * SQL强校验异常(语法错误/危险关键词/非SELECT语句) | |
| 5 | + */ | |
| 6 | +public class SqlValidateException extends Nl2SqlBaseException { | |
| 7 | + public SqlValidateException(String message) { | |
| 8 | + super(message); | |
| 9 | + } | |
| 10 | + | |
| 11 | + public SqlValidateException(String message, Throwable cause) { | |
| 12 | + super(message, cause); | |
| 13 | + } | |
| 14 | +} | |
| 0 | 15 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/mapper/AiToolDetailParamsMapper.java
0 → 100644
| 1 | +package com.xly.mapper; | |
| 2 | + | |
| 3 | +import com.xly.entity.ParamRule; | |
| 4 | +import org.apache.ibatis.annotations.Mapper; | |
| 5 | +import org.apache.ibatis.annotations.Select; | |
| 6 | +import org.springframework.stereotype.Repository; | |
| 7 | + | |
| 8 | +import java.util.List; | |
| 9 | + | |
| 10 | +@Repository | |
| 11 | +@Mapper | |
| 12 | +public interface AiToolDetailParamsMapper { | |
| 13 | + | |
| 14 | + // XML配置方式 | |
| 15 | + @Select("SELECT * FROM ai_tool_detail_params") | |
| 16 | + List<ParamRule> findAll(); | |
| 17 | +} | |
| 0 | 18 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/mapper/DynamicExeDbMapper.java
0 → 100644
| 1 | +package com.xly.mapper; | |
| 2 | + | |
| 3 | + | |
| 4 | +import org.apache.ibatis.annotations.Mapper; | |
| 5 | +import org.springframework.stereotype.Repository; | |
| 6 | + | |
| 7 | +import java.util.List; | |
| 8 | +import java.util.Map; | |
| 9 | + | |
| 10 | + | |
| 11 | +@Repository | |
| 12 | +@Mapper | |
| 13 | +public interface DynamicExeDbMapper { | |
| 14 | + | |
| 15 | + /*** | |
| 16 | + * @Author 钱豹 | |
| 17 | + * @Date 22:43 2026/1/30 | |
| 18 | + * @Param [] | |
| 19 | + * @return java.util.List<java.util.Map<java.lang.String,java.lang.Object>> | |
| 20 | + * @Description 查询SQL执行 | |
| 21 | + **/ | |
| 22 | + List<Map<String,Object>> findSql(Map<String,Object> searMap); | |
| 23 | + | |
| 24 | + /*** | |
| 25 | + * @Author 钱豹 | |
| 26 | + * @Date 22:44 2026/1/30 | |
| 27 | + * @Param [updMap] | |
| 28 | + * @return void | |
| 29 | + * @Description 更新SQL执行 | |
| 30 | + **/ | |
| 31 | + void updateSql(Map<String,Object> updMap); | |
| 32 | + | |
| 33 | + /*** | |
| 34 | + * @Author 钱豹 | |
| 35 | + * @Date 22:45 2026/1/30 | |
| 36 | + * @Param [updMap] | |
| 37 | + * @return void | |
| 38 | + * @Description 新增SQL执行 | |
| 39 | + **/ | |
| 40 | + void addSql(Map<String,Object> addMap); | |
| 41 | + | |
| 42 | + /**** | |
| 43 | + * @Author 钱豹 | |
| 44 | + * @Date 22:56 2026/1/30 | |
| 45 | + * @Param [delMap] | |
| 46 | + * @return void | |
| 47 | + * @Description 删除QL执行 | |
| 48 | + **/ | |
| 49 | + void delSql(Map<String,Object> delMap); | |
| 50 | + | |
| 51 | + | |
| 52 | + /*** | |
| 53 | + * @Author 钱豹 | |
| 54 | + * @Date 22:45 2026/1/30 | |
| 55 | + * @Param | |
| 56 | + * @return | |
| 57 | + * @Description 动态执行过程 并且有返回 执行过程 返回多个数据集 ,默认10个 | |
| 58 | + **/ | |
| 59 | + List<List<Map<String,Object>>> getCallProMoreResult(Map<String,Object> map); | |
| 60 | + | |
| 61 | + /*** | |
| 62 | + * @Author 钱豹 | |
| 63 | + * @Date 1:41 2026/2/4 | |
| 64 | + * @Param [map] | |
| 65 | + * @return java.util.List<java.util.Map<java.lang.String,java.lang.Object>> | |
| 66 | + * @Description 调用过程 | |
| 67 | + **/ | |
| 68 | + List<Map<String,Object>> getCallPro(Map<String,Object> map); | |
| 69 | + | |
| 70 | +} | ... | ... |
src/main/java/com/xly/mapper/ParamRuleMapper.java
0 → 100644
| 1 | +package com.xly.mapper; | |
| 2 | + | |
| 3 | +import com.xly.entity.ParamRule; | |
| 4 | +import org.apache.ibatis.annotations.Mapper; | |
| 5 | +import org.apache.ibatis.annotations.Select; | |
| 6 | +import org.springframework.stereotype.Repository; | |
| 7 | + | |
| 8 | +import java.util.List; | |
| 9 | + | |
| 10 | +@Repository | |
| 11 | +@Mapper | |
| 12 | +public interface ParamRuleMapper { | |
| 13 | + | |
| 14 | + // XML配置方式 | |
| 15 | + @Select("SELECT * FROM ai_tool_detail_params") | |
| 16 | + List<ParamRule> findAll(); | |
| 17 | +} | |
| 0 | 18 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/mapper/SceneMapper.java
0 → 100644
| 1 | +package com.xly.mapper; | |
| 2 | + | |
| 3 | +import com.xly.entity.SceneDto; | |
| 4 | +import org.apache.ibatis.annotations.*; | |
| 5 | +import org.springframework.stereotype.Repository; | |
| 6 | + | |
| 7 | +import java.util.List; | |
| 8 | + | |
| 9 | +@Repository | |
| 10 | +@Mapper | |
| 11 | +public interface SceneMapper { | |
| 12 | + | |
| 13 | + // XML配置方式 | |
| 14 | + @Select("SELECT * FROM ai_agent order by iOrder") | |
| 15 | + List<SceneDto> findAll(); | |
| 16 | +} | |
| 0 | 17 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/mapper/ToolMetaMapper.java
0 → 100644
| 1 | +package com.xly.mapper; | |
| 2 | + | |
| 3 | + | |
| 4 | +import com.xly.entity.ToolMeta; | |
| 5 | +import org.apache.ibatis.annotations.Mapper; | |
| 6 | +import org.apache.ibatis.annotations.Select; | |
| 7 | +import org.springframework.stereotype.Repository; | |
| 8 | + | |
| 9 | +import java.util.List; | |
| 10 | + | |
| 11 | +@Repository | |
| 12 | +@Mapper | |
| 13 | +public interface ToolMetaMapper { | |
| 14 | + | |
| 15 | + // XML配置方式 | |
| 16 | + @Select("SELECT A.* FROM ai_tool AS A order by iOrder") | |
| 17 | + List<ToolMeta> findAll(); | |
| 18 | + | |
| 19 | +} | |
| 0 | 20 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/mapper/UserSceneSessionMapper.java
0 → 100644
| 1 | +package com.xly.mapper; | |
| 2 | + | |
| 3 | +import org.apache.ibatis.annotations.Mapper; | |
| 4 | +import org.apache.ibatis.annotations.Param; | |
| 5 | +import org.apache.ibatis.annotations.Select; | |
| 6 | +import java.util.List; | |
| 7 | +import java.util.Map; | |
| 8 | + | |
| 9 | +@Mapper | |
| 10 | +public interface UserSceneSessionMapper { | |
| 11 | + | |
| 12 | + /** | |
| 13 | + * 查询用户权限Key列表 | |
| 14 | + * @param sUserId 用户ID | |
| 15 | + * @return 权限Key列表 | |
| 16 | + */ | |
| 17 | + @Select("SELECT J.sKey AS sKey FROM sysjurisdiction AS J JOIN sisjurisdictionclassify AS B ON J.sJurisdictionClassifyId = B.sId WHERE B.sId IN(SELECT U.sJurisdictionClassifyId FROM sftlogininfojurisdictiongroup AS U WHERE U.sParentId = #{sUserId}) ") | |
| 18 | + List<Map<String, Object>> findUserPermissions(@Param("sUserId") String sUserId); | |
| 19 | + | |
| 20 | +} | |
| 0 | 21 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/runner/AppStartupRunner.java
0 → 100644
| 1 | +package com.xly.runner; | |
| 2 | + | |
| 3 | +import com.xly.entity.SceneDto; | |
| 4 | +import com.xly.entity.ToolMeta; | |
| 5 | +import com.xly.entity.ParamRule; | |
| 6 | +import com.xly.mapper.SceneMapper; | |
| 7 | +import com.xly.mapper.ParamRuleMapper; | |
| 8 | +import com.xly.mapper.ToolMetaMapper; | |
| 9 | +import lombok.extern.slf4j.Slf4j; | |
| 10 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 11 | +import org.springframework.boot.CommandLineRunner; | |
| 12 | +import org.springframework.core.annotation.Order; | |
| 13 | +import org.springframework.stereotype.Component; | |
| 14 | + | |
| 15 | +import java.util.ArrayList; | |
| 16 | +import java.util.Comparator; | |
| 17 | +import java.util.List; | |
| 18 | +import java.util.stream.Collectors; | |
| 19 | + | |
| 20 | +@Slf4j | |
| 21 | +@Component | |
| 22 | +@Order(100) | |
| 23 | +public class AppStartupRunner implements CommandLineRunner { | |
| 24 | + | |
| 25 | + // 静态缓存列表 | |
| 26 | + private static final List<SceneDto> AI_AGENT_CACHE = new ArrayList<>(); | |
| 27 | + private static final List<ToolMeta> AI_TOOL_CACHE = new ArrayList<>(); | |
| 28 | + private static final List<ParamRule> AI_TOOL_PARAMS_CACHE = new ArrayList<>(); | |
| 29 | + | |
| 30 | + // 使用实例变量进行依赖注入 | |
| 31 | + @Autowired | |
| 32 | + private SceneMapper sceneMapper; | |
| 33 | + | |
| 34 | + @Autowired | |
| 35 | + private ParamRuleMapper paramRuleMapper; | |
| 36 | + | |
| 37 | + @Autowired | |
| 38 | + private ToolMetaMapper toolMetaMapper; | |
| 39 | + | |
| 40 | + public void cleanAllInit(){ | |
| 41 | + AI_AGENT_CACHE.clear(); | |
| 42 | + AI_TOOL_CACHE.clear(); | |
| 43 | + AI_TOOL_PARAMS_CACHE.clear(); | |
| 44 | + //初始化 | |
| 45 | + initCache(); | |
| 46 | + } | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + @Override | |
| 51 | + public void run(String... args) throws Exception { | |
| 52 | + log.info("应用启动完成,开始初始化..."); | |
| 53 | + // 初始化缓存 | |
| 54 | + initCache(); | |
| 55 | + // 打印缓存信息 | |
| 56 | + printCacheInfo(); | |
| 57 | + log.info("应用初始化完成"); | |
| 58 | + } | |
| 59 | + | |
| 60 | + /** | |
| 61 | + * 配置热点数据加载 | |
| 62 | + */ | |
| 63 | + private void initCache() { | |
| 64 | + log.info("开始初始化缓存..."); | |
| 65 | + // 1. 加载AI代理数据 | |
| 66 | + List<SceneDto> sceneDtos = sceneMapper.findAll(); | |
| 67 | + if (sceneDtos != null) { | |
| 68 | + AI_AGENT_CACHE.clear(); | |
| 69 | + AI_AGENT_CACHE.addAll(sceneDtos); | |
| 70 | + log.info("加载AI代理数据: {} 条", sceneDtos.size()); | |
| 71 | + } else { | |
| 72 | + log.warn("AI代理数据为空"); | |
| 73 | + } | |
| 74 | + | |
| 75 | + // 2. 加载AI工具数据 | |
| 76 | + List<ToolMeta> toolMetas = toolMetaMapper.findAll(); | |
| 77 | + if (toolMetas != null) { | |
| 78 | + AI_TOOL_CACHE.clear(); | |
| 79 | + AI_TOOL_CACHE.addAll(toolMetas); | |
| 80 | + log.info("加载AI工具数据: {} 条", toolMetas.size()); | |
| 81 | + } else { | |
| 82 | + log.warn("AI工具数据为空"); | |
| 83 | + } | |
| 84 | + | |
| 85 | + // 3. 加载AI工具参数数据 | |
| 86 | + List<ParamRule> toolParams = paramRuleMapper.findAll(); | |
| 87 | + if (toolParams != null) { | |
| 88 | + AI_TOOL_PARAMS_CACHE.clear(); | |
| 89 | + AI_TOOL_PARAMS_CACHE.addAll(toolParams); | |
| 90 | + log.info("加载AI工具参数数据: {} 条", toolParams.size()); | |
| 91 | + } else { | |
| 92 | + log.warn("AI工具参数数据为空"); | |
| 93 | + } | |
| 94 | + | |
| 95 | + log.info("缓存初始化完成"); | |
| 96 | + } | |
| 97 | + | |
| 98 | + /** | |
| 99 | + * 打印缓存信息 | |
| 100 | + */ | |
| 101 | + private void printCacheInfo() { | |
| 102 | + log.info("=== 缓存统计 ==="); | |
| 103 | + log.info("AI代理缓存: {} 条", AI_AGENT_CACHE.size()); | |
| 104 | + log.info("AI工具缓存: {} 条", AI_TOOL_CACHE.size()); | |
| 105 | + log.info("AI工具参数缓存: {} 条", AI_TOOL_PARAMS_CACHE.size()); | |
| 106 | + // 打印前几条数据示例 | |
| 107 | + if (!AI_AGENT_CACHE.isEmpty()) { | |
| 108 | + log.info("AI代理示例: {}", AI_AGENT_CACHE.get(0)); | |
| 109 | + } | |
| 110 | + if (!AI_TOOL_CACHE.isEmpty()) { | |
| 111 | + log.info("AI工具示例: {}", AI_TOOL_CACHE.get(0)); | |
| 112 | + } | |
| 113 | + } | |
| 114 | + | |
| 115 | + /** | |
| 116 | + * 静态方法获取缓存(线程安全) | |
| 117 | + */ | |
| 118 | + public static List<SceneDto> getAiAgentCache() { | |
| 119 | + return new ArrayList<>(AI_AGENT_CACHE); // 返回副本,保证线程安全 | |
| 120 | + } | |
| 121 | + | |
| 122 | + public static List<ToolMeta> getAiToolCache() { | |
| 123 | + return new ArrayList<>(AI_TOOL_CACHE); | |
| 124 | + } | |
| 125 | + | |
| 126 | + public static List<ParamRule> getAiToolParamsCache() { | |
| 127 | + return new ArrayList<>(AI_TOOL_PARAMS_CACHE); | |
| 128 | + } | |
| 129 | + | |
| 130 | + public static List<ToolMeta> getTools(List<String> sModleData) { | |
| 131 | + List<ToolMeta> rData = AI_TOOL_CACHE.stream() | |
| 132 | + .filter(agent -> sModleData.contains(agent.getSSrcFormId())) | |
| 133 | + .collect(Collectors.toList()); | |
| 134 | + rData.sort(Comparator.comparing(h -> h.getIOrder())); | |
| 135 | + return rData; | |
| 136 | + } | |
| 137 | + | |
| 138 | + public static List<ToolMeta> getAllTools() { | |
| 139 | + return AI_TOOL_CACHE; | |
| 140 | + } | |
| 141 | + | |
| 142 | + /** | |
| 143 | + * 根据ID获取AI代理 | |
| 144 | + */ | |
| 145 | + public static SceneDto getAiAgentById(String sId) { | |
| 146 | + return AI_AGENT_CACHE.stream() | |
| 147 | + .filter(agent -> agent.getSId().equals(sId)) | |
| 148 | + .findFirst() | |
| 149 | + .orElse(null); | |
| 150 | + } | |
| 151 | + /** | |
| 152 | + * 根据编码获取AI代理 | |
| 153 | + */ | |
| 154 | + public static SceneDto getAiAgentByCode(String sCode) { | |
| 155 | + return AI_AGENT_CACHE.stream() | |
| 156 | + .filter(agent -> agent.getSSceneNo().equals(sCode)) | |
| 157 | + .findFirst() | |
| 158 | + .orElse(null); | |
| 159 | + } | |
| 160 | + | |
| 161 | + /** | |
| 162 | + * 刷新缓存 | |
| 163 | + */ | |
| 164 | + public void refreshCache() { | |
| 165 | + initCache(); | |
| 166 | + } | |
| 167 | +} | |
| 0 | 168 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/service/DynamicExeDbService.java
0 → 100644
| 1 | +package com.xly.service; | |
| 2 | + | |
| 3 | +import cn.hutool.core.text.CharSequenceUtil; | |
| 4 | +import cn.hutool.core.util.ObjectUtil; | |
| 5 | +import cn.hutool.core.util.StrUtil; | |
| 6 | +import cn.hutool.json.JSONUtil; | |
| 7 | +import com.alibaba.fastjson.JSONObject; | |
| 8 | +import com.xly.constant.ErrorCode; | |
| 9 | +import com.xly.constant.ProcedureConstant; | |
| 10 | +import com.xly.exception.dto.BusinessException; | |
| 11 | +import com.xly.mapper.DynamicExeDbMapper; | |
| 12 | +import lombok.RequiredArgsConstructor; | |
| 13 | +import lombok.extern.slf4j.Slf4j; | |
| 14 | +import org.springframework.stereotype.Service; | |
| 15 | + | |
| 16 | +import java.util.*; | |
| 17 | +import java.util.stream.Stream; | |
| 18 | + | |
| 19 | +/*** | |
| 20 | + * @Author 钱豹 | |
| 21 | + * @Date 22:39 2026/1/30 | |
| 22 | + * @Param | |
| 23 | + * @return | |
| 24 | + * @Description 动态执行SQL业务类 | |
| 25 | + **/ | |
| 26 | +@Slf4j | |
| 27 | +@Service("dynamicExeDbService") | |
| 28 | +@RequiredArgsConstructor | |
| 29 | +public class DynamicExeDbService { | |
| 30 | + | |
| 31 | + private final DynamicExeDbMapper dynamicExeDbMapper; | |
| 32 | + private static final String PARAMETER_NAME = "PARAMETER_NAME"; | |
| 33 | + private static final String PARAMETER_MODE = "PARAMETER_MODE"; | |
| 34 | + private static final String DATA_TYPE = "DATA_TYPE"; | |
| 35 | + private static final String INT = "int"; | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + /*** | |
| 40 | + * @Author 钱豹 | |
| 41 | + * @Date 22:43 2026/1/30 | |
| 42 | + * @Param [] | |
| 43 | + * @return java.util.List<java.util.Map<java.lang.String,java.lang.Object>> | |
| 44 | + * @Description 查询SQL执行 | |
| 45 | + **/ | |
| 46 | + public List<Map<String,Object>> findSql(Map<String,Object> searMap,String sSql){ | |
| 47 | + Map<String,Object> findMap = new HashMap<>(searMap); | |
| 48 | + findMap.put("sSql",sSql); | |
| 49 | + return dynamicExeDbMapper.findSql(findMap); | |
| 50 | + } | |
| 51 | + | |
| 52 | + /*** | |
| 53 | + * @Author 钱豹 | |
| 54 | + * @Date 22:44 2026/1/30 | |
| 55 | + * @Param [updMap] | |
| 56 | + * @return void | |
| 57 | + * @Description 更新SQL执行 | |
| 58 | + **/ | |
| 59 | + public void updateSql(Map<String,Object> updMap,String sSql){ | |
| 60 | + Map<String,Object> updateMap = new HashMap<>(updMap); | |
| 61 | + updateMap.put("sSql",sSql); | |
| 62 | + dynamicExeDbMapper.updateSql(updateMap); | |
| 63 | + } | |
| 64 | + | |
| 65 | + /*** | |
| 66 | + * @Author 钱豹 | |
| 67 | + * @Date 22:45 2026/1/30 | |
| 68 | + * @Param [updMap] | |
| 69 | + * @return void | |
| 70 | + * @Description 新增SQL执行 | |
| 71 | + **/ | |
| 72 | + public void addSql(Map<String,Object> addMap,String sSql){ | |
| 73 | + Map<String,Object> addSql = new HashMap<>(addMap); | |
| 74 | + addSql.put("sSql",sSql); | |
| 75 | + dynamicExeDbMapper.addSql(addSql); | |
| 76 | + } | |
| 77 | + | |
| 78 | + /**** | |
| 79 | + * @Author 钱豹 | |
| 80 | + * @Date 22:56 2026/1/30 | |
| 81 | + * @Param [delMap] | |
| 82 | + * @return void | |
| 83 | + * @Description 删除QL执行 | |
| 84 | + **/ | |
| 85 | + public void delSql(Map<String,Object> delMap,String sSql){ | |
| 86 | + Map<String,Object> delSql = new HashMap<>(delMap); | |
| 87 | + delSql.put("sSql",sSql); | |
| 88 | + dynamicExeDbMapper.delSql(delSql); | |
| 89 | + } | |
| 90 | + | |
| 91 | + | |
| 92 | + /*** | |
| 93 | + * @Author 钱豹 | |
| 94 | + * @Date 22:57 2026/1/30 | |
| 95 | + * @Param [searMap, sProName] | |
| 96 | + * @return java.util.List<java.util.List<java.util.Map<java.lang.String,java.lang.Object>>> | |
| 97 | + * @Description 动态SQL执行 默认10个结果集返回 | |
| 98 | + **/ | |
| 99 | + public List<List<Map<String,Object>>> getCallProMoreResult(Map<String, Object> searMap) { | |
| 100 | + List<List<Map<String,Object>>> outListData = null; | |
| 101 | + try{ | |
| 102 | + // | |
| 103 | + outListData = this.dynamicExeDbMapper.getCallProMoreResult(searMap); | |
| 104 | + }catch (Exception e){ | |
| 105 | + log.error("执行失败,失败原因:",e); | |
| 106 | + } | |
| 107 | + String sMassage = (ObjectUtil.isEmpty(searMap.get(ProcedureConstant.SRETURN)))? StrUtil.EMPTY:searMap.get(ProcedureConstant.SRETURN).toString(); | |
| 108 | + String sCode = (ObjectUtil.isEmpty(searMap.get(ProcedureConstant.SCODE)))? StrUtil.EMPTY:searMap.get(ProcedureConstant.SCODE).toString(); | |
| 109 | + if(ObjectUtil.isNotEmpty(sCode) | |
| 110 | + && Integer.valueOf(sCode)< 0){ | |
| 111 | + throw new BusinessException(Integer.valueOf(sCode),sMassage); | |
| 112 | + } | |
| 113 | + return outListData; | |
| 114 | + } | |
| 115 | + /*** | |
| 116 | + * @Author 钱豹 | |
| 117 | + * @Date 9:33 2026/1/31 | |
| 118 | + * @Param [searMap, sProName] | |
| 119 | + * @return java.util.Map<java.lang.String,java.lang.Object> | |
| 120 | + * @Description 调用过程 | |
| 121 | + **/ | |
| 122 | + public Map<String,Object> getCallPro(Map<String, Object> searMap, String sProName){ | |
| 123 | + List<Map<String,Object>> outListData = null; | |
| 124 | + long oldRead = System.currentTimeMillis(); | |
| 125 | + try{ | |
| 126 | + outListData = this.dynamicExeDbMapper.getCallPro (searMap); | |
| 127 | + }catch (Exception e){ | |
| 128 | + log.error("调用过程异常",e); | |
| 129 | + this.doException(e,searMap, sProName); | |
| 130 | + } | |
| 131 | + String sMsgText = null; | |
| 132 | + if(ObjectUtil.isNotEmpty(searMap.get(ProcedureConstant.SRETURN))){ | |
| 133 | + sMsgText = searMap.get(ProcedureConstant.SRETURN).toString(); | |
| 134 | + } | |
| 135 | + Map<String,Object> outMapData = new HashMap<>(8); | |
| 136 | + Set<String> outSet = new HashSet<>(); | |
| 137 | + if(ObjectUtil.isNotEmpty(searMap.get(ProcedureConstant.OUTSETSTRING))){ | |
| 138 | + outSet = (Set<String>) searMap.get(ProcedureConstant.OUTSETSTRING); | |
| 139 | + } | |
| 140 | + //返回出参返回的数据 | |
| 141 | + outSet.forEach(out->{ | |
| 142 | + try{ | |
| 143 | + if(ObjectUtil.isNotEmpty(searMap.get(out)) | |
| 144 | + && JSONUtil.isJson(searMap.get(out).toString())){ | |
| 145 | + if(JSONUtil.isJsonObj(searMap.get(out).toString())){ | |
| 146 | + outMapData.put(out, JSONObject.parseObject(searMap.get(out).toString())); | |
| 147 | + }else if(JSONUtil.isJsonArray(searMap.get(out).toString())){ | |
| 148 | + outMapData.put(out, JSONObject.parseArray(searMap.get(out).toString())); | |
| 149 | + } | |
| 150 | + }else{ | |
| 151 | + outMapData.put(out, searMap.get(out)); | |
| 152 | + } | |
| 153 | + }catch (Exception e){ | |
| 154 | + outMapData.put(out, searMap.get(out)); | |
| 155 | + } | |
| 156 | + }); | |
| 157 | + Map<String,Object> retMap = new HashMap<>(4); | |
| 158 | + retMap.put(ProcedureConstant.OUTLIST,ObjectUtil.isNotEmpty(outListData) ? outListData : new ArrayList<>()); | |
| 159 | + retMap.put(ProcedureConstant.OUTMAP,ObjectUtil.isNotEmpty(outMapData) ? outMapData : new HashMap<>(4)); | |
| 160 | + retMap.put(ProcedureConstant.SRETURN,ObjectUtil.isNotEmpty(sMsgText) ? sMsgText : ""); | |
| 161 | + retMap.put(ProcedureConstant.SCODE,searMap.get(ProcedureConstant.SCODE)); | |
| 162 | + return retMap; | |
| 163 | + } | |
| 164 | + /*** | |
| 165 | + * @Author 钱豹 | |
| 166 | + * @Date 9:22 2026/1/31 | |
| 167 | + * @Param [searMap, sProName, msg] | |
| 168 | + * @return java.lang.String | |
| 169 | + * @Description 过程执行错误异常提醒 | |
| 170 | + **/ | |
| 171 | + public String getProExceptionMsg(Map<String, Object> searMap, String sProName , String msg){ | |
| 172 | + StringBuffer sb = new StringBuffer(); | |
| 173 | + try{ | |
| 174 | + //根据过程名称获取过程参数 | |
| 175 | + List<Map<String, Object>> proList = getProcParam(sProName); | |
| 176 | + sb.append("{ CALL ").append(sProName).append("("); | |
| 177 | + Stream.iterate(0, i -> i + 1).limit(proList.size()).forEach(i -> { | |
| 178 | + if(ProcedureConstant.OUT.equalsIgnoreCase(proList.get(i).get(PARAMETER_MODE).toString())){ | |
| 179 | + if(i==0){ | |
| 180 | + sb.append("@").append(proList.get(i).get(PARAMETER_NAME)); | |
| 181 | + }else{ | |
| 182 | + sb.append(",@").append(proList.get(i).get(PARAMETER_NAME)); | |
| 183 | + } | |
| 184 | + }else{ | |
| 185 | + if(ObjectUtil.isEmpty(searMap.get(proList.get(i).get(PARAMETER_NAME)))){ | |
| 186 | + if(i==0){ | |
| 187 | + sb.append("NULL"); | |
| 188 | + }else{ | |
| 189 | + sb.append(",NULL"); | |
| 190 | + } | |
| 191 | + }else{ | |
| 192 | + String sControl = ObjectUtil.isNotEmpty(searMap.get(proList.get(i).get(PARAMETER_NAME)))? | |
| 193 | + searMap.get(proList.get(i).get(PARAMETER_NAME)).toString(): CharSequenceUtil.EMPTY; | |
| 194 | + String sPlit = "'"; | |
| 195 | + if(sControl.contains("'")){ | |
| 196 | + sPlit = "\""; | |
| 197 | + } | |
| 198 | + if(i==0){ | |
| 199 | + sb.append(sPlit).append(sControl).append(sPlit); | |
| 200 | + }else{ | |
| 201 | + sb.append(",").append(sPlit).append(sControl).append(sPlit); | |
| 202 | + } | |
| 203 | + } | |
| 204 | + } | |
| 205 | + }); | |
| 206 | + sb.append(") "); | |
| 207 | + msg = sb.toString(); | |
| 208 | + }catch (Exception e){ | |
| 209 | + e.printStackTrace(); | |
| 210 | + } | |
| 211 | + return msg; | |
| 212 | + } | |
| 213 | + | |
| 214 | + public Map<String, Object> getDoProMap(String sProName, Map<String, Object> params) throws BusinessException{ | |
| 215 | + Map<String,Object> searMap = new HashMap<>(4); | |
| 216 | + try{ | |
| 217 | + //根据过程名称获取过程参数 | |
| 218 | + //添加公司子公司 | |
| 219 | +// if((ObjectUtil.isEmpty(params.get("sBrId")) | |
| 220 | +// || ObjectUtil.isEmpty(params.get("sSuId")))){ | |
| 221 | +// params.put("sBrId",params.get("sBrandsId")); | |
| 222 | +// params.put("sSuId",params.get("sSubsidiaryId")); | |
| 223 | +// } | |
| 224 | +// if(ObjectUtil.isEmpty(params.get("sLoginId"))){ | |
| 225 | +// params.put("sLoginId",params.get("sUserId")); | |
| 226 | +// } | |
| 227 | + List<Map<String, Object>> proList = getProcParam( sProName); | |
| 228 | + //{CALL mytest(#{ownerid,mode=IN,jdbcType=INTEGER},#{examcount,mode=OUT,jdbcType=INTEGER})} | |
| 229 | + StringBuffer sb = new StringBuffer(); | |
| 230 | + sb.append("{CALL ").append(sProName).append("("); | |
| 231 | + Set<String> outSet = new HashSet<>(4); | |
| 232 | + Stream.iterate(0, i -> i + 1).limit(proList.size()).forEach(i -> { | |
| 233 | + if(i==0){ | |
| 234 | + sb.append("#{").append(proList.get(i).get(PARAMETER_NAME)).append(",mode=").append(proList.get(i).get(PARAMETER_MODE)); | |
| 235 | + }else{ | |
| 236 | + sb.append(",#{").append(proList.get(i).get(PARAMETER_NAME)).append(",mode=").append(proList.get(i).get(PARAMETER_MODE)); | |
| 237 | + } | |
| 238 | + if(ProcedureConstant.IN.equalsIgnoreCase(proList.get(i).get(PARAMETER_MODE).toString())){ | |
| 239 | + if(params.get(proList.get(i).get(PARAMETER_NAME)) != null){ | |
| 240 | + if(params.get(proList.get(i).get(PARAMETER_NAME)) instanceof List | |
| 241 | + || params.get(proList.get(i).get(PARAMETER_NAME)) instanceof Map){ | |
| 242 | + searMap.put(proList.get(i).get(PARAMETER_NAME).toString(), | |
| 243 | + JSONUtil.toJsonStr(params.get(proList.get(i).get(PARAMETER_NAME))) | |
| 244 | + ); | |
| 245 | + }else{ | |
| 246 | + searMap.put(proList.get(i).get(PARAMETER_NAME).toString(), | |
| 247 | + params.get(proList.get(i).get(PARAMETER_NAME))); | |
| 248 | + } | |
| 249 | + }else{ | |
| 250 | + searMap.put(proList.get(i).get(PARAMETER_NAME).toString(),null); | |
| 251 | + } | |
| 252 | +// if(proList.get(i).get(PARAMETER_NAME).toString().startsWith("s")){ | |
| 253 | +// sb.append(",jdbcType=").append("LONGVARCHAR"); | |
| 254 | +// } | |
| 255 | + }else{ | |
| 256 | + if(proList.get(i).get(DATA_TYPE).equals(INT)){ | |
| 257 | + sb.append(",jdbcType=").append("INTEGER"); | |
| 258 | + searMap.put(proList.get(i).get(PARAMETER_NAME).toString(),0); | |
| 259 | + }else{ | |
| 260 | + searMap.put(proList.get(i).get(PARAMETER_NAME).toString(),proList.get(i).get(PARAMETER_NAME).toString()); | |
| 261 | + sb.append(",jdbcType=").append("LONGVARCHAR"); | |
| 262 | + } | |
| 263 | + outSet.add(proList.get(i).get(PARAMETER_NAME).toString()); | |
| 264 | + } | |
| 265 | + sb.append("}"); | |
| 266 | + }); | |
| 267 | + sb.append(")} "); | |
| 268 | + //获取过程返回 | |
| 269 | + searMap.put("sSql",sb.toString()); | |
| 270 | + searMap.put("outSet",outSet); | |
| 271 | + }catch (Exception e){ | |
| 272 | + throw new BusinessException(ErrorCode.DB_ERROR,"解析获取过程查询sSql异常。异常原因:"+e.getMessage()); | |
| 273 | + } | |
| 274 | + return searMap; | |
| 275 | + } | |
| 276 | + | |
| 277 | + /*** | |
| 278 | + * @Author 钱豹 | |
| 279 | + * @Date 9:25 2026/1/31 | |
| 280 | + * @Param [proName] | |
| 281 | + * @return java.util.List<java.util.Map<java.lang.String,java.lang.Object>> | |
| 282 | + * @Description 动态获取过程参数 | |
| 283 | + **/ | |
| 284 | + public List<Map<String, Object>> getProcParam(String proName){ | |
| 285 | + StringBuffer paramSql = new StringBuffer(); | |
| 286 | + Map<String, Object> serMap=new HashMap<>(4); | |
| 287 | + paramSql.append(" SELECT PARAMETER_NAME,PARAMETER_MODE,DATA_TYPE,ORDINAL_POSITION from information_schema.PARAMETERS "); | |
| 288 | + paramSql.append(" WHERE SPECIFIC_NAME=#{sProName}"); | |
| 289 | + paramSql.append(" AND SPECIFIC_SCHEMA= DATABASE() ORDER BY ORDINAL_POSITION ASC"); | |
| 290 | + serMap.put("sSql", paramSql.toString()); | |
| 291 | + serMap.put("sProName",proName); | |
| 292 | + List<Map<String,Object>> paramList = dynamicExeDbMapper.findSql(serMap); | |
| 293 | + if(paramList==null || paramList.size()<1){ | |
| 294 | + throw new BusinessException(ErrorCode.DB_ERROR,"数据库中未查询到过程,过程名称:"+proName+",请检查!"); | |
| 295 | + } | |
| 296 | + return paramList; | |
| 297 | + } | |
| 298 | + | |
| 299 | + private void doException(Exception e,Map<String, Object> searMap,String sProName){ | |
| 300 | + String msg = StrUtil.EMPTY; | |
| 301 | + String exMsg = ""; | |
| 302 | + try{ | |
| 303 | + msg = getProExceptionMsg(searMap, sProName,JSONUtil.toJsonStr(searMap)); | |
| 304 | + if(ObjectUtil.isNotEmpty( e.getMessage())){ | |
| 305 | + String[] msA= e.getMessage().split(":"); | |
| 306 | + exMsg = msA[msA.length-1]; | |
| 307 | + } | |
| 308 | + }catch (Exception ex){ | |
| 309 | + | |
| 310 | + } | |
| 311 | + //-10标识过程中返回错误异常,非正常提示异常 | |
| 312 | + throw new BusinessException(ErrorCode.DB_ERROR,"调用过程执行语句报错,SQL:"+msg+" ; 失败原因:"+ exMsg); | |
| 313 | + } | |
| 314 | + | |
| 315 | +} | |
| 0 | 316 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/service/DynamicNl2SqlService.java
0 → 100644
| 1 | +package com.xly.service; | |
| 2 | + | |
| 3 | + | |
| 4 | +import com.alibaba.fastjson2.JSON; | |
| 5 | + | |
| 6 | +import com.xly.agent.DynamicTableNl2SqlAiAgent; | |
| 7 | +import com.xly.entity.DynamicNl2SqlRequest; | |
| 8 | +import com.xly.entity.Nl2SqlResult; | |
| 9 | +import com.xly.util.SqlExecuteUtil; | |
| 10 | +import com.xly.util.SqlValidateUtil; | |
| 11 | +import jakarta.annotation.Resource; | |
| 12 | +import org.slf4j.Logger; | |
| 13 | +import org.slf4j.LoggerFactory; | |
| 14 | +import org.springframework.stereotype.Service; | |
| 15 | + | |
| 16 | +import java.util.List; | |
| 17 | +import java.util.Map; | |
| 18 | + | |
| 19 | +/** | |
| 20 | + * 动态表结构的NL2SQL业务服务层 | |
| 21 | + * 适配任意表结构、多业务场景 | |
| 22 | + */ | |
| 23 | +@Service | |
| 24 | +public class DynamicNl2SqlService { | |
| 25 | + private static final Logger log = LoggerFactory.getLogger(DynamicNl2SqlService.class); | |
| 26 | + | |
| 27 | + @Resource | |
| 28 | + private DynamicTableNl2SqlAiAgent dynamicTableNl2SqlAiAgent; | |
| 29 | + | |
| 30 | + @Resource | |
| 31 | + private SqlExecuteUtil sqlExecuteUtil; | |
| 32 | + | |
| 33 | + /** | |
| 34 | + * 执行动态表结构的NL2SQL全流程 | |
| 35 | + * @param request 动态表结构请求(含db名、表名、表结构、用户问题) | |
| 36 | + * @return 结构化NL2SQL结果 | |
| 37 | + */ | |
| 38 | + public Nl2SqlResult executeDynamicNl2Sql(DynamicNl2SqlRequest request) { | |
| 39 | + log.info("开始执行动态表结构NL2SQL流程,请求:{}", JSON.toJSONString(request)); | |
| 40 | + // 1. 入参非空校验(重点校验表结构和用户问题) | |
| 41 | + if (request.getTableStruct() == null || request.getTableStruct().trim().isEmpty() | |
| 42 | + || request.getQuestion() == null || request.getQuestion().trim().isEmpty()) { | |
| 43 | + throw new IllegalArgumentException("表结构和用户查询问题不能为空"); | |
| 44 | + } | |
| 45 | + // 补全默认值(如数据库名默认取配置中的值) | |
| 46 | + | |
| 47 | + String tableNames = request.getTableNames() == null ? "" : request.getTableNames().trim(); | |
| 48 | + String rawSql = "select * from " + tableNames + " where 1=1 "; | |
| 49 | + // 2. 调用AI服务生成SQL(传入所有动态参数) | |
| 50 | +// String rawSql = dynamicTableNl2SqlAiAgent.generateMysqlSql( | |
| 51 | +// tableNames, | |
| 52 | +// request.getTableStruct().trim(), | |
| 53 | +// request.getQuestion().trim() | |
| 54 | +// ); | |
| 55 | +// if (rawSql == null || rawSql.trim().isEmpty()) { | |
| 56 | +// throw new SqlGenerateException("AI服务生成SQL失败,返回结果为空"); | |
| 57 | +// } | |
| 58 | +// log.info("AI服务生成原始SQL:{}", rawSql); | |
| 59 | + | |
| 60 | + // 3. 清理SQL多余符号 + 生产级强校验(核心安全保障,不可省略) | |
| 61 | + String cleanSql = SqlValidateUtil.cleanSqlSymbol(rawSql); | |
| 62 | + SqlValidateUtil.validateMysqlSql(cleanSql); | |
| 63 | + log.info("SQL清理并强校验通过,可执行SQL:{}", cleanSql); | |
| 64 | + | |
| 65 | + // 4. 执行SQL获取结构化结果 | |
| 66 | + List<Map<String, Object>> sqlResult = sqlExecuteUtil.executeSelectSql(cleanSql); | |
| 67 | + log.info("MySQL SQL执行完成,返回结果条数:{}", sqlResult.size()); | |
| 68 | + String resultExplain =""; | |
| 69 | + // 5. 调用AI服务生成自然语言解释(传入表结构,让解释更贴合业务) | |
| 70 | + String resultJson = JSON.toJSONString(sqlResult); | |
| 71 | +// String resultExplain = dynamicTableNl2SqlAiAgent.explainSqlResult( | |
| 72 | +// request.getQuestion().trim(), | |
| 73 | +// cleanSql, | |
| 74 | +// request.getTableStruct().trim(), | |
| 75 | +// resultJson | |
| 76 | +// ); | |
| 77 | + | |
| 78 | + // 6. 封装结果返回 | |
| 79 | + Nl2SqlResult nl2SqlResult = new Nl2SqlResult(); | |
| 80 | + nl2SqlResult.setGenerateSql(cleanSql); | |
| 81 | + nl2SqlResult.setSqlResult(sqlResult); | |
| 82 | + nl2SqlResult.setResultExplain(resultExplain); | |
| 83 | + | |
| 84 | + log.info("动态表结构NL2SQL流程执行完成"); | |
| 85 | + return nl2SqlResult; | |
| 86 | + } | |
| 87 | +} | |
| 0 | 88 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/service/ToolMetaService.java
0 → 100644
| 1 | +package com.xly.service; | |
| 2 | + | |
| 3 | +import com.xly.entity.ToolMeta; | |
| 4 | +import com.xly.mapper.ToolMetaMapper; | |
| 5 | +import lombok.RequiredArgsConstructor; | |
| 6 | +import org.springframework.stereotype.Service; | |
| 7 | + | |
| 8 | +import java.util.List; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * 动态Tool Service实现 | |
| 12 | + */ | |
| 13 | +@Service("aiToolService") | |
| 14 | +@RequiredArgsConstructor | |
| 15 | +public class ToolMetaService { | |
| 16 | + | |
| 17 | + private final ToolMetaMapper toolMetaMapper; | |
| 18 | + | |
| 19 | + public List<ToolMeta> listAllEnable() { | |
| 20 | + return toolMetaMapper.findAll(); | |
| 21 | + } | |
| 22 | + | |
| 23 | +} | |
| 0 | 24 | \ No newline at end of file | ... | ... |
src/main/java/com/xly/service/UserSceneSessionService.java
0 → 100644
| 1 | +package com.xly.service; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.ObjectUtil; | |
| 4 | +import com.xly.agent.ChatiAgent; | |
| 5 | +import com.xly.agent.DynamicTableNl2SqlAiAgent; | |
| 6 | +import com.xly.agent.ErpAiAgent; | |
| 7 | +import com.xly.config.OperableChatMemoryProvider; | |
| 8 | +import com.xly.entity.SceneDto; | |
| 9 | +import com.xly.entity.ToolMeta; | |
| 10 | +import com.xly.entity.UserSceneSession; | |
| 11 | +import com.xly.mapper.UserSceneSessionMapper; | |
| 12 | +import com.xly.runner.AppStartupRunner; | |
| 13 | +import dev.langchain4j.memory.chat.ChatMemoryProvider; | |
| 14 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 15 | +import org.springframework.stereotype.Service; | |
| 16 | + | |
| 17 | +import java.util.*; | |
| 18 | +import java.util.stream.Collectors; | |
| 19 | + | |
| 20 | +@Service("userSceneSessionService") | |
| 21 | +public class UserSceneSessionService { | |
| 22 | + | |
| 23 | + // 新增核心缓存:用户场景会话状态(记录是否选场景、当前场景、权限内场景) | |
| 24 | + public static final Map<String, UserSceneSession> USER_SCENE_SESSION_CACHE = new HashMap<>(); | |
| 25 | + | |
| 26 | + // 原有缓存:Agent实例、会话记忆 | |
| 27 | + public static final Map<String, ErpAiAgent> ERP_AGENT_CACHE = new HashMap<>(); | |
| 28 | + public static final Map<String, ChatiAgent> CHAT_AGENT_CACHE = new HashMap<>(); | |
| 29 | + public static final Map<String, DynamicTableNl2SqlAiAgent> ERP_DynamicTableNl2SqlAiAgent_CACHE = new HashMap<>(); | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + @Autowired | |
| 38 | + private UserSceneSessionMapper modelMapper; | |
| 39 | + | |
| 40 | + public List<Map<String, Object>> getModelAllByUserId(String sUserId){ | |
| 41 | + return modelMapper.findUserPermissions(sUserId); | |
| 42 | + } | |
| 43 | + | |
| 44 | + public List<ToolMeta> getModle(String sUserId, String sUserType){ | |
| 45 | + List<Map<String, Object>> modleList = getModelAllByUserId(sUserId); | |
| 46 | + Set<String> dataSet = new HashSet<>(); | |
| 47 | + if(ObjectUtil.isNotEmpty(modleList)){ | |
| 48 | + modleList.forEach(one->{ | |
| 49 | + String sKeys = one.get("sKey").toString(); | |
| 50 | + String[] sIds = sKeys.split("-"); | |
| 51 | + for(String sId:sIds){ | |
| 52 | + if(ObjectUtil.isNotEmpty(sId)){ | |
| 53 | + dataSet.add(sId); | |
| 54 | + } | |
| 55 | + } | |
| 56 | + }); | |
| 57 | + } | |
| 58 | + List<String> data = new ArrayList<>(dataSet); | |
| 59 | + if("sysadmin".equals(sUserType)){ | |
| 60 | + return AppStartupRunner.getAllTools(); | |
| 61 | + }else { | |
| 62 | + return AppStartupRunner.getTools(data); | |
| 63 | + } | |
| 64 | + } | |
| 65 | + /*** | |
| 66 | + * @Author 钱豹 | |
| 67 | + * @Date 14:03 2026/2/10 | |
| 68 | + * @Param [] | |
| 69 | + * @return void | |
| 70 | + * @Description 清除所有记忆 | |
| 71 | + **/ | |
| 72 | + public void cleanAllSession(){ | |
| 73 | + USER_SCENE_SESSION_CACHE.clear(); | |
| 74 | + ERP_AGENT_CACHE.clear(); | |
| 75 | + CHAT_AGENT_CACHE.clear(); | |
| 76 | + ERP_DynamicTableNl2SqlAiAgent_CACHE.clear(); | |
| 77 | + } | |
| 78 | + public UserSceneSession getUserSceneSession(String sUserId, String sUserType,String authorization){ | |
| 79 | + if (USER_SCENE_SESSION_CACHE.containsKey(sUserId)) { | |
| 80 | + return USER_SCENE_SESSION_CACHE.get(sUserId); | |
| 81 | + } | |
| 82 | + UserSceneSession userSceneSession = new UserSceneSession(); | |
| 83 | + List<ToolMeta> tools = getModle(sUserId,sUserType); | |
| 84 | + List<SceneDto> sceneDtos = getAiAgent(tools); | |
| 85 | + userSceneSession.setSceneSelected(false); // 初始状态:未选择场景 | |
| 86 | + userSceneSession.setAuthorization(authorization);//存入用户token | |
| 87 | + userSceneSession.setUserId(sUserId);//存入用户ID | |
| 88 | + userSceneSession.setAuthTool(tools);//方法 | |
| 89 | + userSceneSession.setAuthScenes(sceneDtos);//场景 | |
| 90 | + // 3. 缓存会话 | |
| 91 | + USER_SCENE_SESSION_CACHE.put(sUserId, userSceneSession); | |
| 92 | + return userSceneSession; | |
| 93 | + } | |
| 94 | + | |
| 95 | + public List<SceneDto> getAiAgent(List<ToolMeta> tools){ | |
| 96 | + List<String> aiAgentIds = new ArrayList<>(); | |
| 97 | + tools.forEach(tool->{ | |
| 98 | + aiAgentIds.add(tool.getSSceneId()); | |
| 99 | + }); | |
| 100 | + List<SceneDto> agAll = AppStartupRunner.getAiAgentCache(); | |
| 101 | + return agAll.stream().filter(one-> aiAgentIds.contains(one.getSId())).collect(Collectors.toList()); | |
| 102 | + } | |
| 103 | + | |
| 104 | +} | ... | ... |
src/main/java/com/xly/service/XlyErpService.java
0 → 100644
| 1 | +package com.xly.service; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.ObjectUtil; | |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 5 | +import com.alibaba.fastjson2.JSON; | |
| 6 | +import com.xly.agent.ChatiAgent; | |
| 7 | +import com.xly.agent.DynamicTableNl2SqlAiAgent; | |
| 8 | +import com.xly.agent.ErpAiAgent; | |
| 9 | +import com.xly.agent.SceneSelectorAiAgent; | |
| 10 | +import com.xly.config.OperableChatMemoryProvider; | |
| 11 | +import com.xly.constant.CommonConstant; | |
| 12 | +import com.xly.constant.ReturnTypeCode; | |
| 13 | +import com.xly.entity.*; | |
| 14 | +import com.xly.exception.sqlexception.SqlGenerateException; | |
| 15 | +import com.xly.mapper.ToolMetaMapper; | |
| 16 | +import com.xly.runner.AppStartupRunner; | |
| 17 | +import com.xly.tool.DynamicToolProvider; | |
| 18 | +import com.xly.util.InputPreprocessor; | |
| 19 | +import com.xly.util.SqlValidateUtil; | |
| 20 | +import com.xly.util.ValiDataUtil; | |
| 21 | +import dev.langchain4j.agent.tool.ToolExecutionRequest; | |
| 22 | +import dev.langchain4j.data.message.AiMessage; | |
| 23 | +import dev.langchain4j.model.chat.ChatLanguageModel; | |
| 24 | +import dev.langchain4j.model.ollama.OllamaChatModel; | |
| 25 | +import dev.langchain4j.service.AiServices; | |
| 26 | +import lombok.RequiredArgsConstructor; | |
| 27 | +import lombok.extern.slf4j.Slf4j; | |
| 28 | +import org.python.antlr.ast.Str; | |
| 29 | +import org.springframework.stereotype.Service; | |
| 30 | + | |
| 31 | +import java.math.BigDecimal; | |
| 32 | +import java.time.LocalDate; | |
| 33 | +import java.util.*; | |
| 34 | + | |
| 35 | +@Service | |
| 36 | +@RequiredArgsConstructor | |
| 37 | +@Slf4j | |
| 38 | +public class XlyErpService { | |
| 39 | + //中文对话模型 | |
| 40 | + private final OllamaChatModel chatModel; | |
| 41 | + private final ChatLanguageModel chatiModel; | |
| 42 | + private final ChatLanguageModel sqlChatModel; | |
| 43 | + private final SceneSelectorAiAgent sceneSelectorAiAgent; | |
| 44 | + private final UserSceneSessionService userSceneSessionService; | |
| 45 | + private final DynamicToolProvider dynamicToolProvider; | |
| 46 | + private final OperableChatMemoryProvider operableChatMemoryProvider; | |
| 47 | + private final DynamicExeDbService dynamicExeDbService; | |
| 48 | + private final ToolMetaMapper toolMetaMapper; | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + /*** | |
| 53 | + * @Author 钱豹 | |
| 54 | + * @Date 19:18 2026/1/27 | |
| 55 | + * @Param [userInput, userId, sUserType] | |
| 56 | + * @return java.lang.String | |
| 57 | + * @Description 问答 | |
| 58 | + **/ | |
| 59 | + public AiResponseDTO erpUserInput(String userInput, String userId , String sUserType, String authorization) { | |
| 60 | + long startTime = System.currentTimeMillis(); | |
| 61 | + try { | |
| 62 | + // 0. 预处理用户输入:去空格、转小写(方便匹配) | |
| 63 | + String input= InputPreprocessor.preprocessWithCommons(userInput); | |
| 64 | + // 1. 初始化用户场景会话(权限内场景) | |
| 65 | + UserSceneSession session = userSceneSessionService.getUserSceneSession(userId, sUserType,authorization); | |
| 66 | + session.setAuthorization(authorization); | |
| 67 | + session.setSFunPrompts(null); | |
| 68 | + // 2. 特殊指令:重置场景(无论是否已选,都可重置) | |
| 69 | + if (input.contains("重置") || input.contains("重新选择")) { | |
| 70 | + //清除记忆缓存 | |
| 71 | + operableChatMemoryProvider.clearSpecifiedMemory(userId); | |
| 72 | + return AiResponseDTO.builder().aiText(resetUserScene(userId,session)).build(); | |
| 73 | + } | |
| 74 | + //聊天只能体 | |
| 75 | + if (session.getCurrentScene() != null | |
| 76 | + && Objects.equals(session.getCurrentScene().getSSceneNo(), "ChatZone")) | |
| 77 | + { | |
| 78 | + return getChatiAgent(input, session); | |
| 79 | + } | |
| 80 | + | |
| 81 | + // 3. 未选场景:先展示场景选择界面,处理用户序号选择 | |
| 82 | + if (!session.isSceneSelected() && ValiDataUtil.me().isPureNumber(input)){ | |
| 83 | + // 3.1 尝试处理场景选择(输入序号则匹配,否则展示选择提示) | |
| 84 | + return handleSceneSelect(userId, input, session); | |
| 85 | + } | |
| 86 | + // 4. 构建Agent,执行业务交互,如果返回为null,说明大模型没有判段出场景,必判断出后才能继续 | |
| 87 | + ErpAiAgent aiAgent = createErpAiAgent(userId, input, session); | |
| 88 | + // 没有选择到场景,进闲聊模式 | |
| 89 | + if (aiAgent == null){ | |
| 90 | + return getChatiAgent (input, session); | |
| 91 | + } | |
| 92 | + String sResponMessage = aiAgent.chat(userId, input); | |
| 93 | +// 调用方法,参数缺失部分提示,就直接使用方法返回的 | |
| 94 | + if(session.getCurrentTool() != null | |
| 95 | + && session.getSFunPrompts()!=null | |
| 96 | + ){ // 缺失的参数明细 | |
| 97 | + sResponMessage = session.getSFunPrompts(); | |
| 98 | + } | |
| 99 | +// if (session.getCurrentTool()== null){ | |
| 100 | +// sResponMessage = StrUtil.EMPTY; | |
| 101 | +// } | |
| 102 | + //5.执行工具方法后,清除记忆 | |
| 103 | + if(session.getBCleanMemory()){ | |
| 104 | + operableChatMemoryProvider.clearSpecifiedMemory(userId); | |
| 105 | + session.setBCleanMemory(false); | |
| 106 | + } | |
| 107 | +// 6.找到方法并且本方法带表结构描述时,需要调用 自然语言转SQL智能体 | |
| 108 | + if((ObjectUtil.isNotEmpty(session.getCurrentTool()) | |
| 109 | + && ObjectUtil.isNotEmpty(session.getCurrentTool().getSInputTabelName()) | |
| 110 | + && ObjectUtil.isNotEmpty(session.getCurrentTool().getSStructureMemo())) | |
| 111 | + ){ | |
| 112 | + sResponMessage = getDynamicTableSql(session, input, userId, userInput); | |
| 113 | + } | |
| 114 | + //如果返回空的进入闲聊模式 | |
| 115 | + if (ObjectUtil.isEmpty(sResponMessage)){ | |
| 116 | + return getChatiAgent (input, session); | |
| 117 | + } | |
| 118 | + if (session.getCurrentScene()!= null ){ | |
| 119 | + return AiResponseDTO.builder().aiText(sResponMessage) | |
| 120 | + .sSceneName(session.getCurrentScene().getSSceneName()) | |
| 121 | + .sMethodName((ObjectUtil.isEmpty(session.getCurrentTool()))?StrUtil.EMPTY:session.getCurrentTool().getSMethodName()) | |
| 122 | + .sReturnType(ReturnTypeCode.MAKEDOWN.getCode()) | |
| 123 | + .build(); | |
| 124 | + }else { | |
| 125 | + return AiResponseDTO.builder().aiText("当前场景:没有选择 退回当前场景 请输入 "+ CommonConstant.RESET + sResponMessage).sReturnType(ReturnTypeCode.HTML.getCode()).build(); | |
| 126 | + } | |
| 127 | + } catch (Exception e) { | |
| 128 | + return AiResponseDTO.builder().aiText("系统异常:" + e.getMessage() + ",请稍后重试!").sReturnType(ReturnTypeCode.HTML.getCode()).build(); | |
| 129 | + } | |
| 130 | + } | |
| 131 | + | |
| 132 | + | |
| 133 | + /*** | |
| 134 | + * @Author 钱豹 | |
| 135 | + * @Date 18:38 2026/2/5 | |
| 136 | + * @Param [session, input, userId, userInput] | |
| 137 | + * @return java.lang.String | |
| 138 | + * @Description 获取执行动态SQL | |
| 139 | + **/ | |
| 140 | + private String getDynamicTableSql(UserSceneSession session,String input,String userId,String userInput){ | |
| 141 | + String resultExplain = StrUtil.EMPTY; | |
| 142 | + try{ | |
| 143 | + // 1. 构建自然语言转SQLAgent, | |
| 144 | + DynamicTableNl2SqlAiAgent aiDynamicTableNl2SqlAiAgent = createDynamicTableNl2SqlAiAgent(userId, input, session); | |
| 145 | + String tableNames = session.getCurrentTool().getSInputTabelName(); | |
| 146 | + // "订单表:viw_salsalesorder,客户信息表:elecustomer,结算方式表:sispayment,产品表(无单价,无金额,无数量):viw_product_sort,销售人员表:viw_sissalesman_depart"; | |
| 147 | + String tableStruct = session.getCurrentTool().getSStructureMemo(); | |
| 148 | + String rawSql = aiDynamicTableNl2SqlAiAgent.generateMysqlSql(userId,tableNames,tableStruct,userInput); | |
| 149 | + if (rawSql == null || rawSql.trim().isEmpty()) { | |
| 150 | + throw new SqlGenerateException("AI服务生成SQL失败,返回结果为空"); | |
| 151 | + } | |
| 152 | + // 2. 清理SQL多余符号 + 生产级强校验(核心安全保障,不可省略) | |
| 153 | + String cleanSql = SqlValidateUtil.cleanSqlSymbol(rawSql); | |
| 154 | + SqlValidateUtil.validateMysqlSql(cleanSql); | |
| 155 | + log.info("SQL清理并强校验通过,可执行SQL:{}", cleanSql); | |
| 156 | + // 4. 执行SQL获取结构化结果 | |
| 157 | +// Map<String,Object> params = new HashMap<>(); | |
| 158 | + List<Map<String, Object>> sqlResult = dynamicExeDbService.findSql(new HashMap<>(),cleanSql); | |
| 159 | + // 5. 调用AI服务生成自然语言解释(传入表结构,让解释更贴合业务) | |
| 160 | + String resultJson = JSON.toJSONString(sqlResult); | |
| 161 | + resultExplain = aiDynamicTableNl2SqlAiAgent.explainSqlResult( | |
| 162 | + userId, | |
| 163 | + userInput, | |
| 164 | + cleanSql, | |
| 165 | + tableStruct, | |
| 166 | + resultJson | |
| 167 | + ); | |
| 168 | + }catch (Exception e){ | |
| 169 | + session.setCurrentTool(null); | |
| 170 | + resultExplain = "动态SQL执行错误,请提供更具体的问题或指令"; | |
| 171 | + } | |
| 172 | + log.info("动态表结构NL2SQL流程执行完成"); | |
| 173 | + return resultExplain; | |
| 174 | + } | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + /*** | |
| 180 | + * @Author 钱豹 | |
| 181 | + * @Date 11:22 2026/1/31 | |
| 182 | + * @Param | |
| 183 | + * @return | |
| 184 | + * @Description 动态参数补齐处理 | |
| 185 | + **/ | |
| 186 | + private String dotoolExecutionRequests(AiMessage aiMessage){ | |
| 187 | + String textTs = aiMessage.text(); | |
| 188 | + if(aiMessage.hasToolExecutionRequests()){ | |
| 189 | + List<ToolExecutionRequest> toolExecutionRequests = aiMessage.toolExecutionRequests(); | |
| 190 | + toolExecutionRequests.forEach(toolRequests->{ | |
| 191 | + String arguments = toolRequests.arguments(); | |
| 192 | + log.info(arguments); | |
| 193 | + }); | |
| 194 | + } | |
| 195 | + return textTs; | |
| 196 | + } | |
| 197 | + | |
| 198 | + /*** | |
| 199 | + * 存入全部场景 | |
| 200 | + * @Author 钱豹 | |
| 201 | + * @Date 19:06 2026/1/26 | |
| 202 | + * @Param [sUserId, sUserType] | |
| 203 | + * @return java.lang.String | |
| 204 | + * 页面刷新/首次进入时调用:初始化用户场景会话,直接返回场景选择引导词 | |
| 205 | + * 前端页面加载完成后,无需用户输入,直接调用该方法即可显示引导词 | |
| 206 | + * @param sUserId 用户ID(前端传入,如user-001) sUserType 角色状态 | |
| 207 | + * @return 场景选择引导词(即原buildSceneSelectHint生成的文案) | |
| 208 | + */ | |
| 209 | + public AiResponseDTO initSceneGuide(String systemText,String sUserId,String sUserType,String authorization) { | |
| 210 | + try { | |
| 211 | + UserSceneSession userSceneSession = userSceneSessionService.getUserSceneSession( sUserId, sUserType,authorization); | |
| 212 | + systemText = userSceneSession.buildSceneSelectHint(); | |
| 213 | + } catch (Exception e) { | |
| 214 | + systemText = "<p style='color:red;'>抱歉,你暂无任何业务场景的访问权限,请联系管理员开通!</p>"; | |
| 215 | + } | |
| 216 | + return AiResponseDTO.builder().aiText(StrUtil.EMPTY).systemText(systemText) .build(); | |
| 217 | + } | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + // ====================== 动态构建Agent(支持选定场景/未选场景) ====================== | |
| 223 | + private DynamicTableNl2SqlAiAgent createDynamicTableNl2SqlAiAgent(String userId, String userInput, UserSceneSession session) { | |
| 224 | +// 4. 获取/创建用DynamicTableNl2SqlAiAgent | |
| 225 | + DynamicTableNl2SqlAiAgent aiAgent = UserSceneSessionService.ERP_DynamicTableNl2SqlAiAgent_CACHE.get(userId); | |
| 226 | + if(ObjectUtil.isEmpty(aiAgent)){ | |
| 227 | + aiAgent = AiServices.builder(DynamicTableNl2SqlAiAgent.class) | |
| 228 | + .chatLanguageModel(sqlChatModel) | |
| 229 | + .chatMemoryProvider(operableChatMemoryProvider) | |
| 230 | + .toolProvider(dynamicToolProvider) | |
| 231 | + .build(); | |
| 232 | + UserSceneSessionService.ERP_DynamicTableNl2SqlAiAgent_CACHE.put(userId, aiAgent); | |
| 233 | + } | |
| 234 | + return aiAgent; | |
| 235 | + } | |
| 236 | + | |
| 237 | + // ====================== 动态构建Agent(支持选定场景/未选场景) ====================== | |
| 238 | + private ErpAiAgent createErpAiAgent(String userId, String userInput, UserSceneSession session) { | |
| 239 | + | |
| 240 | + // 1. 已选场景:强制绑定该场景工具 | |
| 241 | + if (session.isSceneSelected() && session.getCurrentScene() != null) { | |
| 242 | + dynamicToolProvider.sSceneIdMap.put(userId,session.getCurrentScene().getSId()); | |
| 243 | + } else { | |
| 244 | + // 2. 未选场景:大模型根据输入返加相应的场景 | |
| 245 | + SceneDto sceneDto = parseSceneByLlm(userId, userInput, session); | |
| 246 | + if (sceneDto != null) { | |
| 247 | + session.setCurrentScene(sceneDto); | |
| 248 | + session.setSceneSelected(true); | |
| 249 | + UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); | |
| 250 | + dynamicToolProvider.sSceneIdMap.put(userId,session.getCurrentScene().getSId()); | |
| 251 | + }else {return null;} | |
| 252 | + } | |
| 253 | + // 4. 获取/创建用Agent | |
| 254 | + ErpAiAgent aiAgent = UserSceneSessionService.ERP_AGENT_CACHE.get(userId); | |
| 255 | + if(ObjectUtil.isEmpty(aiAgent)){ | |
| 256 | + aiAgent = AiServices.builder(ErpAiAgent.class) | |
| 257 | + .chatLanguageModel(chatModel) | |
| 258 | + .chatMemoryProvider(operableChatMemoryProvider) | |
| 259 | + .toolProvider(dynamicToolProvider) | |
| 260 | + .build(); | |
| 261 | + UserSceneSessionService.ERP_AGENT_CACHE.put(userId, aiAgent); | |
| 262 | + log.info("用户{}Agent构建完成,已选场景:{},场景ID{}", | |
| 263 | + userId, session.isSceneSelected() ? session.getCurrentScene().getSSceneName() : "未选(全场景匹配)", dynamicToolProvider.sSceneIdMap.get(userId)); | |
| 264 | + } | |
| 265 | + return aiAgent; | |
| 266 | + } | |
| 267 | + | |
| 268 | + | |
| 269 | + /** | |
| 270 | + * 大模型意图解析核心方法(获取场景) | |
| 271 | + * @param userId 用户ID | |
| 272 | + * @param userInput 用户输入 | |
| 273 | + * @param session 用户会话 | |
| 274 | + * @return 匹配的BusinessScene,null表示解析失败 | |
| 275 | + */ | |
| 276 | + private SceneDto parseSceneByLlm(String userId, String userInput, UserSceneSession session) { | |
| 277 | + try { | |
| 278 | + List<ToolMeta> metasAll = session.getAuthTool(); | |
| 279 | + // toolMetaMapper.findAll(); | |
| 280 | + // 1. 构建大模型意图解析请求 | |
| 281 | + String authScenesDesc =session.buildAuthScenesForLlm(metasAll); | |
| 282 | + // 2. 调用大模型解析意图,LangChain4j自动将大模型输出映射为SceneIntentParseResp | |
| 283 | +// {{authScenesDesc}} | |
| 284 | + SceneIntentParseResp parseResp = sceneSelectorAiAgent.parseSceneIntent(userInput,authScenesDesc); | |
| 285 | +// authScenesDesc | |
| 286 | + // 3. 解析结果处理 | |
| 287 | + if (parseResp == null || parseResp.getSceneCode() == null || "NO_MATCH".equals(parseResp.getSceneCode())) { | |
| 288 | + log.warn("用户{}大模型未匹配到任何场景,输入:{}", userId, userInput); | |
| 289 | + return null; | |
| 290 | + } | |
| 291 | + // 4. 将场景编码转换为BusinessScene枚举 | |
| 292 | + String sSceneNo = parseResp.getSceneCode(); | |
| 293 | + return AppStartupRunner.getAiAgentByCode(sSceneNo); | |
| 294 | + } catch (Exception e) { | |
| 295 | + log.error("用户{}大模型意图解析失败,输入:{}", userId, userInput, e); | |
| 296 | + return null; | |
| 297 | + } | |
| 298 | + } | |
| 299 | + | |
| 300 | + /*** | |
| 301 | + * @Author 钱豹 | |
| 302 | + * @Date 19:28 2026/1/26 | |
| 303 | + * @Param [userId, session] | |
| 304 | + * @return java.lang.String | |
| 305 | + * @Description 重置用户场景选择:恢复为未选状态,清空当前场景,重新展示选择界面 | |
| 306 | + **/ | |
| 307 | + private String resetUserScene(String userId, UserSceneSession session) { | |
| 308 | + session.setSceneSelected(false); | |
| 309 | + session.setBCleanMemory(false); | |
| 310 | + session.setCurrentTool(null); | |
| 311 | + session.setCurrentScene(null); | |
| 312 | + UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); | |
| 313 | + // 清空Agent缓存 | |
| 314 | + UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); | |
| 315 | + UserSceneSessionService.CHAT_AGENT_CACHE.remove(userId); | |
| 316 | + return "场景选择已重置!请重新选择业务场景:\n" + session.buildSceneSelectHint(); | |
| 317 | + } | |
| 318 | + | |
| 319 | + /** | |
| 320 | + * 处理用户场景选择:输入序号→匹配场景→更新会话状态 | |
| 321 | + */ | |
| 322 | + private AiResponseDTO handleSceneSelect(String userId, String userInput, UserSceneSession session) { | |
| 323 | + // 1. 尝试根据序号匹配场景 | |
| 324 | + boolean selectSuccess = session.selectSceneByInput(userInput); | |
| 325 | + if (selectSuccess) { | |
| 326 | + // 2. 选择成功:更新缓存,返回成功提示 | |
| 327 | + UserSceneSessionService.USER_SCENE_SESSION_CACHE.put(userId, session); | |
| 328 | + // 清空该用户原有Agent缓存(重新构建绑定新场景的Agent) | |
| 329 | + UserSceneSessionService.ERP_AGENT_CACHE.remove(userId); | |
| 330 | + //清除记忆缓存 | |
| 331 | + operableChatMemoryProvider.clearSpecifiedMemory(userId); | |
| 332 | + String aiText = "智能体选择成功! 现在可以问她相关问题(如" + String.join("、", session.getCurrentScene().getSSceneContext()) + ")"; | |
| 333 | + return AiResponseDTO.builder().aiText(aiText).sSceneName(session.getCurrentScene().getSSceneName()).build(); | |
| 334 | + } else { | |
| 335 | + // 3. 选择失败:重新展示场景选择提示 | |
| 336 | + return AiResponseDTO.builder().aiText(session.buildSceneSelectHint()).build(); | |
| 337 | + } | |
| 338 | + } | |
| 339 | + | |
| 340 | + /*** | |
| 341 | + * @Author 钱豹 | |
| 342 | + * @Date 13:32 2026/2/6 | |
| 343 | + * @Param [input, session] | |
| 344 | + * @return java.lang.String | |
| 345 | + * @Description 获取智普通智能体 | |
| 346 | + **/ | |
| 347 | + private AiResponseDTO getChatiAgent (String input,UserSceneSession session){ | |
| 348 | + ChatiAgent chatiAgent = UserSceneSessionService.CHAT_AGENT_CACHE.get(session.getUserId()); | |
| 349 | + if(ObjectUtil.isEmpty(chatiAgent)){ | |
| 350 | + chatiAgent = AiServices.builder(ChatiAgent.class) | |
| 351 | + .chatLanguageModel(chatiModel) | |
| 352 | + .chatMemoryProvider(operableChatMemoryProvider) | |
| 353 | + .build(); | |
| 354 | + UserSceneSessionService.CHAT_AGENT_CACHE.put(session.getUserId(), chatiAgent); } | |
| 355 | + String sChatMessage = chatiAgent.chat(session.getUserId(), input); | |
| 356 | + String systemText = " 你已进入「随便聊聊」专栏, 可"+CommonConstant.RESET; | |
| 357 | + return AiResponseDTO.builder().aiText(sChatMessage).sSceneName("随便聊聊").systemText(systemText).sReturnType(ReturnTypeCode.HTML.getCode()).build(); | |
| 358 | + } | |
| 359 | + | |
| 360 | +} | |
| 0 | 361 | \ No newline at end of file | ... | ... |