正在显示
28 个修改的文件
包含
4294 行增加
和
0 行删除
.claude/settings.local.json
0 → 100644
.gitattributes
0 → 100644
.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
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.3 | ||
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.3 | ||
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" |
pom.xml
0 → 100644
1 | +<?xml version="1.0" encoding="UTF-8"?> | ||
2 | +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
3 | + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
4 | + <modelVersion>4.0.0</modelVersion> | ||
5 | + | ||
6 | + <parent> | ||
7 | + <groupId>org.springframework.boot</groupId> | ||
8 | + <artifactId>spring-boot-starter-parent</artifactId> | ||
9 | + <version>3.3.3</version> | ||
10 | + <relativePath/> | ||
11 | + </parent> | ||
12 | + | ||
13 | + <groupId>com.aigeo</groupId> | ||
14 | + <artifactId>aigeo</artifactId> | ||
15 | + <version>1.0.0</version> | ||
16 | + <name>AIGEO</name> | ||
17 | + <description>AI Content Generation Platform</description> | ||
18 | + | ||
19 | + <properties> | ||
20 | + <java.version>17</java.version> | ||
21 | + <mysql.version>8.0.33</mysql.version> | ||
22 | + <mybatis-plus.version>3.5.5</mybatis-plus.version> | ||
23 | + <knife4j.version>4.4.0</knife4j.version> | ||
24 | + <fastjson2.version>2.0.43</fastjson2.version> | ||
25 | + <hutool.version>5.8.25</hutool.version> | ||
26 | + <jwt.version>4.4.0</jwt.version> | ||
27 | + <redisson.version>3.25.2</redisson.version> | ||
28 | + <jsqlparser.version>4.9</jsqlparser.version> | ||
29 | + </properties> | ||
30 | + | ||
31 | + <dependencies> | ||
32 | + <!-- Spring Boot Starters --> | ||
33 | + <dependency> | ||
34 | + <groupId>org.springframework.boot</groupId> | ||
35 | + <artifactId>spring-boot-starter-web</artifactId> | ||
36 | + </dependency> | ||
37 | + | ||
38 | + <dependency> | ||
39 | + <groupId>org.springframework.boot</groupId> | ||
40 | + <artifactId>spring-boot-starter-data-jpa</artifactId> | ||
41 | + <exclusions> | ||
42 | + <exclusion> | ||
43 | + <groupId>com.github.jsqlparser</groupId> | ||
44 | + <artifactId>jsqlparser</artifactId> | ||
45 | + </exclusion> | ||
46 | + </exclusions> | ||
47 | + </dependency> | ||
48 | + | ||
49 | + <dependency> | ||
50 | + <groupId>org.springframework.boot</groupId> | ||
51 | + <artifactId>spring-boot-starter-validation</artifactId> | ||
52 | + </dependency> | ||
53 | + | ||
54 | + <dependency> | ||
55 | + <groupId>org.springframework.boot</groupId> | ||
56 | + <artifactId>spring-boot-starter-security</artifactId> | ||
57 | + </dependency> | ||
58 | + | ||
59 | + <dependency> | ||
60 | + <groupId>org.springframework.boot</groupId> | ||
61 | + <artifactId>spring-boot-starter-data-redis</artifactId> | ||
62 | + </dependency> | ||
63 | + | ||
64 | + <dependency> | ||
65 | + <groupId>org.springframework.boot</groupId> | ||
66 | + <artifactId>spring-boot-starter-quartz</artifactId> | ||
67 | + </dependency> | ||
68 | + | ||
69 | + <dependency> | ||
70 | + <groupId>org.springframework.boot</groupId> | ||
71 | + <artifactId>spring-boot-starter-actuator</artifactId> | ||
72 | + </dependency> | ||
73 | + | ||
74 | + <!-- Database --> | ||
75 | + <dependency> | ||
76 | + <groupId>com.mysql</groupId> | ||
77 | + <artifactId>mysql-connector-j</artifactId> | ||
78 | + <scope>runtime</scope> | ||
79 | + </dependency> | ||
80 | + | ||
81 | + <!-- MyBatis Plus --> | ||
82 | + <dependency> | ||
83 | + <groupId>com.baomidou</groupId> | ||
84 | + <artifactId>mybatis-plus-boot-starter</artifactId> | ||
85 | + <version>${mybatis-plus.version}</version> | ||
86 | + </dependency> | ||
87 | + | ||
88 | + <!-- API Documentation --> | ||
89 | + <dependency> | ||
90 | + <groupId>com.github.xiaoymin</groupId> | ||
91 | + <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> | ||
92 | + <version>${knife4j.version}</version> | ||
93 | + </dependency> | ||
94 | + | ||
95 | + <!-- JSON Processing --> | ||
96 | + <dependency> | ||
97 | + <groupId>com.alibaba.fastjson2</groupId> | ||
98 | + <artifactId>fastjson2</artifactId> | ||
99 | + <version>${fastjson2.version}</version> | ||
100 | + </dependency> | ||
101 | + | ||
102 | + <!-- Utilities --> | ||
103 | + <dependency> | ||
104 | + <groupId>cn.hutool</groupId> | ||
105 | + <artifactId>hutool-all</artifactId> | ||
106 | + <version>${hutool.version}</version> | ||
107 | + </dependency> | ||
108 | + | ||
109 | + <!-- JWT --> | ||
110 | + <dependency> | ||
111 | + <groupId>io.jsonwebtoken</groupId> | ||
112 | + <artifactId>jjwt-api</artifactId> | ||
113 | + <version>0.11.5</version> | ||
114 | + </dependency> | ||
115 | + <dependency> | ||
116 | + <groupId>io.jsonwebtoken</groupId> | ||
117 | + <artifactId>jjwt-impl</artifactId> | ||
118 | + <version>0.11.5</version> | ||
119 | + <scope>runtime</scope> | ||
120 | + </dependency> | ||
121 | + <dependency> | ||
122 | + <groupId>io.jsonwebtoken</groupId> | ||
123 | + <artifactId>jjwt-jackson</artifactId> | ||
124 | + <version>0.11.5</version> | ||
125 | + <scope>runtime</scope> | ||
126 | + </dependency> | ||
127 | + | ||
128 | + <!-- Redis Client --> | ||
129 | + <dependency> | ||
130 | + <groupId>org.redisson</groupId> | ||
131 | + <artifactId>redisson-spring-boot-starter</artifactId> | ||
132 | + <version>${redisson.version}</version> | ||
133 | + </dependency> | ||
134 | + | ||
135 | + <!-- HTTP Client --> | ||
136 | + <dependency> | ||
137 | + <groupId>org.springframework.boot</groupId> | ||
138 | + <artifactId>spring-boot-starter-webflux</artifactId> | ||
139 | + </dependency> | ||
140 | + | ||
141 | + <!-- Configuration Processor --> | ||
142 | + <dependency> | ||
143 | + <groupId>org.springframework.boot</groupId> | ||
144 | + <artifactId>spring-boot-configuration-processor</artifactId> | ||
145 | + <optional>true</optional> | ||
146 | + </dependency> | ||
147 | + | ||
148 | + <!-- Jakarta Annotations --> | ||
149 | + <dependency> | ||
150 | + <groupId>jakarta.annotation</groupId> | ||
151 | + <artifactId>jakarta.annotation-api</artifactId> | ||
152 | + </dependency> | ||
153 | + | ||
154 | + <!-- Lombok --> | ||
155 | + <dependency> | ||
156 | + <groupId>org.projectlombok</groupId> | ||
157 | + <artifactId>lombok</artifactId> | ||
158 | + <optional>true</optional> | ||
159 | + </dependency> | ||
160 | + | ||
161 | + <!-- JSqlParser --> | ||
162 | + <dependency> | ||
163 | + <groupId>com.github.jsqlparser</groupId> | ||
164 | + <artifactId>jsqlparser</artifactId> | ||
165 | + <version>${jsqlparser.version}</version> | ||
166 | + </dependency> | ||
167 | + | ||
168 | + <!-- Development Tools --> | ||
169 | + <dependency> | ||
170 | + <groupId>org.springframework.boot</groupId> | ||
171 | + <artifactId>spring-boot-devtools</artifactId> | ||
172 | + <scope>runtime</scope> | ||
173 | + <optional>true</optional> | ||
174 | + </dependency> | ||
175 | + | ||
176 | + <!-- Test Dependencies --> | ||
177 | + <dependency> | ||
178 | + <groupId>org.springframework.boot</groupId> | ||
179 | + <artifactId>spring-boot-starter-test</artifactId> | ||
180 | + <scope>test</scope> | ||
181 | + </dependency> | ||
182 | + | ||
183 | + <dependency> | ||
184 | + <groupId>org.springframework.security</groupId> | ||
185 | + <artifactId>spring-security-test</artifactId> | ||
186 | + <scope>test</scope> | ||
187 | + </dependency> | ||
188 | + <dependency> | ||
189 | + <groupId>org.projectlombok</groupId> | ||
190 | + <artifactId>lombok</artifactId> | ||
191 | + <scope>provided</scope> | ||
192 | + </dependency> | ||
193 | + </dependencies> | ||
194 | + | ||
195 | + <build> | ||
196 | + <plugins> | ||
197 | + <plugin> | ||
198 | + <groupId>org.apache.maven.plugins</groupId> | ||
199 | + <artifactId>maven-compiler-plugin</artifactId> | ||
200 | + <version>3.14.0</version> | ||
201 | + <configuration> | ||
202 | + <source>17</source> | ||
203 | + <target>17</target> | ||
204 | + <fork>true</fork> | ||
205 | + <executable>F:/IntelliJ IDEA/jdk-17.0.12_windows-x64_bin/jdk-17.0.12/bin/javac.exe</executable> | ||
206 | + <annotationProcessorPaths> | ||
207 | + <path> | ||
208 | + <groupId>org.projectlombok</groupId> | ||
209 | + <artifactId>lombok</artifactId> | ||
210 | + <version>1.18.34</version> | ||
211 | + </path> | ||
212 | + </annotationProcessorPaths> | ||
213 | + </configuration> | ||
214 | + </plugin> | ||
215 | + <plugin> | ||
216 | + <groupId>org.springframework.boot</groupId> | ||
217 | + <artifactId>spring-boot-maven-plugin</artifactId> | ||
218 | + <configuration> | ||
219 | + <excludes> | ||
220 | + <exclude> | ||
221 | + <groupId>org.projectlombok</groupId> | ||
222 | + <artifactId>lombok</artifactId> | ||
223 | + </exclude> | ||
224 | + </excludes> | ||
225 | + </configuration> | ||
226 | + </plugin> | ||
227 | + </plugins> | ||
228 | + </build> | ||
229 | + | ||
230 | +</project> |
1 | +package com.aigeo; | ||
2 | + | ||
3 | +import org.springframework.boot.SpringApplication; | ||
4 | +import org.springframework.boot.autoconfigure.SpringBootApplication; | ||
5 | +import org.springframework.boot.ApplicationRunner; | ||
6 | +import org.springframework.context.annotation.Bean; | ||
7 | +import org.springframework.scheduling.annotation.EnableAsync; | ||
8 | +import org.springframework.scheduling.annotation.EnableScheduling; | ||
9 | +import org.springframework.transaction.annotation.EnableTransactionManagement; | ||
10 | +import org.slf4j.Logger; | ||
11 | +import org.slf4j.LoggerFactory; | ||
12 | + | ||
13 | +/** | ||
14 | + * AIGEO AI内容生成平台主启动类 | ||
15 | + * | ||
16 | + * @author AIGEO Team | ||
17 | + * @since 1.0.0 | ||
18 | + */ | ||
19 | +@SpringBootApplication | ||
20 | +@EnableScheduling | ||
21 | +@EnableAsync | ||
22 | +@EnableTransactionManagement | ||
23 | +public class AigeoApplication { | ||
24 | + | ||
25 | + private static final Logger log = LoggerFactory.getLogger(AigeoApplication.class); | ||
26 | + | ||
27 | + public static void main(String[] args) { | ||
28 | + SpringApplication.run(AigeoApplication.class, args); | ||
29 | + log.info("\n========================================"); | ||
30 | + log.info(" AIGEO AI Content Platform Started!"); | ||
31 | + log.info(" API Documentation: /doc.html"); | ||
32 | + log.info("========================================\n"); | ||
33 | + } | ||
34 | + | ||
35 | + /** | ||
36 | + * 提供一个空的ddlApplicationRunner bean以防止Spring Boot自动配置创建的bean类型不匹配 | ||
37 | + */ | ||
38 | + @Bean | ||
39 | + public ApplicationRunner ddlApplicationRunner() { | ||
40 | + return args -> { | ||
41 | + System.out.println("DDL Application Runner executed"); | ||
42 | + }; | ||
43 | + } | ||
44 | + | ||
45 | +} |
1 | +package com.aigeo.ai.controller; | ||
2 | + | ||
3 | +import com.aigeo.ai.entity.DifyApiConfig; | ||
4 | +import com.aigeo.ai.service.DifyApiConfigService; | ||
5 | +import com.aigeo.common.result.Result; | ||
6 | +import io.swagger.v3.oas.annotations.Operation; | ||
7 | +import io.swagger.v3.oas.annotations.Parameter; | ||
8 | +import io.swagger.v3.oas.annotations.tags.Tag; | ||
9 | +import lombok.RequiredArgsConstructor; | ||
10 | +import lombok.extern.slf4j.Slf4j; | ||
11 | +import org.springframework.data.domain.Page; | ||
12 | +import org.springframework.data.domain.PageRequest; | ||
13 | +import org.springframework.data.domain.Pageable; | ||
14 | +import org.springframework.web.bind.annotation.*; | ||
15 | + | ||
16 | +import jakarta.validation.Valid; | ||
17 | +import jakarta.validation.constraints.NotNull; | ||
18 | +import java.util.List; | ||
19 | +import java.util.Optional; | ||
20 | + | ||
21 | +/** | ||
22 | + * AI配置管理控制器 | ||
23 | + * | ||
24 | + * @author AIGEO Team | ||
25 | + * @since 1.0.0 | ||
26 | + */ | ||
27 | +@Tag(name = "AI配置管理", description = "Dify API配置管理接口") | ||
28 | +@Slf4j | ||
29 | +@RestController | ||
30 | +@RequestMapping("/api/ai-configs") | ||
31 | +@RequiredArgsConstructor | ||
32 | +public class DifyApiConfigController { | ||
33 | + | ||
34 | + private final DifyApiConfigService difyApiConfigService; | ||
35 | + | ||
36 | + @Operation(summary = "获取所有配置", description = "获取所有AI配置列表") | ||
37 | + @GetMapping | ||
38 | + public Result<List<DifyApiConfig>> getAllConfigs() { | ||
39 | + log.debug("获取所有AI配置"); | ||
40 | + List<DifyApiConfig> configs = difyApiConfigService.getAllConfigs(); | ||
41 | + return Result.success(configs); | ||
42 | + } | ||
43 | + | ||
44 | + @Operation(summary = "分页查询配置", description = "分页查询AI配置列表") | ||
45 | + @GetMapping("/list") | ||
46 | + public Result<Page<DifyApiConfig>> listConfigs( | ||
47 | + @Parameter(description = "页码") @RequestParam(defaultValue = "0") int page, | ||
48 | + @Parameter(description = "页大小") @RequestParam(defaultValue = "10") int size) { | ||
49 | + log.debug("分页查询AI配置,page: {}, size: {}", page, size); | ||
50 | + Pageable pageable = PageRequest.of(page, size); | ||
51 | + Page<DifyApiConfig> configs = difyApiConfigService.findAll(pageable); | ||
52 | + return Result.success(configs); | ||
53 | + } | ||
54 | + | ||
55 | + @Operation(summary = "根据ID获取配置", description = "根据配置ID获取单个AI配置") | ||
56 | + @GetMapping("/{id}") | ||
57 | + public Result<DifyApiConfig> getConfigById( | ||
58 | + @Parameter(description = "配置ID") @PathVariable @NotNull Integer id) { | ||
59 | + log.debug("获取AI配置,id: {}", id); | ||
60 | + Optional<DifyApiConfig> config = difyApiConfigService.getConfigById(id); | ||
61 | + if (config.isPresent()) { | ||
62 | + return Result.success(config.get()); | ||
63 | + } else { | ||
64 | + return Result.error(404, "配置不存在"); | ||
65 | + } | ||
66 | + } | ||
67 | + | ||
68 | + @Operation(summary = "根据公司ID获取配置", description = "获取指定公司的AI配置列表") | ||
69 | + @GetMapping("/company/{companyId}") | ||
70 | + public Result<List<DifyApiConfig>> getConfigsByCompanyId( | ||
71 | + @Parameter(description = "公司ID") @PathVariable @NotNull Integer companyId) { | ||
72 | + log.debug("获取公司AI配置,companyId: {}", companyId); | ||
73 | + List<DifyApiConfig> configs = difyApiConfigService.getConfigsByCompanyId(companyId); | ||
74 | + return Result.success(configs); | ||
75 | + } | ||
76 | + | ||
77 | + @Operation(summary = "获取共享配置", description = "获取所有共享的AI配置") | ||
78 | + @GetMapping("/shared") | ||
79 | + public Result<List<DifyApiConfig>> getSharedConfigs() { | ||
80 | + log.debug("获取共享AI配置"); | ||
81 | + List<DifyApiConfig> configs = difyApiConfigService.getSharedConfigs(); | ||
82 | + return Result.success(configs); | ||
83 | + } | ||
84 | + | ||
85 | + @Operation(summary = "获取启用的配置", description = "获取所有启用状态的AI配置") | ||
86 | + @GetMapping("/active") | ||
87 | + public Result<List<DifyApiConfig>> getActiveConfigs() { | ||
88 | + log.debug("获取启用的AI配置"); | ||
89 | + List<DifyApiConfig> configs = difyApiConfigService.getActiveConfigs(); | ||
90 | + return Result.success(configs); | ||
91 | + } | ||
92 | + | ||
93 | + @Operation(summary = "创建配置", description = "创建新的AI配置") | ||
94 | + @PostMapping | ||
95 | + public Result<DifyApiConfig> createConfig( | ||
96 | + @Parameter(description = "配置信息") @Valid @RequestBody DifyApiConfig config) { | ||
97 | + log.info("创建AI配置,name: {}", config.getName()); | ||
98 | + DifyApiConfig createdConfig = difyApiConfigService.saveConfig(config); | ||
99 | + return Result.success(createdConfig); | ||
100 | + } | ||
101 | + | ||
102 | + @Operation(summary = "更新配置", description = "更新指定的AI配置") | ||
103 | + @PutMapping("/{id}") | ||
104 | + public Result<DifyApiConfig> updateConfig( | ||
105 | + @Parameter(description = "配置ID") @PathVariable @NotNull Integer id, | ||
106 | + @Parameter(description = "配置信息") @Valid @RequestBody DifyApiConfig configDetails) { | ||
107 | + log.info("更新AI配置,id: {}, name: {}", id, configDetails.getName()); | ||
108 | + DifyApiConfig updatedConfig = difyApiConfigService.updateConfig(id, configDetails); | ||
109 | + return Result.success(updatedConfig); | ||
110 | + } | ||
111 | + | ||
112 | + @Operation(summary = "删除配置", description = "删除指定的AI配置") | ||
113 | + @DeleteMapping("/{id}") | ||
114 | + public Result<Void> deleteConfig( | ||
115 | + @Parameter(description = "配置ID") @PathVariable @NotNull Integer id) { | ||
116 | + log.info("删除AI配置,id: {}", id); | ||
117 | + difyApiConfigService.deleteConfig(id); | ||
118 | + return Result.success(); | ||
119 | + } | ||
120 | + | ||
121 | + @Operation(summary = "测试配置", description = "测试AI配置的连接性") | ||
122 | + @PostMapping("/{id}/test") | ||
123 | + public Result<Boolean> testConfig( | ||
124 | + @Parameter(description = "配置ID") @PathVariable @NotNull Integer id) { | ||
125 | + log.info("测试AI配置连接,id: {}", id); | ||
126 | + boolean testResult = difyApiConfigService.testConnection(id); | ||
127 | + return Result.success(testResult); | ||
128 | + } | ||
129 | + | ||
130 | + @Operation(summary = "启用/禁用配置", description = "启用或禁用指定的AI配置") | ||
131 | + @PutMapping("/{id}/toggle") | ||
132 | + public Result<DifyApiConfig> toggleConfig( | ||
133 | + @Parameter(description = "配置ID") @PathVariable @NotNull Integer id, | ||
134 | + @Parameter(description = "是否启用") @RequestParam boolean active) { | ||
135 | + log.info("切换AI配置状态,id: {}, active: {}", id, active); | ||
136 | + DifyApiConfig config = difyApiConfigService.toggleActive(id, active); | ||
137 | + return Result.success(config); | ||
138 | + } | ||
139 | +} |
1 | +// ai/controller/PromptTemplateController.java | ||
2 | +package com.aigeo.ai.controller; | ||
3 | + | ||
4 | +import com.aigeo.ai.entity.PromptTemplate; | ||
5 | +import com.aigeo.ai.service.PromptTemplateService; | ||
6 | +import org.springframework.beans.factory.annotation.Autowired; | ||
7 | +import org.springframework.http.ResponseEntity; | ||
8 | +import org.springframework.web.bind.annotation.*; | ||
9 | + | ||
10 | +import java.util.List; | ||
11 | +import java.util.Optional; | ||
12 | + | ||
13 | +@RestController | ||
14 | +@RequestMapping("/api/prompt-templates") | ||
15 | +public class PromptTemplateController { | ||
16 | + | ||
17 | + @Autowired | ||
18 | + private PromptTemplateService promptTemplateService; | ||
19 | + | ||
20 | + @GetMapping | ||
21 | + public List<PromptTemplate> getAllTemplates() { | ||
22 | + return promptTemplateService.getAllTemplates(); | ||
23 | + } | ||
24 | + | ||
25 | + @GetMapping("/{id}") | ||
26 | + public ResponseEntity<PromptTemplate> getTemplateById(@PathVariable Integer id) { | ||
27 | + Optional<PromptTemplate> template = promptTemplateService.getTemplateById(id); | ||
28 | + return template.map(ResponseEntity::ok) | ||
29 | + .orElse(ResponseEntity.notFound().build()); | ||
30 | + } | ||
31 | + | ||
32 | + @GetMapping("/company/{companyId}") | ||
33 | + public List<PromptTemplate> getTemplatesByCompanyId(@PathVariable Integer companyId) { | ||
34 | + return promptTemplateService.getTemplatesByCompanyId(companyId); | ||
35 | + } | ||
36 | + | ||
37 | + @GetMapping("/system") | ||
38 | + public List<PromptTemplate> getSystemTemplates() { | ||
39 | + return promptTemplateService.getSystemTemplates(); | ||
40 | + } | ||
41 | + | ||
42 | + @GetMapping("/active") | ||
43 | + public List<PromptTemplate> getActiveTemplates() { | ||
44 | + return promptTemplateService.getActiveTemplates(); | ||
45 | + } | ||
46 | + | ||
47 | + @GetMapping("/company/{companyId}/active") | ||
48 | + public List<PromptTemplate> getActiveTemplatesByCompanyId(@PathVariable Integer companyId) { | ||
49 | + return promptTemplateService.getActiveTemplatesByCompanyId(companyId); | ||
50 | + } | ||
51 | + | ||
52 | + @PostMapping | ||
53 | + public PromptTemplate createTemplate(@RequestBody PromptTemplate template) { | ||
54 | + return promptTemplateService.saveTemplate(template); | ||
55 | + } | ||
56 | + | ||
57 | + @PutMapping("/{id}") | ||
58 | + public ResponseEntity<PromptTemplate> updateTemplate(@PathVariable Integer id, | ||
59 | + @RequestBody PromptTemplate templateDetails) { | ||
60 | + Optional<PromptTemplate> template = promptTemplateService.getTemplateById(id); | ||
61 | + if (template.isPresent()) { | ||
62 | + PromptTemplate updatedTemplate = template.get(); | ||
63 | + updatedTemplate.setName(templateDetails.getName()); | ||
64 | + updatedTemplate.setDescription(templateDetails.getDescription()); | ||
65 | + updatedTemplate.setLanguage(templateDetails.getLanguage()); | ||
66 | + updatedTemplate.setContent(templateDetails.getContent()); | ||
67 | + updatedTemplate.setVariables(templateDetails.getVariables()); | ||
68 | + updatedTemplate.setIsActive(templateDetails.getIsActive()); | ||
69 | + | ||
70 | + PromptTemplate savedTemplate = promptTemplateService.saveTemplate(updatedTemplate); | ||
71 | + return ResponseEntity.ok(savedTemplate); | ||
72 | + } else { | ||
73 | + return ResponseEntity.notFound().build(); | ||
74 | + } | ||
75 | + } | ||
76 | + | ||
77 | + @DeleteMapping("/{id}") | ||
78 | + public ResponseEntity<Void> deleteTemplate(@PathVariable Integer id) { | ||
79 | + promptTemplateService.deleteTemplate(id); | ||
80 | + return ResponseEntity.noContent().build(); | ||
81 | + } | ||
82 | +} |
1 | +// ai/controller/UploadedFileController.java | ||
2 | +package com.aigeo.ai.controller; | ||
3 | + | ||
4 | +import com.aigeo.ai.entity.UploadedFile; | ||
5 | +import com.aigeo.common.enums.FileStatus; | ||
6 | +import com.aigeo.common.enums.FileType; | ||
7 | +import com.aigeo.ai.service.UploadedFileService; | ||
8 | +import org.springframework.beans.factory.annotation.Autowired; | ||
9 | +import org.springframework.http.ResponseEntity; | ||
10 | +import org.springframework.web.bind.annotation.*; | ||
11 | + | ||
12 | +import java.util.List; | ||
13 | +import java.util.Optional; | ||
14 | + | ||
15 | +@RestController | ||
16 | +@RequestMapping("/api/uploaded-files") | ||
17 | +public class UploadedFileController { | ||
18 | + | ||
19 | + @Autowired | ||
20 | + private UploadedFileService uploadedFileService; | ||
21 | + | ||
22 | + @GetMapping | ||
23 | + public List<UploadedFile> getAllFiles() { | ||
24 | + return uploadedFileService.getAllFiles(); | ||
25 | + } | ||
26 | + | ||
27 | + @GetMapping("/{id}") | ||
28 | + public ResponseEntity<UploadedFile> getFileById(@PathVariable Integer id) { | ||
29 | + Optional<UploadedFile> file = uploadedFileService.getFileById(id); | ||
30 | + return file.map(ResponseEntity::ok) | ||
31 | + .orElse(ResponseEntity.notFound().build()); | ||
32 | + } | ||
33 | + | ||
34 | + @GetMapping("/company/{companyId}") | ||
35 | + public List<UploadedFile> getFilesByCompanyId(@PathVariable Integer companyId) { | ||
36 | + return uploadedFileService.getFilesByCompanyId(companyId); | ||
37 | + } | ||
38 | + | ||
39 | + @GetMapping("/user/{userId}") | ||
40 | + public List<UploadedFile> getFilesByUserId(@PathVariable Integer userId) { | ||
41 | + return uploadedFileService.getFilesByUserId(userId); | ||
42 | + } | ||
43 | + | ||
44 | + @GetMapping("/company/{companyId}/user/{userId}") | ||
45 | + public List<UploadedFile> getFilesByCompanyIdAndUserId(@PathVariable Integer companyId, | ||
46 | + @PathVariable Integer userId) { | ||
47 | + return uploadedFileService.getFilesByCompanyIdAndUserId(companyId, userId); | ||
48 | + } | ||
49 | + | ||
50 | + @GetMapping("/type/{fileType}") | ||
51 | + public List<UploadedFile> getFilesByFileType(@PathVariable FileType fileType) { | ||
52 | + return uploadedFileService.getFilesByFileType(fileType); | ||
53 | + } | ||
54 | + | ||
55 | + @GetMapping("/status/{status}") | ||
56 | + public List<UploadedFile> getFilesByStatus(@PathVariable FileStatus status) { | ||
57 | + return uploadedFileService.getFilesByStatus(status); | ||
58 | + } | ||
59 | + | ||
60 | + @GetMapping("/company/{companyId}/status/{status}") | ||
61 | + public List<UploadedFile> getFilesByCompanyIdAndStatus(@PathVariable Integer companyId, | ||
62 | + @PathVariable FileStatus status) { | ||
63 | + return uploadedFileService.getFilesByCompanyIdAndStatus(companyId, status); | ||
64 | + } | ||
65 | + | ||
66 | + @PostMapping | ||
67 | + public UploadedFile createFile(@RequestBody UploadedFile file) { | ||
68 | + return uploadedFileService.saveFile(file); | ||
69 | + } | ||
70 | + | ||
71 | + @PutMapping("/{id}") | ||
72 | + public ResponseEntity<UploadedFile> updateFile(@PathVariable Integer id, | ||
73 | + @RequestBody UploadedFile fileDetails) { | ||
74 | + Optional<UploadedFile> file = uploadedFileService.getFileById(id); | ||
75 | + if (file.isPresent()) { | ||
76 | + UploadedFile updatedFile = file.get(); | ||
77 | + updatedFile.setFileName(fileDetails.getFileName()); | ||
78 | + updatedFile.setFilePath(fileDetails.getFilePath()); | ||
79 | + updatedFile.setFileType(fileDetails.getFileType()); | ||
80 | + updatedFile.setFileSize(fileDetails.getFileSize()); | ||
81 | + updatedFile.setMimeType(fileDetails.getMimeType()); | ||
82 | + updatedFile.setStatus(fileDetails.getStatus()); | ||
83 | + | ||
84 | + UploadedFile savedFile = uploadedFileService.saveFile(updatedFile); | ||
85 | + return ResponseEntity.ok(savedFile); | ||
86 | + } else { | ||
87 | + return ResponseEntity.notFound().build(); | ||
88 | + } | ||
89 | + } | ||
90 | + | ||
91 | + @DeleteMapping("/{id}") | ||
92 | + public ResponseEntity<Void> deleteFile(@PathVariable Integer id) { | ||
93 | + uploadedFileService.deleteFile(id); | ||
94 | + return ResponseEntity.noContent().build(); | ||
95 | + } | ||
96 | +} |
1 | +// ai/entity/DifyApiConfig.java | ||
2 | +package com.aigeo.ai.entity; | ||
3 | +import org.hibernate.annotations.JdbcTypeCode; | ||
4 | +import java.sql.Types; | ||
5 | +import jakarta.persistence.*; | ||
6 | +import jakarta.validation.constraints.NotNull; | ||
7 | +import jakarta.validation.constraints.NotBlank; | ||
8 | +import lombok.Data; | ||
9 | +import lombok.Builder; | ||
10 | +import lombok.NoArgsConstructor; | ||
11 | +import lombok.AllArgsConstructor; | ||
12 | +import com.aigeo.common.enums.AiProvider; | ||
13 | +import java.time.LocalDateTime; | ||
14 | +import java.util.Date; | ||
15 | + | ||
16 | + | ||
17 | +/** | ||
18 | + * DifyApiConfig类是一个实体类,用于映射数据库中的ai_dify_api_configs表。 | ||
19 | + * 该类存储了Dify API的配置信息,包括提供商、API密钥、模型参数等。 | ||
20 | + * | ||
21 | + * @author AIGEO Team | ||
22 | + * @since 1.0.0 | ||
23 | + */ | ||
24 | +@Data | ||
25 | +@Builder | ||
26 | +@NoArgsConstructor | ||
27 | +@AllArgsConstructor | ||
28 | +@Entity | ||
29 | +@Table(name = "ai_dify_api_configs") | ||
30 | +public class DifyApiConfig { | ||
31 | + /** | ||
32 | + * 主键ID,采用自增策略 | ||
33 | + */ | ||
34 | + @Id | ||
35 | + @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
36 | + private Integer id; | ||
37 | + | ||
38 | + /** | ||
39 | + * 公司ID,用于区分不同公司的配置 | ||
40 | + */ | ||
41 | + @Column(name = "company_id") | ||
42 | + private Integer companyId; | ||
43 | + | ||
44 | + /** | ||
45 | + * AI服务提供商,使用枚举类型存储 | ||
46 | + */ | ||
47 | + @Enumerated(EnumType.STRING) | ||
48 | + private AiProvider provider; | ||
49 | + | ||
50 | + /** | ||
51 | + * 配置名称,不能为空 | ||
52 | + */ | ||
53 | + @NotBlank(message = "配置名称不能为空") | ||
54 | + @Column(nullable = false) | ||
55 | + private String name; | ||
56 | + | ||
57 | + /** | ||
58 | + * API的基础URL | ||
59 | + */ | ||
60 | + @Column(name = "base_url") | ||
61 | + private String baseUrl; | ||
62 | + | ||
63 | + /** | ||
64 | + * API密钥,用于身份验证 | ||
65 | + */ | ||
66 | + @Column(name = "api_key") | ||
67 | + private String apiKey; | ||
68 | + | ||
69 | + /** | ||
70 | + * 模型名称,指定使用的具体模型 | ||
71 | + */ | ||
72 | + @Column(name = "model_name") | ||
73 | + private String modelName; | ||
74 | + | ||
75 | + /** | ||
76 | + * 温度参数,控制输出的随机性 | ||
77 | + */ | ||
78 | + private Double temperature; | ||
79 | + | ||
80 | + /** | ||
81 | + * Top P参数,控制词汇采样的概率范围 | ||
82 | + */ | ||
83 | + @Column(name = "top_p") | ||
84 | + private Double topP; | ||
85 | + | ||
86 | + /** | ||
87 | + * 最大令牌数,限制生成内容的长度 | ||
88 | + */ | ||
89 | + @Column(name = "max_tokens") | ||
90 | + private Integer maxTokens; | ||
91 | + | ||
92 | + /** | ||
93 | + * 请求头信息,以JSON格式存储 | ||
94 | + */ | ||
95 | + @Column(name = "request_headers") | ||
96 | + private String requestHeaders; | ||
97 | + | ||
98 | + /** | ||
99 | + * 是否激活该配置 | ||
100 | + */ | ||
101 | + @Column(name = "is_active") | ||
102 | + private Boolean isActive; | ||
103 | + | ||
104 | + /** | ||
105 | + * 创建时间,使用时间戳类型 | ||
106 | + */ | ||
107 | + @Column(name = "created_at") | ||
108 | + | ||
109 | + private LocalDateTime createdAt; | ||
110 | + | ||
111 | + /** | ||
112 | + * 更新时间,使用时间戳类型 | ||
113 | + */ | ||
114 | + @Column(name = "updated_at") | ||
115 | + | ||
116 | + private LocalDateTime updatedAt; | ||
117 | + | ||
118 | + /** | ||
119 | + * 创建时间设置,新建实体时自动设置 | ||
120 | + */ | ||
121 | + @PrePersist | ||
122 | + protected void onCreate() { | ||
123 | + createdAt = LocalDateTime.now(); | ||
124 | + updatedAt = LocalDateTime.now(); | ||
125 | + if (isActive == null) { | ||
126 | + isActive = true; | ||
127 | + } | ||
128 | + } | ||
129 | + | ||
130 | + /** | ||
131 | + * 更新时间设置,实体更新时自动设置 | ||
132 | + */ | ||
133 | + @PreUpdate | ||
134 | + protected void onUpdate() { | ||
135 | + updatedAt = LocalDateTime.now(); | ||
136 | + } | ||
137 | +} |
1 | +// ai/entity/PromptTemplate.java | ||
2 | +package com.aigeo.ai.entity; | ||
3 | +import org.hibernate.annotations.JdbcTypeCode; | ||
4 | +import java.sql.Types; | ||
5 | +import jakarta.persistence.*; | ||
6 | +import jakarta.validation.constraints.NotBlank; | ||
7 | +import jakarta.validation.constraints.NotNull; | ||
8 | +import lombok.AllArgsConstructor; | ||
9 | +import lombok.Builder; | ||
10 | +import lombok.Data; | ||
11 | +import lombok.NoArgsConstructor; | ||
12 | +import com.fasterxml.jackson.annotation.JsonFormat; | ||
13 | +import org.hibernate.annotations.CreationTimestamp; | ||
14 | +import org.hibernate.annotations.UpdateTimestamp; | ||
15 | +import java.time.LocalDateTime; | ||
16 | + | ||
17 | +/** | ||
18 | + * 提示模板实体类 | ||
19 | + * 对应数据库表:ai_prompt_templates | ||
20 | + * | ||
21 | + * 存储AI生成内容时使用的提示词模板,包括: | ||
22 | + * - 模板内容和变量定义 | ||
23 | + * - 语言和适用场景配置 | ||
24 | + * - 使用统计和版本管理 | ||
25 | + * | ||
26 | + * @author AIGEO Team | ||
27 | + * @since 1.0.0 | ||
28 | + */ | ||
29 | +@Data | ||
30 | +@Builder | ||
31 | +@NoArgsConstructor | ||
32 | +@AllArgsConstructor | ||
33 | +@Entity | ||
34 | +@Table(name = "ai_prompt_templates", indexes = { | ||
35 | + @Index(name = "idx_prompt_templates_company", columnList = "company_id"), | ||
36 | + @Index(name = "idx_prompt_templates_active", columnList = "is_active"), | ||
37 | + @Index(name = "idx_prompt_templates_language", columnList = "language") | ||
38 | +}) | ||
39 | +public class PromptTemplate { | ||
40 | + | ||
41 | + /** | ||
42 | + * 主键ID | ||
43 | + */ | ||
44 | + @Id | ||
45 | + @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
46 | + @Column(name = "id", nullable = false, updatable = false) | ||
47 | + private Integer id; | ||
48 | + | ||
49 | + /** | ||
50 | + * 公司ID(多租户字段) | ||
51 | + */ | ||
52 | + @NotNull(message = "公司ID不能为空") | ||
53 | + @Column(name = "company_id", nullable = false) | ||
54 | + private Integer companyId; | ||
55 | + | ||
56 | + /** | ||
57 | + * 模板名称 | ||
58 | + */ | ||
59 | + @NotBlank(message = "模板名称不能为空") | ||
60 | + @Column(name = "name", nullable = false, length = 200) | ||
61 | + private String name; | ||
62 | + | ||
63 | + /** | ||
64 | + * 模板描述 | ||
65 | + */ | ||
66 | + @Column(name = "description", length = 500) | ||
67 | + private String description; | ||
68 | + | ||
69 | + /** | ||
70 | + * 模板语言 | ||
71 | + */ | ||
72 | + @Column(name = "language", length = 10) | ||
73 | + @Builder.Default | ||
74 | + private String language = "zh-CN"; | ||
75 | + | ||
76 | + /** | ||
77 | + * 模板内容 | ||
78 | + */ | ||
79 | + @NotBlank(message = "模板内容不能为空") | ||
80 | + @Column(name = "content", nullable = false, columnDefinition = "TEXT") | ||
81 | + private String content; | ||
82 | + | ||
83 | + /** | ||
84 | + * 模板变量(JSON格式存储变量定义) | ||
85 | + */ | ||
86 | + @Column(name = "variables", columnDefinition = "JSON") | ||
87 | + private String variables; | ||
88 | + | ||
89 | + /** | ||
90 | + * 模板类型(如:article, product, seo等) | ||
91 | + */ | ||
92 | + @Column(name = "template_type", length = 50) | ||
93 | + @Builder.Default | ||
94 | + private String templateType = "article"; | ||
95 | + | ||
96 | + /** | ||
97 | + * 使用次数统计 | ||
98 | + */ | ||
99 | + @Column(name = "usage_count") | ||
100 | + @Builder.Default | ||
101 | + private Integer usageCount = 0; | ||
102 | + | ||
103 | + /** | ||
104 | + * 是否激活 | ||
105 | + */ | ||
106 | + @Column(name = "is_active") | ||
107 | + @Builder.Default | ||
108 | + private Boolean isActive = true; | ||
109 | + | ||
110 | + /** | ||
111 | + * 创建时间 | ||
112 | + */ | ||
113 | + @CreationTimestamp | ||
114 | + @Column(name = "created_at", updatable = false) | ||
115 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
116 | + private LocalDateTime createdAt; | ||
117 | + | ||
118 | + /** | ||
119 | + * 更新时间 | ||
120 | + */ | ||
121 | + @UpdateTimestamp | ||
122 | + @Column(name = "updated_at") | ||
123 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
124 | + private LocalDateTime updatedAt; | ||
125 | + | ||
126 | + /** | ||
127 | + * 实体创建前的处理 | ||
128 | + */ | ||
129 | + @PrePersist | ||
130 | + protected void onCreate() { | ||
131 | + if (isActive == null) isActive = true; | ||
132 | + if (language == null) language = "zh-CN"; | ||
133 | + if (templateType == null) templateType = "article"; | ||
134 | + if (usageCount == null) usageCount = 0; | ||
135 | + } | ||
136 | + | ||
137 | + /** | ||
138 | + * 检查是否为激活状态 | ||
139 | + */ | ||
140 | + public boolean isActive() { | ||
141 | + return Boolean.TRUE.equals(isActive); | ||
142 | + } | ||
143 | + | ||
144 | + /** | ||
145 | + * 增加使用次数 | ||
146 | + */ | ||
147 | + public void incrementUsageCount() { | ||
148 | + this.usageCount = (usageCount == null ? 0 : usageCount) + 1; | ||
149 | + } | ||
150 | + | ||
151 | + /** | ||
152 | + * 检查是否为热门模板(使用次数超过阈值) | ||
153 | + */ | ||
154 | + public boolean isPopularTemplate() { | ||
155 | + return usageCount != null && usageCount >= 50; | ||
156 | + } | ||
157 | + | ||
158 | + /** | ||
159 | + * 获取模板的复杂度评级(基于内容长度和变量数量) | ||
160 | + */ | ||
161 | + public String getComplexityLevel() { | ||
162 | + if (content == null) return "SIMPLE"; | ||
163 | + | ||
164 | + int contentLength = content.length(); | ||
165 | + int variableCount = (variables != null && !variables.trim().isEmpty()) ? | ||
166 | + variables.split(",").length : 0; | ||
167 | + | ||
168 | + if (contentLength > 2000 || variableCount > 10) { | ||
169 | + return "COMPLEX"; | ||
170 | + } else if (contentLength > 500 || variableCount > 3) { | ||
171 | + return "MEDIUM"; | ||
172 | + } else { | ||
173 | + return "SIMPLE"; | ||
174 | + } | ||
175 | + } | ||
176 | + | ||
177 | + /** | ||
178 | + * 检查是否包含指定变量 | ||
179 | + */ | ||
180 | + public boolean hasVariable(String variableName) { | ||
181 | + if (variables == null || variables.trim().isEmpty()) { | ||
182 | + return false; | ||
183 | + } | ||
184 | + return variables.contains("\"" + variableName + "\"") || | ||
185 | + variables.contains(variableName); | ||
186 | + } | ||
187 | +} |
1 | +// ai/entity/UploadedFile.java | ||
2 | +package com.aigeo.ai.entity; | ||
3 | +import org.hibernate.annotations.JdbcTypeCode; | ||
4 | +import java.sql.Types; | ||
5 | +import jakarta.persistence.*; | ||
6 | +import jakarta.validation.constraints.NotBlank; | ||
7 | +import jakarta.validation.constraints.NotNull; | ||
8 | +import lombok.Data; | ||
9 | +import lombok.Builder; | ||
10 | +import lombok.NoArgsConstructor; | ||
11 | +import lombok.AllArgsConstructor; | ||
12 | +import com.aigeo.common.enums.FileType; | ||
13 | +import com.aigeo.common.enums.FileStatus; | ||
14 | + | ||
15 | +import java.time.LocalDateTime; | ||
16 | + | ||
17 | + | ||
18 | +/** | ||
19 | + * 上传文件实体类,对应数据库表 ai_uploaded_files | ||
20 | + * | ||
21 | + * 管理系统中上传的文件信息,包括文件元数据、存储路径、 | ||
22 | + * 文件类型、大小、校验和等信息,支持多租户和版本控制 | ||
23 | + * | ||
24 | + * @author AIGEO Team | ||
25 | + * @since 1.0.0 | ||
26 | + */ | ||
27 | +@Data | ||
28 | +@Builder | ||
29 | +@NoArgsConstructor | ||
30 | +@AllArgsConstructor | ||
31 | +@Entity | ||
32 | +@Table(name = "ai_uploaded_files", indexes = { | ||
33 | + @Index(name = "idx_uploaded_files_company", columnList = "company_id"), | ||
34 | + @Index(name = "idx_uploaded_files_user", columnList = "user_id"), | ||
35 | + @Index(name = "idx_uploaded_files_type", columnList = "file_type"), | ||
36 | + @Index(name = "idx_uploaded_files_status", columnList = "status"), | ||
37 | + @Index(name = "idx_uploaded_files_created", columnList = "created_at") | ||
38 | +}) | ||
39 | +public class UploadedFile { | ||
40 | + @Id | ||
41 | + @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
42 | + private Integer id; | ||
43 | + | ||
44 | + /** | ||
45 | + * 公司ID,多租户标识 | ||
46 | + */ | ||
47 | + @NotNull(message = "公司ID不能为空") | ||
48 | + @Column(name = "company_id", nullable = false) | ||
49 | + private Integer companyId; | ||
50 | + | ||
51 | + /** | ||
52 | + * 用户ID,文件上传者 | ||
53 | + */ | ||
54 | + @NotNull(message = "用户ID不能为空") | ||
55 | + @Column(name = "user_id", nullable = false) | ||
56 | + private Integer userId; | ||
57 | + | ||
58 | + /** | ||
59 | + * 原始文件名 | ||
60 | + */ | ||
61 | + @NotBlank(message = "文件名不能为空") | ||
62 | + @Column(name = "file_name", nullable = false, length = 255) | ||
63 | + private String fileName; | ||
64 | + | ||
65 | + /** | ||
66 | + * 文件存储路径 | ||
67 | + */ | ||
68 | + @NotBlank(message = "文件路径不能为空") | ||
69 | + @Column(name = "file_path", nullable = false, length = 500) | ||
70 | + private String filePath; | ||
71 | + | ||
72 | + /** | ||
73 | + * 文件类型枚举 | ||
74 | + */ | ||
75 | + @NotNull(message = "文件类型不能为空") | ||
76 | + @Enumerated(EnumType.STRING) | ||
77 | + @Column(name = "file_type", nullable = false, length = 50) | ||
78 | + private FileType fileType; | ||
79 | + | ||
80 | + /** | ||
81 | + * 文件大小,单位字节 | ||
82 | + */ | ||
83 | + @Column(name = "file_size") | ||
84 | + private Long fileSize; | ||
85 | + | ||
86 | + /** | ||
87 | + * MIME类型 | ||
88 | + */ | ||
89 | + @Column(name = "mime_type", length = 100) | ||
90 | + private String mimeType; | ||
91 | + | ||
92 | + /** | ||
93 | + * 文件校验和,用于文件完整性验证 | ||
94 | + */ | ||
95 | + @Column(length = 128) | ||
96 | + private String checksum; | ||
97 | + | ||
98 | + /** | ||
99 | + * 文件版本号,支持文件版本管理 | ||
100 | + */ | ||
101 | + @Builder.Default | ||
102 | + private Integer version = 1; | ||
103 | + | ||
104 | + /** | ||
105 | + * 文件状态 | ||
106 | + */ | ||
107 | + @Enumerated(EnumType.STRING) | ||
108 | + @Column(length = 20) | ||
109 | + @Builder.Default | ||
110 | + private FileStatus status = FileStatus.UPLOADED; | ||
111 | + | ||
112 | + /** | ||
113 | + * 创建时间 | ||
114 | + */ | ||
115 | + @Column(name = "created_at", nullable = false, updatable = false) | ||
116 | + | ||
117 | + private LocalDateTime createdAt; | ||
118 | + | ||
119 | + /** | ||
120 | + * 创建时间自动设置 | ||
121 | + */ | ||
122 | + @PrePersist | ||
123 | + protected void onCreate() { | ||
124 | + createdAt = LocalDateTime.now(); | ||
125 | + } | ||
126 | + | ||
127 | + /** | ||
128 | + * 获取文件大小的可读格式 | ||
129 | + * @return 格式化的文件大小字符串 | ||
130 | + */ | ||
131 | + public String getFormattedFileSize() { | ||
132 | + if (fileSize == null) return "未知"; | ||
133 | + if (fileSize < 1024) return fileSize + " B"; | ||
134 | + if (fileSize < 1024 * 1024) return String.format("%.1f KB", fileSize / 1024.0); | ||
135 | + if (fileSize < 1024 * 1024 * 1024) return String.format("%.1f MB", fileSize / (1024.0 * 1024.0)); | ||
136 | + return String.format("%.1f GB", fileSize / (1024.0 * 1024.0 * 1024.0)); | ||
137 | + } | ||
138 | + | ||
139 | + /** | ||
140 | + * 检查文件是否为图片类型 | ||
141 | + * @return 是否为图片 | ||
142 | + */ | ||
143 | + public boolean isImage() { | ||
144 | + return fileType == FileType.IMAGE || | ||
145 | + (mimeType != null && mimeType.startsWith("image/")); | ||
146 | + } | ||
147 | + | ||
148 | + /** | ||
149 | + * 检查文件是否为文档类型 | ||
150 | + * @return 是否为文档 | ||
151 | + */ | ||
152 | + public boolean isDocument() { | ||
153 | + return fileType == FileType.DOCUMENT || | ||
154 | + (mimeType != null && (mimeType.contains("pdf") || | ||
155 | + mimeType.contains("word") || | ||
156 | + mimeType.contains("text"))); | ||
157 | + } | ||
158 | +} |
1 | +// ai/repository/DifyApiConfigRepository.java | ||
2 | +package com.aigeo.ai.repository; | ||
3 | + | ||
4 | +import com.aigeo.ai.entity.DifyApiConfig; | ||
5 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
6 | +import org.springframework.stereotype.Repository; | ||
7 | + | ||
8 | +import java.util.List; | ||
9 | + | ||
10 | +@Repository | ||
11 | +public interface DifyApiConfigRepository extends JpaRepository<DifyApiConfig, Integer> { | ||
12 | + List<DifyApiConfig> findByCompanyId(Integer companyId); | ||
13 | + List<DifyApiConfig> findByCompanyIdIsNull(); // 共享配置 | ||
14 | + List<DifyApiConfig> findByIsActiveTrue(); | ||
15 | +} |
1 | +// ai/repository/PromptTemplateRepository.java | ||
2 | +package com.aigeo.ai.repository; | ||
3 | + | ||
4 | +import com.aigeo.ai.entity.PromptTemplate; | ||
5 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
6 | +import org.springframework.stereotype.Repository; | ||
7 | + | ||
8 | +import java.util.List; | ||
9 | + | ||
10 | +@Repository | ||
11 | +public interface PromptTemplateRepository extends JpaRepository<PromptTemplate, Integer> { | ||
12 | + List<PromptTemplate> findByCompanyId(Integer companyId); | ||
13 | + List<PromptTemplate> findByCompanyIdIsNull(); // 系统模板 | ||
14 | + List<PromptTemplate> findByIsActiveTrue(); | ||
15 | + List<PromptTemplate> findByCompanyIdAndIsActiveTrue(Integer companyId); | ||
16 | +} |
1 | +// ai/repository/UploadedFileRepository.java | ||
2 | +package com.aigeo.ai.repository; | ||
3 | + | ||
4 | +import com.aigeo.ai.entity.UploadedFile; | ||
5 | +import com.aigeo.common.enums.FileStatus; | ||
6 | +import com.aigeo.common.enums.FileType; | ||
7 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
8 | +import org.springframework.stereotype.Repository; | ||
9 | + | ||
10 | +import java.util.List; | ||
11 | + | ||
12 | +@Repository | ||
13 | +public interface UploadedFileRepository extends JpaRepository<UploadedFile, Integer> { | ||
14 | + List<UploadedFile> findByCompanyId(Integer companyId); | ||
15 | + List<UploadedFile> findByUserId(Integer userId); | ||
16 | + List<UploadedFile> findByCompanyIdAndUserId(Integer companyId, Integer userId); | ||
17 | + List<UploadedFile> findByFileType(FileType fileType); | ||
18 | + List<UploadedFile> findByStatus(FileStatus status); | ||
19 | + List<UploadedFile> findByCompanyIdAndStatus(Integer companyId, FileStatus status); | ||
20 | +} |
1 | +// ai/service/DifyApiConfigService.java | ||
2 | +package com.aigeo.ai.service; | ||
3 | + | ||
4 | +import com.aigeo.ai.entity.DifyApiConfig; | ||
5 | +import com.aigeo.ai.repository.DifyApiConfigRepository; | ||
6 | +import com.aigeo.common.exception.BusinessException; | ||
7 | +import com.aigeo.common.result.ResultCode; | ||
8 | +import jakarta.validation.Valid; | ||
9 | +import jakarta.validation.constraints.NotNull; | ||
10 | +import lombok.RequiredArgsConstructor; | ||
11 | +import lombok.extern.slf4j.Slf4j; | ||
12 | +import org.springframework.beans.factory.annotation.Autowired; | ||
13 | +import org.springframework.data.domain.Page; | ||
14 | +import org.springframework.data.domain.Pageable; | ||
15 | +import org.springframework.stereotype.Service; | ||
16 | +import org.springframework.transaction.annotation.Transactional; | ||
17 | + | ||
18 | +import java.util.List; | ||
19 | +import java.util.Optional; | ||
20 | + | ||
21 | +/** | ||
22 | + * Dify API配置服务类 | ||
23 | + * | ||
24 | + * 负责管理AI配置的业务逻辑,包括配置的CRUD操作、 | ||
25 | + * 连接测试、状态管理等功能,支持多租户和权限控制 | ||
26 | + * | ||
27 | + * @author AIGEO Team | ||
28 | + * @since 1.0.0 | ||
29 | + */ | ||
30 | +@Slf4j | ||
31 | +@Service | ||
32 | +@RequiredArgsConstructor | ||
33 | +@Transactional(readOnly = true) | ||
34 | +public class DifyApiConfigService { | ||
35 | + | ||
36 | + private final DifyApiConfigRepository difyApiConfigRepository; | ||
37 | + | ||
38 | + /** | ||
39 | + * 获取所有配置 | ||
40 | + * @return 所有配置列表 | ||
41 | + */ | ||
42 | + public List<DifyApiConfig> getAllConfigs() { | ||
43 | + log.debug("获取所有AI配置"); | ||
44 | + return difyApiConfigRepository.findAll(); | ||
45 | + } | ||
46 | + | ||
47 | + /** | ||
48 | + * 根据公司ID获取配置列表 | ||
49 | + * @param companyId 公司ID | ||
50 | + * @return 该公司的配置列表 | ||
51 | + * @throws BusinessException 当公司ID为空时抛出 | ||
52 | + */ | ||
53 | + public List<DifyApiConfig> getConfigsByCompanyId(Integer companyId) { | ||
54 | + log.debug("根据公司ID获取AI配置,companyId: {}", companyId); | ||
55 | + if (companyId == null) { | ||
56 | + throw new BusinessException(ResultCode.PARAM_ERROR, "公司ID不能为空"); | ||
57 | + } | ||
58 | + return difyApiConfigRepository.findByCompanyId(companyId); | ||
59 | + } | ||
60 | + | ||
61 | + public List<DifyApiConfig> getSharedConfigs() { | ||
62 | + return difyApiConfigRepository.findByCompanyIdIsNull(); | ||
63 | + } | ||
64 | + | ||
65 | + public List<DifyApiConfig> getActiveConfigs() { | ||
66 | + return difyApiConfigRepository.findByIsActiveTrue(); | ||
67 | + } | ||
68 | + | ||
69 | + public Optional<DifyApiConfig> getConfigById(Integer id) { | ||
70 | + return difyApiConfigRepository.findById(id); | ||
71 | + } | ||
72 | + | ||
73 | + public DifyApiConfig saveConfig(DifyApiConfig config) { | ||
74 | + return difyApiConfigRepository.save(config); | ||
75 | + } | ||
76 | + | ||
77 | + public void deleteConfig(Integer id) { | ||
78 | + difyApiConfigRepository.deleteById(id); | ||
79 | + } | ||
80 | + | ||
81 | + public Page<DifyApiConfig> findAll(Pageable pageable) { | ||
82 | + return null; | ||
83 | + } | ||
84 | + | ||
85 | + public DifyApiConfig updateConfig(@NotNull Integer id, @Valid DifyApiConfig configDetails) { | ||
86 | + return configDetails; | ||
87 | + } | ||
88 | + | ||
89 | + /** | ||
90 | + * 测试API配置的连接性 | ||
91 | + * @param id 配置ID | ||
92 | + * @return 连接是否成功 | ||
93 | + */ | ||
94 | + public boolean testConnection(@NotNull Integer id) { | ||
95 | + Optional<DifyApiConfig> configOpt = difyApiConfigRepository.findById(id); | ||
96 | + if (!configOpt.isPresent()) { | ||
97 | + return false; | ||
98 | + } | ||
99 | + | ||
100 | + DifyApiConfig config = configOpt.get(); | ||
101 | + if (config.getApiKey() == null || config.getBaseUrl() == null) { | ||
102 | + return false; | ||
103 | + } | ||
104 | + | ||
105 | + // 这里应该实现具体的连接测试逻辑 | ||
106 | + // 比如发送HTTP请求到API端点进行验证 | ||
107 | + // 暂时返回true表示测试通过 | ||
108 | + return true; | ||
109 | + } | ||
110 | + | ||
111 | + /** | ||
112 | + * 切换配置的激活状态 | ||
113 | + * @param id 配置ID | ||
114 | + * @param active 目标状态 | ||
115 | + * @return 更新后的配置对象 | ||
116 | + * @throws BusinessException 当配置不存在时抛出 | ||
117 | + */ | ||
118 | + @Transactional | ||
119 | + public DifyApiConfig toggleActive(@NotNull Integer id, boolean active) { | ||
120 | + Optional<DifyApiConfig> configOpt = difyApiConfigRepository.findById(id); | ||
121 | + if (!configOpt.isPresent()) { | ||
122 | + throw new BusinessException(ResultCode.DATA_NOT_FOUND, "配置不存在"); | ||
123 | + } | ||
124 | + | ||
125 | + DifyApiConfig config = configOpt.get(); | ||
126 | + config.setIsActive(active); | ||
127 | + return difyApiConfigRepository.save(config); | ||
128 | + } | ||
129 | +} |
1 | +// ai/service/PromptTemplateService.java | ||
2 | +package com.aigeo.ai.service; | ||
3 | + | ||
4 | +import com.aigeo.ai.entity.PromptTemplate; | ||
5 | +import com.aigeo.ai.repository.PromptTemplateRepository; | ||
6 | +import org.springframework.beans.factory.annotation.Autowired; | ||
7 | +import org.springframework.stereotype.Service; | ||
8 | + | ||
9 | +import java.util.List; | ||
10 | +import java.util.Optional; | ||
11 | + | ||
12 | +@Service | ||
13 | +public class PromptTemplateService { | ||
14 | + | ||
15 | + @Autowired | ||
16 | + private PromptTemplateRepository promptTemplateRepository; | ||
17 | + | ||
18 | + public List<PromptTemplate> getAllTemplates() { | ||
19 | + return promptTemplateRepository.findAll(); | ||
20 | + } | ||
21 | + | ||
22 | + public List<PromptTemplate> getTemplatesByCompanyId(Integer companyId) { | ||
23 | + return promptTemplateRepository.findByCompanyId(companyId); | ||
24 | + } | ||
25 | + | ||
26 | + public List<PromptTemplate> getSystemTemplates() { | ||
27 | + return promptTemplateRepository.findByCompanyIdIsNull(); | ||
28 | + } | ||
29 | + | ||
30 | + public List<PromptTemplate> getActiveTemplates() { | ||
31 | + return promptTemplateRepository.findByIsActiveTrue(); | ||
32 | + } | ||
33 | + | ||
34 | + public List<PromptTemplate> getActiveTemplatesByCompanyId(Integer companyId) { | ||
35 | + return promptTemplateRepository.findByCompanyIdAndIsActiveTrue(companyId); | ||
36 | + } | ||
37 | + | ||
38 | + public Optional<PromptTemplate> getTemplateById(Integer id) { | ||
39 | + return promptTemplateRepository.findById(id); | ||
40 | + } | ||
41 | + | ||
42 | + public PromptTemplate saveTemplate(PromptTemplate template) { | ||
43 | + return promptTemplateRepository.save(template); | ||
44 | + } | ||
45 | + | ||
46 | + public void deleteTemplate(Integer id) { | ||
47 | + promptTemplateRepository.deleteById(id); | ||
48 | + } | ||
49 | +} |
1 | +// ai/service/UploadedFileService.java | ||
2 | +package com.aigeo.ai.service; | ||
3 | + | ||
4 | +import com.aigeo.ai.entity.UploadedFile; | ||
5 | +import com.aigeo.common.enums.FileStatus; | ||
6 | +import com.aigeo.common.enums.FileType; | ||
7 | +import com.aigeo.ai.repository.UploadedFileRepository; | ||
8 | +import org.springframework.beans.factory.annotation.Autowired; | ||
9 | +import org.springframework.stereotype.Service; | ||
10 | + | ||
11 | +import java.util.List; | ||
12 | +import java.util.Optional; | ||
13 | + | ||
14 | +@Service | ||
15 | +public class UploadedFileService { | ||
16 | + | ||
17 | + @Autowired | ||
18 | + private UploadedFileRepository uploadedFileRepository; | ||
19 | + | ||
20 | + public List<UploadedFile> getAllFiles() { | ||
21 | + return uploadedFileRepository.findAll(); | ||
22 | + } | ||
23 | + | ||
24 | + public List<UploadedFile> getFilesByCompanyId(Integer companyId) { | ||
25 | + return uploadedFileRepository.findByCompanyId(companyId); | ||
26 | + } | ||
27 | + | ||
28 | + public List<UploadedFile> getFilesByUserId(Integer userId) { | ||
29 | + return uploadedFileRepository.findByUserId(userId); | ||
30 | + } | ||
31 | + | ||
32 | + public List<UploadedFile> getFilesByCompanyIdAndUserId(Integer companyId, Integer userId) { | ||
33 | + return uploadedFileRepository.findByCompanyIdAndUserId(companyId, userId); | ||
34 | + } | ||
35 | + | ||
36 | + public List<UploadedFile> getFilesByFileType(FileType fileType) { | ||
37 | + return uploadedFileRepository.findByFileType(fileType); | ||
38 | + } | ||
39 | + | ||
40 | + public List<UploadedFile> getFilesByStatus(FileStatus status) { | ||
41 | + return uploadedFileRepository.findByStatus(status); | ||
42 | + } | ||
43 | + | ||
44 | + public List<UploadedFile> getFilesByCompanyIdAndStatus(Integer companyId, FileStatus status) { | ||
45 | + return uploadedFileRepository.findByCompanyIdAndStatus(companyId, status); | ||
46 | + } | ||
47 | + | ||
48 | + public Optional<UploadedFile> getFileById(Integer id) { | ||
49 | + return uploadedFileRepository.findById(id); | ||
50 | + } | ||
51 | + | ||
52 | + public UploadedFile saveFile(UploadedFile file) { | ||
53 | + return uploadedFileRepository.save(file); | ||
54 | + } | ||
55 | + | ||
56 | + public void deleteFile(Integer id) { | ||
57 | + uploadedFileRepository.deleteById(id); | ||
58 | + } | ||
59 | +} |
1 | +package com.aigeo.article.controller; | ||
2 | + | ||
3 | +import com.aigeo.article.entity.ArticleGenerationTask; | ||
4 | +import com.aigeo.article.service.ArticleGenerationService; | ||
5 | +import com.aigeo.common.enums.TaskStatus; | ||
6 | +import com.aigeo.common.result.Result; | ||
7 | +import com.aigeo.common.result.ResultCode; | ||
8 | +import io.swagger.v3.oas.annotations.Operation; | ||
9 | +import io.swagger.v3.oas.annotations.Parameter; | ||
10 | +import io.swagger.v3.oas.annotations.responses.ApiResponse; | ||
11 | +import io.swagger.v3.oas.annotations.responses.ApiResponses; | ||
12 | +import io.swagger.v3.oas.annotations.tags.Tag; | ||
13 | +import jakarta.validation.Valid; | ||
14 | +import jakarta.validation.constraints.NotNull; | ||
15 | +import lombok.RequiredArgsConstructor; | ||
16 | +import lombok.extern.slf4j.Slf4j; | ||
17 | +import org.springframework.data.domain.Page; | ||
18 | +import org.springframework.data.domain.PageRequest; | ||
19 | +import org.springframework.data.domain.Pageable; | ||
20 | +import org.springframework.data.domain.Sort; | ||
21 | +import org.springframework.web.bind.annotation.*; | ||
22 | + | ||
23 | +import java.time.LocalDateTime; | ||
24 | +import java.util.List; | ||
25 | + | ||
26 | +/** | ||
27 | + * 文章生成任务管理控制器 | ||
28 | + * | ||
29 | + * @author AIGEO Team | ||
30 | + * @since 1.0.0 | ||
31 | + */ | ||
32 | +@Tag(name = "文章生成任务管理", description = "AI文章生成任务管理接口,支持任务的创建、查询、更新和删除操作") | ||
33 | +@RestController | ||
34 | +@RequestMapping("/api/article-tasks") | ||
35 | +@RequiredArgsConstructor | ||
36 | +@Slf4j | ||
37 | +public class ArticleGenerationTaskController { | ||
38 | + | ||
39 | + private final ArticleGenerationService articleGenerationService; | ||
40 | + | ||
41 | + @Operation( | ||
42 | + summary = "分页查询文章生成任务列表", | ||
43 | + description = "支持按任务状态、用户、公司等条件分页查询文章生成任务列表,默认按创建时间倒序排列" | ||
44 | + ) | ||
45 | + @ApiResponses(value = { | ||
46 | + @ApiResponse(responseCode = "200", description = "查询成功"), | ||
47 | + @ApiResponse(responseCode = "400", description = "请求参数错误"), | ||
48 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
49 | + }) | ||
50 | + @GetMapping("/list") | ||
51 | + public Result<Page<ArticleGenerationTask>> listTasks( | ||
52 | + @Parameter(description = "页码,从0开始", example = "0") | ||
53 | + @RequestParam(defaultValue = "0") int page, | ||
54 | + | ||
55 | + @Parameter(description = "页大小,默认10条", example = "10") | ||
56 | + @RequestParam(defaultValue = "10") int size, | ||
57 | + | ||
58 | + @Parameter(description = "公司ID(可选)") | ||
59 | + @RequestParam(required = false) Integer companyId, | ||
60 | + | ||
61 | + @Parameter(description = "用户ID(可选)") | ||
62 | + @RequestParam(required = false) Integer userId, | ||
63 | + | ||
64 | + @Parameter(description = "任务状态") | ||
65 | + @RequestParam(required = false) TaskStatus status, | ||
66 | + | ||
67 | + @Parameter(description = "文章主题(模糊查询)") | ||
68 | + @RequestParam(required = false) String articleTheme, | ||
69 | + | ||
70 | + @Parameter(description = "开始时间(格式:yyyy-MM-ddTHH:mm:ss)") | ||
71 | + @RequestParam(required = false) String startTime, | ||
72 | + | ||
73 | + @Parameter(description = "结束时间(格式:yyyy-MM-ddTHH:mm:ss)") | ||
74 | + @RequestParam(required = false) String endTime | ||
75 | + ) { | ||
76 | + try { | ||
77 | + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); | ||
78 | + LocalDateTime start = startTime != null ? LocalDateTime.parse(startTime) : null; | ||
79 | + LocalDateTime end = endTime != null ? LocalDateTime.parse(endTime) : null; | ||
80 | + | ||
81 | + Page<ArticleGenerationTask> tasks = articleGenerationService.searchTasks( | ||
82 | + companyId, userId, status, articleTheme, start, end, pageable | ||
83 | + ); | ||
84 | + return Result.success("查询成功", tasks); | ||
85 | + } catch (Exception e) { | ||
86 | + log.error("分页查询文章生成任务列表失败", e); | ||
87 | + return Result.error("查询失败"); | ||
88 | + } | ||
89 | + } | ||
90 | + | ||
91 | + @Operation( | ||
92 | + summary = "获取所有文章生成任务(简化版)", | ||
93 | + description = "获取所有文章生成任务的基本信息,用于下拉选择等场景" | ||
94 | + ) | ||
95 | + @ApiResponses(value = { | ||
96 | + @ApiResponse(responseCode = "200", description = "查询成功"), | ||
97 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
98 | + }) | ||
99 | + @GetMapping | ||
100 | + public Result<List<ArticleGenerationTask>> getAllTasks() { | ||
101 | + try { | ||
102 | + List<ArticleGenerationTask> tasks = articleGenerationService.getAllTasks(); | ||
103 | + return Result.success("查询成功", tasks); | ||
104 | + } catch (Exception e) { | ||
105 | + log.error("获取所有文章生成任务失败", e); | ||
106 | + return Result.error("查询失败"); | ||
107 | + } | ||
108 | + } | ||
109 | + | ||
110 | + @Operation( | ||
111 | + summary = "根据ID查询文章生成任务详情", | ||
112 | + description = "通过任务ID获取文章生成任务的详细信息" | ||
113 | + ) | ||
114 | + @ApiResponses(value = { | ||
115 | + @ApiResponse(responseCode = "200", description = "查询成功"), | ||
116 | + @ApiResponse(responseCode = "404", description = "任务不存在"), | ||
117 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
118 | + }) | ||
119 | + @GetMapping("/{id}") | ||
120 | + public Result<ArticleGenerationTask> getTaskById( | ||
121 | + @Parameter(description = "任务ID", required = true, example = "1") | ||
122 | + @PathVariable @NotNull Integer id | ||
123 | + ) { | ||
124 | + try { | ||
125 | + return articleGenerationService.getTaskById(id) | ||
126 | + .map(task -> Result.success("查询成功", task)) | ||
127 | + .orElse(Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在")); | ||
128 | + } catch (Exception e) { | ||
129 | + log.error("根据ID查询文章生成任务详情失败, id: {}", id, e); | ||
130 | + return Result.error("查询失败"); | ||
131 | + } | ||
132 | + } | ||
133 | + | ||
134 | + @Operation( | ||
135 | + summary = "根据公司ID查询文章生成任务", | ||
136 | + description = "获取指定公司下的所有文章生成任务" | ||
137 | + ) | ||
138 | + @ApiResponses(value = { | ||
139 | + @ApiResponse(responseCode = "200", description = "查询成功"), | ||
140 | + @ApiResponse(responseCode = "404", description = "公司不存在"), | ||
141 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
142 | + }) | ||
143 | + @GetMapping("/company/{companyId}") | ||
144 | + public Result<List<ArticleGenerationTask>> getTasksByCompanyId( | ||
145 | + @Parameter(description = "公司ID", required = true, example = "1") | ||
146 | + @PathVariable @NotNull Integer companyId | ||
147 | + ) { | ||
148 | + try { | ||
149 | + List<ArticleGenerationTask> tasks = articleGenerationService.getTasksByCompanyId(companyId); | ||
150 | + return Result.success("查询成功", tasks); | ||
151 | + } catch (Exception e) { | ||
152 | + log.error("根据公司ID查询文章生成任务失败, companyId: {}", companyId, e); | ||
153 | + return Result.error("查询失败"); | ||
154 | + } | ||
155 | + } | ||
156 | + | ||
157 | + @Operation( | ||
158 | + summary = "根据用户ID查询文章生成任务", | ||
159 | + description = "获取指定用户创建的所有文章生成任务" | ||
160 | + ) | ||
161 | + @ApiResponses(value = { | ||
162 | + @ApiResponse(responseCode = "200", description = "查询成功"), | ||
163 | + @ApiResponse(responseCode = "404", description = "用户不存在"), | ||
164 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
165 | + }) | ||
166 | + @GetMapping("/user/{userId}") | ||
167 | + public Result<List<ArticleGenerationTask>> getTasksByUserId( | ||
168 | + @Parameter(description = "用户ID", required = true, example = "1") | ||
169 | + @PathVariable @NotNull Integer userId | ||
170 | + ) { | ||
171 | + try { | ||
172 | + List<ArticleGenerationTask> tasks = articleGenerationService.getTasksByUserId(userId); | ||
173 | + return Result.success("查询成功", tasks); | ||
174 | + } catch (Exception e) { | ||
175 | + log.error("根据用户ID查询文章生成任务失败, userId: {}", userId, e); | ||
176 | + return Result.error("查询失败"); | ||
177 | + } | ||
178 | + } | ||
179 | + | ||
180 | + @Operation( | ||
181 | + summary = "根据任务状态查询文章生成任务", | ||
182 | + description = "获取指定状态的所有文章生成任务" | ||
183 | + ) | ||
184 | + @ApiResponses(value = { | ||
185 | + @ApiResponse(responseCode = "200", description = "查询成功"), | ||
186 | + @ApiResponse(responseCode = "400", description = "请求参数错误"), | ||
187 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
188 | + }) | ||
189 | + @GetMapping("/status/{status}") | ||
190 | + public Result<List<ArticleGenerationTask>> getTasksByStatus( | ||
191 | + @Parameter(description = "任务状态", required = true) | ||
192 | + @PathVariable @NotNull TaskStatus status | ||
193 | + ) { | ||
194 | + try { | ||
195 | + List<ArticleGenerationTask> tasks = articleGenerationService.getTasksByStatus(status); | ||
196 | + return Result.success("查询成功", tasks); | ||
197 | + } catch (Exception e) { | ||
198 | + log.error("根据任务状态查询文章生成任务失败, status: {}", status, e); | ||
199 | + return Result.error("查询失败"); | ||
200 | + } | ||
201 | + } | ||
202 | + | ||
203 | + @Operation( | ||
204 | + summary = "获取进行中的任务", | ||
205 | + description = "获取所有状态为进行中的文章生成任务" | ||
206 | + ) | ||
207 | + @ApiResponses(value = { | ||
208 | + @ApiResponse(responseCode = "200", description = "查询成功"), | ||
209 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
210 | + }) | ||
211 | + @GetMapping("/running") | ||
212 | + public Result<List<ArticleGenerationTask>> getRunningTasks( | ||
213 | + @Parameter(description = "公司ID(可选)") | ||
214 | + @RequestParam(required = false) Integer companyId | ||
215 | + ) { | ||
216 | + try { | ||
217 | + List<ArticleGenerationTask> tasks = articleGenerationService.getRunningTasks(companyId); | ||
218 | + return Result.success("查询成功", tasks); | ||
219 | + } catch (Exception e) { | ||
220 | + log.error("获取进行中的任务失败", e); | ||
221 | + return Result.error("查询失败"); | ||
222 | + } | ||
223 | + } | ||
224 | + | ||
225 | + @Operation( | ||
226 | + summary = "获取最近完成的任务", | ||
227 | + description = "获取最近完成的文章生成任务列表" | ||
228 | + ) | ||
229 | + @ApiResponses(value = { | ||
230 | + @ApiResponse(responseCode = "200", description = "查询成功"), | ||
231 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
232 | + }) | ||
233 | + @GetMapping("/recent-completed") | ||
234 | + public Result<List<ArticleGenerationTask>> getRecentCompletedTasks( | ||
235 | + @Parameter(description = "公司ID(可选)") | ||
236 | + @RequestParam(required = false) Integer companyId, | ||
237 | + | ||
238 | + @Parameter(description = "返回数量", example = "10") | ||
239 | + @RequestParam(defaultValue = "10") int limit | ||
240 | + ) { | ||
241 | + try { | ||
242 | + List<ArticleGenerationTask> tasks = articleGenerationService.getRecentCompletedTasks(companyId, limit); | ||
243 | + return Result.success("查询成功", tasks); | ||
244 | + } catch (Exception e) { | ||
245 | + log.error("获取最近完成的任务失败", e); | ||
246 | + return Result.error("查询失败"); | ||
247 | + } | ||
248 | + } | ||
249 | + | ||
250 | + @Operation( | ||
251 | + summary = "创建新的文章生成任务", | ||
252 | + description = "创建一个新的文章生成任务,包含生成参数和配置" | ||
253 | + ) | ||
254 | + @ApiResponses(value = { | ||
255 | + @ApiResponse(responseCode = "200", description = "创建成功"), | ||
256 | + @ApiResponse(responseCode = "400", description = "请求参数错误"), | ||
257 | + @ApiResponse(responseCode = "429", description = "任务队列已满"), | ||
258 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
259 | + }) | ||
260 | + @PostMapping | ||
261 | + public Result<ArticleGenerationTask> createTask( | ||
262 | + @Parameter(description = "文章生成任务信息", required = true) | ||
263 | + @Valid @RequestBody ArticleGenerationTask task | ||
264 | + ) { | ||
265 | + try { | ||
266 | + // 检查任务队列是否已满 | ||
267 | + if (articleGenerationService.isTaskQueueFull(task.getCompanyId())) { | ||
268 | + return Result.error(ResultCode.TOO_MANY_REQUESTS, "任务队列已满,请稍后再试"); | ||
269 | + } | ||
270 | + | ||
271 | + ArticleGenerationTask savedTask = articleGenerationService.saveTask(task); | ||
272 | + log.info("成功创建文章生成任务: {} (用户ID: {})", savedTask.getArticleTheme(), savedTask.getUserId()); | ||
273 | + return Result.success("创建成功", savedTask); | ||
274 | + } catch (Exception e) { | ||
275 | + log.error("创建文章生成任务失败", e); | ||
276 | + return Result.error("创建失败"); | ||
277 | + } | ||
278 | + } | ||
279 | + | ||
280 | + @Operation( | ||
281 | + summary = "批量创建文章生成任务", | ||
282 | + description = "批量创建多个文章生成任务" | ||
283 | + ) | ||
284 | + @ApiResponses(value = { | ||
285 | + @ApiResponse(responseCode = "200", description = "批量创建成功"), | ||
286 | + @ApiResponse(responseCode = "400", description = "请求参数错误"), | ||
287 | + @ApiResponse(responseCode = "429", description = "任务数量超限"), | ||
288 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
289 | + }) | ||
290 | + @PostMapping("/batch") | ||
291 | + public Result<List<ArticleGenerationTask>> batchCreateTasks( | ||
292 | + @Parameter(description = "文章生成任务列表", required = true) | ||
293 | + @Valid @RequestBody List<ArticleGenerationTask> tasks | ||
294 | + ) { | ||
295 | + try { | ||
296 | + if (tasks.size() > 50) { | ||
297 | + return Result.error(ResultCode.BAD_REQUEST, "单次批量创建任务数量不能超过50个"); | ||
298 | + } | ||
299 | + | ||
300 | + List<ArticleGenerationTask> savedTasks = articleGenerationService.batchSaveTasks(tasks); | ||
301 | + log.info("成功批量创建文章生成任务,数量: {}", savedTasks.size()); | ||
302 | + return Result.success("批量创建成功", savedTasks); | ||
303 | + } catch (Exception e) { | ||
304 | + log.error("批量创建文章生成任务失败", e); | ||
305 | + return Result.error("批量创建失败"); | ||
306 | + } | ||
307 | + } | ||
308 | + | ||
309 | + @Operation( | ||
310 | + summary = "更新文章生成任务", | ||
311 | + description = "根据ID更新文章生成任务的详细信息" | ||
312 | + ) | ||
313 | + @ApiResponses(value = { | ||
314 | + @ApiResponse(responseCode = "200", description = "更新成功"), | ||
315 | + @ApiResponse(responseCode = "400", description = "请求参数错误"), | ||
316 | + @ApiResponse(responseCode = "404", description = "任务不存在"), | ||
317 | + @ApiResponse(responseCode = "409", description = "任务状态不允许更新"), | ||
318 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
319 | + }) | ||
320 | + @PutMapping("/{id}") | ||
321 | + public Result<ArticleGenerationTask> updateTask( | ||
322 | + @Parameter(description = "任务ID", required = true, example = "1") | ||
323 | + @PathVariable @NotNull Integer id, | ||
324 | + | ||
325 | + @Parameter(description = "更新的任务信息", required = true) | ||
326 | + @Valid @RequestBody ArticleGenerationTask taskDetails | ||
327 | + ) { | ||
328 | + try { | ||
329 | + return articleGenerationService.getTaskById(id) | ||
330 | + .map(existingTask -> { | ||
331 | + // 检查任务状态是否允许更新 | ||
332 | + if (existingTask.getStatus() == TaskStatus.COMPLETED) { | ||
333 | + return Result.<ArticleGenerationTask>error(ResultCode.CONFLICT, "已完成的任务不允许更新"); | ||
334 | + } | ||
335 | + | ||
336 | + // 更新字段 | ||
337 | + existingTask.setArticleTheme(taskDetails.getArticleTheme()); | ||
338 | + existingTask.setStatus(taskDetails.getStatus()); | ||
339 | + existingTask.setProgress(taskDetails.getProgress()); | ||
340 | + | ||
341 | + ArticleGenerationTask savedTask = articleGenerationService.saveTask(existingTask); | ||
342 | + log.info("成功更新文章生成任务: {}", savedTask.getArticleTheme()); | ||
343 | + return Result.success("更新成功", savedTask); | ||
344 | + }) | ||
345 | + .orElse(Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在")); | ||
346 | + } catch (Exception e) { | ||
347 | + log.error("更新文章生成任务失败, id: {}", id, e); | ||
348 | + return Result.error("更新失败"); | ||
349 | + } | ||
350 | + } | ||
351 | + | ||
352 | + @Operation( | ||
353 | + summary = "启动文章生成任务", | ||
354 | + description = "启动指定的文章生成任务" | ||
355 | + ) | ||
356 | + @ApiResponses(value = { | ||
357 | + @ApiResponse(responseCode = "200", description = "启动成功"), | ||
358 | + @ApiResponse(responseCode = "404", description = "任务不存在"), | ||
359 | + @ApiResponse(responseCode = "409", description = "任务状态不允许启动"), | ||
360 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
361 | + }) | ||
362 | + @PostMapping("/{id}/start") | ||
363 | + public Result<String> startTask( | ||
364 | + @Parameter(description = "任务ID", required = true, example = "1") | ||
365 | + @PathVariable @NotNull Integer id | ||
366 | + ) { | ||
367 | + try { | ||
368 | + if (!articleGenerationService.existsById(id)) { | ||
369 | + return Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在"); | ||
370 | + } | ||
371 | + | ||
372 | + boolean started = articleGenerationService.startTask(id); | ||
373 | + if (started) { | ||
374 | + log.info("成功启动文章生成任务, id: {}", id); | ||
375 | + return Result.success("任务启动成功"); | ||
376 | + } else { | ||
377 | + return Result.error(ResultCode.CONFLICT, "任务状态不允许启动"); | ||
378 | + } | ||
379 | + } catch (Exception e) { | ||
380 | + log.error("启动文章生成任务失败, id: {}", id, e); | ||
381 | + return Result.error("启动失败"); | ||
382 | + } | ||
383 | + } | ||
384 | + | ||
385 | + @Operation( | ||
386 | + summary = "暂停文章生成任务", | ||
387 | + description = "暂停正在运行的文章生成任务" | ||
388 | + ) | ||
389 | + @ApiResponses(value = { | ||
390 | + @ApiResponse(responseCode = "200", description = "暂停成功"), | ||
391 | + @ApiResponse(responseCode = "404", description = "任务不存在"), | ||
392 | + @ApiResponse(responseCode = "409", description = "任务状态不允许暂停"), | ||
393 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
394 | + }) | ||
395 | + @PostMapping("/{id}/pause") | ||
396 | + public Result<String> pauseTask( | ||
397 | + @Parameter(description = "任务ID", required = true, example = "1") | ||
398 | + @PathVariable @NotNull Integer id | ||
399 | + ) { | ||
400 | + try { | ||
401 | + if (!articleGenerationService.existsById(id)) { | ||
402 | + return Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在"); | ||
403 | + } | ||
404 | + | ||
405 | + boolean paused = articleGenerationService.pauseTask(id); | ||
406 | + if (paused) { | ||
407 | + log.info("成功暂停文章生成任务, id: {}", id); | ||
408 | + return Result.success("任务暂停成功"); | ||
409 | + } else { | ||
410 | + return Result.error(ResultCode.CONFLICT, "任务状态不允许暂停"); | ||
411 | + } | ||
412 | + } catch (Exception e) { | ||
413 | + log.error("暂停文章生成任务失败, id: {}", id, e); | ||
414 | + return Result.error("暂停失败"); | ||
415 | + } | ||
416 | + } | ||
417 | + | ||
418 | + @Operation( | ||
419 | + summary = "取消文章生成任务", | ||
420 | + description = "取消指定的文章生成任务" | ||
421 | + ) | ||
422 | + @ApiResponses(value = { | ||
423 | + @ApiResponse(responseCode = "200", description = "取消成功"), | ||
424 | + @ApiResponse(responseCode = "404", description = "任务不存在"), | ||
425 | + @ApiResponse(responseCode = "409", description = "任务状态不允许取消"), | ||
426 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
427 | + }) | ||
428 | + @PostMapping("/{id}/cancel") | ||
429 | + public Result<String> cancelTask( | ||
430 | + @Parameter(description = "任务ID", required = true, example = "1") | ||
431 | + @PathVariable @NotNull Integer id | ||
432 | + ) { | ||
433 | + try { | ||
434 | + if (!articleGenerationService.existsById(id)) { | ||
435 | + return Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在"); | ||
436 | + } | ||
437 | + | ||
438 | + boolean cancelled = articleGenerationService.cancelTask(id); | ||
439 | + if (cancelled) { | ||
440 | + log.info("成功取消文章生成任务, id: {}", id); | ||
441 | + return Result.success("任务取消成功"); | ||
442 | + } else { | ||
443 | + return Result.error(ResultCode.CONFLICT, "任务状态不允许取消"); | ||
444 | + } | ||
445 | + } catch (Exception e) { | ||
446 | + log.error("取消文章生成任务失败, id: {}", id, e); | ||
447 | + return Result.error("取消失败"); | ||
448 | + } | ||
449 | + } | ||
450 | + | ||
451 | + @Operation( | ||
452 | + summary = "重试失败的文章生成任务", | ||
453 | + description = "重新启动失败的文章生成任务" | ||
454 | + ) | ||
455 | + @ApiResponses(value = { | ||
456 | + @ApiResponse(responseCode = "200", description = "重试成功"), | ||
457 | + @ApiResponse(responseCode = "404", description = "任务不存在"), | ||
458 | + @ApiResponse(responseCode = "409", description = "任务状态不允许重试"), | ||
459 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
460 | + }) | ||
461 | + @PostMapping("/{id}/retry") | ||
462 | + public Result<String> retryTask( | ||
463 | + @Parameter(description = "任务ID", required = true, example = "1") | ||
464 | + @PathVariable @NotNull Integer id | ||
465 | + ) { | ||
466 | + try { | ||
467 | + if (!articleGenerationService.existsById(id)) { | ||
468 | + return Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在"); | ||
469 | + } | ||
470 | + | ||
471 | + boolean retried = articleGenerationService.retryTask(id); | ||
472 | + if (retried) { | ||
473 | + log.info("成功重试文章生成任务, id: {}", id); | ||
474 | + return Result.success("任务重试成功"); | ||
475 | + } else { | ||
476 | + return Result.error(ResultCode.CONFLICT, "任务状态不允许重试"); | ||
477 | + } | ||
478 | + } catch (Exception e) { | ||
479 | + log.error("重试文章生成任务失败, id: {}", id, e); | ||
480 | + return Result.error("重试失败"); | ||
481 | + } | ||
482 | + } | ||
483 | + | ||
484 | + @Operation( | ||
485 | + summary = "删除文章生成任务", | ||
486 | + description = "根据ID删除文章生成任务记录(软删除)" | ||
487 | + ) | ||
488 | + @ApiResponses(value = { | ||
489 | + @ApiResponse(responseCode = "200", description = "删除成功"), | ||
490 | + @ApiResponse(responseCode = "404", description = "任务不存在"), | ||
491 | + @ApiResponse(responseCode = "409", description = "任务正在运行,无法删除"), | ||
492 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
493 | + }) | ||
494 | + @DeleteMapping("/{id}") | ||
495 | + public Result<String> deleteTask( | ||
496 | + @Parameter(description = "任务ID", required = true, example = "1") | ||
497 | + @PathVariable @NotNull Integer id | ||
498 | + ) { | ||
499 | + try { | ||
500 | + if (!articleGenerationService.existsById(id)) { | ||
501 | + return Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在"); | ||
502 | + } | ||
503 | + | ||
504 | + // 检查任务是否正在运行 | ||
505 | + if (articleGenerationService.isTaskRunning(id)) { | ||
506 | + return Result.error(ResultCode.CONFLICT, "任务正在运行,无法删除"); | ||
507 | + } | ||
508 | + | ||
509 | + articleGenerationService.deleteTask(id); | ||
510 | + log.info("成功删除文章生成任务, id: {}", id); | ||
511 | + return Result.success("删除成功"); | ||
512 | + } catch (Exception e) { | ||
513 | + log.error("删除文章生成任务失败, id: {}", id, e); | ||
514 | + return Result.error("删除失败"); | ||
515 | + } | ||
516 | + } | ||
517 | + | ||
518 | + @Operation( | ||
519 | + summary = "获取任务执行日志", | ||
520 | + description = "获取文章生成任务的执行日志信息" | ||
521 | + ) | ||
522 | + @ApiResponses(value = { | ||
523 | + @ApiResponse(responseCode = "200", description = "查询成功"), | ||
524 | + @ApiResponse(responseCode = "404", description = "任务不存在"), | ||
525 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
526 | + }) | ||
527 | + @GetMapping("/{id}/logs") | ||
528 | + public Result<List<String>> getTaskLogs( | ||
529 | + @Parameter(description = "任务ID", required = true, example = "1") | ||
530 | + @PathVariable @NotNull Integer id, | ||
531 | + | ||
532 | + @Parameter(description = "日志级别(INFO/WARN/ERROR)") | ||
533 | + @RequestParam(required = false) String level, | ||
534 | + | ||
535 | + @Parameter(description = "最大返回行数", example = "100") | ||
536 | + @RequestParam(defaultValue = "100") int limit | ||
537 | + ) { | ||
538 | + try { | ||
539 | + if (!articleGenerationService.existsById(id)) { | ||
540 | + return Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在"); | ||
541 | + } | ||
542 | + | ||
543 | + List<String> logs = articleGenerationService.getTaskLogs(id, level, limit); | ||
544 | + return Result.success("查询成功", logs); | ||
545 | + } catch (Exception e) { | ||
546 | + log.error("获取任务执行日志失败, id: {}", id, e); | ||
547 | + return Result.error("查询失败"); | ||
548 | + } | ||
549 | + } | ||
550 | + | ||
551 | + @Operation( | ||
552 | + summary = "获取任务统计信息", | ||
553 | + description = "获取文章生成任务相关的统计数据" | ||
554 | + ) | ||
555 | + @ApiResponses(value = { | ||
556 | + @ApiResponse(responseCode = "200", description = "统计成功"), | ||
557 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
558 | + }) | ||
559 | + @GetMapping("/statistics") | ||
560 | + public Result<TaskStatistics> getTaskStatistics( | ||
561 | + @Parameter(description = "公司ID(可选)") | ||
562 | + @RequestParam(required = false) Integer companyId, | ||
563 | + | ||
564 | + @Parameter(description = "统计时间范围(天数)", example = "30") | ||
565 | + @RequestParam(defaultValue = "30") int days | ||
566 | + ) { | ||
567 | + try { | ||
568 | + ArticleGenerationService.TaskStatistics serviceStats = articleGenerationService.getTaskStatistics(companyId, days); | ||
569 | + TaskStatistics statistics = new TaskStatistics( | ||
570 | + serviceStats.getTotalTasks(), | ||
571 | + serviceStats.getPendingTasks(), | ||
572 | + serviceStats.getRunningTasks(), | ||
573 | + serviceStats.getCompletedTasks(), | ||
574 | + serviceStats.getFailedTasks(), | ||
575 | + serviceStats.getSuccessRate(), | ||
576 | + serviceStats.getAverageExecutionTime() | ||
577 | + ); | ||
578 | + return Result.success("统计成功", statistics); | ||
579 | + } catch (Exception e) { | ||
580 | + log.error("获取任务统计信息失败", e); | ||
581 | + return Result.error("统计失败"); | ||
582 | + } | ||
583 | + } | ||
584 | + | ||
585 | + @Operation( | ||
586 | + summary = "清理已完成的任务", | ||
587 | + description = "清理指定天数前完成的文章生成任务" | ||
588 | + ) | ||
589 | + @ApiResponses(value = { | ||
590 | + @ApiResponse(responseCode = "200", description = "清理成功"), | ||
591 | + @ApiResponse(responseCode = "400", description = "请求参数错误"), | ||
592 | + @ApiResponse(responseCode = "500", description = "服务器内部错误") | ||
593 | + }) | ||
594 | + @DeleteMapping("/cleanup") | ||
595 | + public Result<String> cleanupCompletedTasks( | ||
596 | + @Parameter(description = "保留天数", required = true, example = "30") | ||
597 | + @RequestParam @NotNull Integer retentionDays | ||
598 | + ) { | ||
599 | + try { | ||
600 | + if (retentionDays < 1) { | ||
601 | + return Result.error(ResultCode.BAD_REQUEST, "保留天数必须大于0"); | ||
602 | + } | ||
603 | + | ||
604 | + int cleanedCount = articleGenerationService.cleanupCompletedTasks(retentionDays); | ||
605 | + log.info("清理已完成的任务成功,清理数量: {}", cleanedCount); | ||
606 | + return Result.success(String.format("成功清理 %d 个已完成的任务", cleanedCount)); | ||
607 | + } catch (Exception e) { | ||
608 | + log.error("清理已完成的任务失败", e); | ||
609 | + return Result.error("清理失败"); | ||
610 | + } | ||
611 | + } | ||
612 | + | ||
613 | + /** | ||
614 | + * 任务统计信息DTO | ||
615 | + */ | ||
616 | + public static class TaskStatistics { | ||
617 | + private long totalTasks; | ||
618 | + private long pendingTasks; | ||
619 | + private long runningTasks; | ||
620 | + private long completedTasks; | ||
621 | + private long failedTasks; | ||
622 | + private double successRate; | ||
623 | + private double averageExecutionTime; | ||
624 | + | ||
625 | + // constructors, getters and setters | ||
626 | + public TaskStatistics(long totalTasks, long pendingTasks, long runningTasks, | ||
627 | + long completedTasks, long failedTasks, double successRate, | ||
628 | + double averageExecutionTime) { | ||
629 | + this.totalTasks = totalTasks; | ||
630 | + this.pendingTasks = pendingTasks; | ||
631 | + this.runningTasks = runningTasks; | ||
632 | + this.completedTasks = completedTasks; | ||
633 | + this.failedTasks = failedTasks; | ||
634 | + this.successRate = successRate; | ||
635 | + this.averageExecutionTime = averageExecutionTime; | ||
636 | + } | ||
637 | + | ||
638 | + // getters and setters | ||
639 | + public long getTotalTasks() { return totalTasks; } | ||
640 | + public void setTotalTasks(long totalTasks) { this.totalTasks = totalTasks; } | ||
641 | + | ||
642 | + public long getPendingTasks() { return pendingTasks; } | ||
643 | + public void setPendingTasks(long pendingTasks) { this.pendingTasks = pendingTasks; } | ||
644 | + | ||
645 | + public long getRunningTasks() { return runningTasks; } | ||
646 | + public void setRunningTasks(long runningTasks) { this.runningTasks = runningTasks; } | ||
647 | + | ||
648 | + public long getCompletedTasks() { return completedTasks; } | ||
649 | + public void setCompletedTasks(long completedTasks) { this.completedTasks = completedTasks; } | ||
650 | + | ||
651 | + public long getFailedTasks() { return failedTasks; } | ||
652 | + public void setFailedTasks(long failedTasks) { this.failedTasks = failedTasks; } | ||
653 | + | ||
654 | + public double getSuccessRate() { return successRate; } | ||
655 | + public void setSuccessRate(double successRate) { this.successRate = successRate; } | ||
656 | + | ||
657 | + public double getAverageExecutionTime() { return averageExecutionTime; } | ||
658 | + public void setAverageExecutionTime(double averageExecutionTime) { this.averageExecutionTime = averageExecutionTime; } | ||
659 | + } | ||
660 | +} |
1 | +package com.aigeo.article.entity; | ||
2 | + | ||
3 | +import com.aigeo.common.enums.AiTasteLevel; | ||
4 | +import com.fasterxml.jackson.annotation.JsonFormat; | ||
5 | +import lombok.AllArgsConstructor; | ||
6 | +import lombok.Builder; | ||
7 | +import lombok.Data; | ||
8 | +import lombok.NoArgsConstructor; | ||
9 | +import org.hibernate.annotations.CreationTimestamp; | ||
10 | +import org.hibernate.annotations.UpdateTimestamp; | ||
11 | + | ||
12 | +import jakarta.persistence.*; | ||
13 | +import jakarta.validation.constraints.NotBlank; | ||
14 | +import jakarta.validation.constraints.NotNull; | ||
15 | +import java.time.LocalDateTime; | ||
16 | + | ||
17 | +/** | ||
18 | + * 文章生成配置实体类 | ||
19 | + * 对应数据库表:ai_article_generation_configs | ||
20 | + * | ||
21 | + * 用于存储文章生成的各种配置参数,包括: | ||
22 | + * - AI写作风格和复杂度设置 | ||
23 | + * - 文章结构和长度配置 | ||
24 | + * - SEO优化参数设置 | ||
25 | + * - 输出格式和样式配置 | ||
26 | + * | ||
27 | + * @author AIGEO Team | ||
28 | + * @since 1.0.0 | ||
29 | + */ | ||
30 | +@Data | ||
31 | +@Entity | ||
32 | +@Builder | ||
33 | +@NoArgsConstructor | ||
34 | +@AllArgsConstructor | ||
35 | +@Table(name = "ai_article_generation_configs") | ||
36 | +public class ArticleGenerationConfig { | ||
37 | + | ||
38 | + /** | ||
39 | + * 主键ID | ||
40 | + */ | ||
41 | + @Id | ||
42 | + @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
43 | + @Column(name = "id", nullable = false, updatable = false) | ||
44 | + private Integer id; | ||
45 | + | ||
46 | + /** | ||
47 | + * 公司ID(多租户字段) | ||
48 | + */ | ||
49 | + @NotNull(message = "公司ID不能为空") | ||
50 | + @Column(name = "company_id", nullable = false) | ||
51 | + private Integer companyId; | ||
52 | + | ||
53 | + /** | ||
54 | + * 配置名称 | ||
55 | + */ | ||
56 | + @NotBlank(message = "配置名称不能为空") | ||
57 | + @Column(name = "config_name", nullable = false, length = 100) | ||
58 | + private String configName; | ||
59 | + | ||
60 | + /** | ||
61 | + * AI写作风格等级 | ||
62 | + */ | ||
63 | + @Enumerated(EnumType.STRING) | ||
64 | + @Column(name = "ai_taste_level") | ||
65 | + @Builder.Default | ||
66 | + private AiTasteLevel aiTasteLevel = AiTasteLevel.JUNIOR_HIGH; | ||
67 | + | ||
68 | + /** | ||
69 | + * 目标字数(范围的最小值) | ||
70 | + */ | ||
71 | + @Column(name = "target_word_count_min") | ||
72 | + @Builder.Default | ||
73 | + private Integer targetWordCountMin = 800; | ||
74 | + | ||
75 | + /** | ||
76 | + * 目标字数(范围的最大值) | ||
77 | + */ | ||
78 | + @Column(name = "target_word_count_max") | ||
79 | + @Builder.Default | ||
80 | + private Integer targetWordCountMax = 1500; | ||
81 | + | ||
82 | + /** | ||
83 | + * 是否包含FAQ部分 | ||
84 | + */ | ||
85 | + @Column(name = "include_faq") | ||
86 | + @Builder.Default | ||
87 | + private Boolean includeFaq = true; | ||
88 | + | ||
89 | + /** | ||
90 | + * 是否生成结构化数据(Schema.org) | ||
91 | + */ | ||
92 | + @Column(name = "generate_structured_data") | ||
93 | + @Builder.Default | ||
94 | + private Boolean generateStructuredData = true; | ||
95 | + | ||
96 | + /** | ||
97 | + * 是否包含相关链接 | ||
98 | + */ | ||
99 | + @Column(name = "include_related_links") | ||
100 | + @Builder.Default | ||
101 | + private Boolean includeRelatedLinks = true; | ||
102 | + | ||
103 | + /** | ||
104 | + * SEO标题模板 | ||
105 | + */ | ||
106 | + @Column(name = "seo_title_template", length = 500) | ||
107 | + private String seoTitleTemplate; | ||
108 | + | ||
109 | + /** | ||
110 | + * SEO描述模板 | ||
111 | + */ | ||
112 | + @Column(name = "seo_description_template", length = 500) | ||
113 | + private String seoDescriptionTemplate; | ||
114 | + | ||
115 | + /** | ||
116 | + * 文章标签模板(用逗号分隔) | ||
117 | + */ | ||
118 | + @Column(name = "article_tags_template", length = 500) | ||
119 | + private String articleTagsTemplate; | ||
120 | + | ||
121 | + /** | ||
122 | + * 自定义提示词 | ||
123 | + */ | ||
124 | + @Column(name = "custom_prompt", columnDefinition = "TEXT") | ||
125 | + private String customPrompt; | ||
126 | + | ||
127 | + /** | ||
128 | + * 语言设置 | ||
129 | + */ | ||
130 | + @Column(name = "language", length = 10) | ||
131 | + @Builder.Default | ||
132 | + private String language = "zh-CN"; | ||
133 | + | ||
134 | + /** | ||
135 | + * 输出格式(markdown, html等) | ||
136 | + */ | ||
137 | + @Column(name = "output_format", length = 20) | ||
138 | + @Builder.Default | ||
139 | + private String outputFormat = "markdown"; | ||
140 | + | ||
141 | + /** | ||
142 | + * 是否为默认配置 | ||
143 | + */ | ||
144 | + @Column(name = "is_default") | ||
145 | + @Builder.Default | ||
146 | + private Boolean isDefault = false; | ||
147 | + | ||
148 | + /** | ||
149 | + * 是否启用 | ||
150 | + */ | ||
151 | + @Column(name = "is_active") | ||
152 | + @Builder.Default | ||
153 | + private Boolean isActive = true; | ||
154 | + | ||
155 | + /** | ||
156 | + * 配置描述 | ||
157 | + */ | ||
158 | + @Column(name = "description") | ||
159 | + private String description; | ||
160 | + | ||
161 | + /** | ||
162 | + * 创建时间 | ||
163 | + */ | ||
164 | + @CreationTimestamp | ||
165 | + @Column(name = "created_at", updatable = false) | ||
166 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
167 | + private LocalDateTime createdAt; | ||
168 | + | ||
169 | + /** | ||
170 | + * 更新时间 | ||
171 | + */ | ||
172 | + @UpdateTimestamp | ||
173 | + @Column(name = "updated_at") | ||
174 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
175 | + private LocalDateTime updatedAt; | ||
176 | + | ||
177 | + /** | ||
178 | + * 实体创建前的处理 | ||
179 | + */ | ||
180 | + @PrePersist | ||
181 | + protected void onCreate() { | ||
182 | + if (isActive == null) isActive = true; | ||
183 | + if (isDefault == null) isDefault = false; | ||
184 | + if (includeFaq == null) includeFaq = true; | ||
185 | + if (generateStructuredData == null) generateStructuredData = true; | ||
186 | + if (includeRelatedLinks == null) includeRelatedLinks = true; | ||
187 | + if (aiTasteLevel == null) aiTasteLevel = AiTasteLevel.JUNIOR_HIGH; | ||
188 | + if (language == null) language = "zh-CN"; | ||
189 | + if (outputFormat == null) outputFormat = "markdown"; | ||
190 | + if (targetWordCountMin == null) targetWordCountMin = 800; | ||
191 | + if (targetWordCountMax == null) targetWordCountMax = 1500; | ||
192 | + } | ||
193 | + | ||
194 | + /** | ||
195 | + * 获取目标字数范围描述 | ||
196 | + */ | ||
197 | + public String getWordCountRange() { | ||
198 | + return targetWordCountMin + "-" + targetWordCountMax + "字"; | ||
199 | + } | ||
200 | + | ||
201 | + /** | ||
202 | + * 检查是否为完整配置(包含所有必要参数) | ||
203 | + */ | ||
204 | + public boolean isComplete() { | ||
205 | + return configName != null && !configName.trim().isEmpty() | ||
206 | + && aiTasteLevel != null | ||
207 | + && targetWordCountMin != null && targetWordCountMin > 0 | ||
208 | + && targetWordCountMax != null && targetWordCountMax > targetWordCountMin; | ||
209 | + } | ||
210 | + | ||
211 | + /** | ||
212 | + * 获取配置的复杂度评分(1-5分) | ||
213 | + */ | ||
214 | + public int getComplexityScore() { | ||
215 | + int score = aiTasteLevel != null ? aiTasteLevel.getComplexityLevel() : 2; | ||
216 | + | ||
217 | + // 根据配置的复杂程度调整评分 | ||
218 | + if (Boolean.TRUE.equals(generateStructuredData)) score += 1; | ||
219 | + if (Boolean.TRUE.equals(includeFaq)) score += 1; | ||
220 | + if (customPrompt != null && !customPrompt.trim().isEmpty()) score += 1; | ||
221 | + | ||
222 | + return Math.min(score, 5); // 最高5分 | ||
223 | + } | ||
224 | +} |
1 | +package com.aigeo.article.entity; | ||
2 | + | ||
3 | +import com.aigeo.common.enums.TaskStatus; | ||
4 | +import com.aigeo.common.enums.AiTasteLevel; | ||
5 | +import com.fasterxml.jackson.annotation.JsonFormat; | ||
6 | +import lombok.AllArgsConstructor; | ||
7 | +import lombok.Builder; | ||
8 | +import lombok.Data; | ||
9 | +import lombok.NoArgsConstructor; | ||
10 | +import org.hibernate.annotations.CreationTimestamp; | ||
11 | +import org.hibernate.annotations.UpdateTimestamp; | ||
12 | + | ||
13 | +import jakarta.persistence.*; | ||
14 | +import jakarta.validation.constraints.NotBlank; | ||
15 | +import jakarta.validation.constraints.NotNull; | ||
16 | +import java.time.LocalDateTime; | ||
17 | + | ||
18 | +/** | ||
19 | + * 文章生成任务实体类 | ||
20 | + * 对应数据库表:ai_article_generation_tasks | ||
21 | + * | ||
22 | + * 用于存储AI文章生成任务信息,包括: | ||
23 | + * - 任务配置和参数设置 | ||
24 | + * - 执行状态和进度跟踪 | ||
25 | + * - 参考资料和知识来源 | ||
26 | + * - 生成结果和错误信息 | ||
27 | + * | ||
28 | + * @author AIGEO Team | ||
29 | + * @since 1.0.0 | ||
30 | + */ | ||
31 | +@Data | ||
32 | +@Entity | ||
33 | +@Builder | ||
34 | +@NoArgsConstructor | ||
35 | +@AllArgsConstructor | ||
36 | +@Table(name = "ai_article_generation_tasks", indexes = { | ||
37 | + @Index(name = "idx_tasks_company_status", columnList = "company_id, status"), | ||
38 | + @Index(name = "idx_tasks_user_id", columnList = "user_id"), | ||
39 | + @Index(name = "idx_tasks_created_at", columnList = "created_at DESC") | ||
40 | +}) | ||
41 | +public class ArticleGenerationTask { | ||
42 | + | ||
43 | + /** | ||
44 | + * 主键ID | ||
45 | + */ | ||
46 | + @Id | ||
47 | + @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
48 | + @Column(name = "id", nullable = false, updatable = false) | ||
49 | + private Integer id; | ||
50 | + | ||
51 | + /** | ||
52 | + * 公司ID(多租户字段) | ||
53 | + */ | ||
54 | + @NotNull(message = "公司ID不能为空") | ||
55 | + @Column(name = "company_id", nullable = false) | ||
56 | + private Integer companyId; | ||
57 | + | ||
58 | + /** | ||
59 | + * 创建用户ID | ||
60 | + */ | ||
61 | + @NotNull(message = "用户ID不能为空") | ||
62 | + @Column(name = "user_id", nullable = false) | ||
63 | + private Integer userId; | ||
64 | + | ||
65 | + /** | ||
66 | + * 生成配置ID | ||
67 | + */ | ||
68 | + @Column(name = "config_id") | ||
69 | + private Integer configId; | ||
70 | + | ||
71 | + /** | ||
72 | + * 关键词ID | ||
73 | + */ | ||
74 | + @Column(name = "keyword_id") | ||
75 | + private Integer keywordId; | ||
76 | + | ||
77 | + /** | ||
78 | + * 文章主题 | ||
79 | + */ | ||
80 | + @NotBlank(message = "文章主题不能为空") | ||
81 | + @Column(name = "article_theme", nullable = false, length = 500) | ||
82 | + private String articleTheme; | ||
83 | + | ||
84 | + /** | ||
85 | + * 关联话题ID列表(逗号分隔) | ||
86 | + */ | ||
87 | + @Column(name = "topic_ids", length = 500) | ||
88 | + private String topicIds; | ||
89 | + | ||
90 | + /** | ||
91 | + * 参考资料URL列表(逗号分隔) | ||
92 | + */ | ||
93 | + @Column(name = "reference_urls", columnDefinition = "TEXT") | ||
94 | + private String referenceUrls; | ||
95 | + | ||
96 | + /** | ||
97 | + * 参考内容 | ||
98 | + */ | ||
99 | + @Column(name = "reference_content", columnDefinition = "LONGTEXT") | ||
100 | + private String referenceContent; | ||
101 | + | ||
102 | + /** | ||
103 | + * AI写作风格等级 | ||
104 | + */ | ||
105 | + @Enumerated(EnumType.STRING) | ||
106 | + @Column(name = "ai_taste_level") | ||
107 | + @Builder.Default | ||
108 | + private AiTasteLevel aiTasteLevel = AiTasteLevel.JUNIOR_HIGH; | ||
109 | + | ||
110 | + /** | ||
111 | + * 任务状态 | ||
112 | + */ | ||
113 | + @Enumerated(EnumType.STRING) | ||
114 | + @Column(name = "status") | ||
115 | + @Builder.Default | ||
116 | + private TaskStatus status = TaskStatus.PENDING; | ||
117 | + | ||
118 | + /** | ||
119 | + * 任务进度(0-100) | ||
120 | + */ | ||
121 | + @Column(name = "progress") | ||
122 | + @Builder.Default | ||
123 | + private Integer progress = 0; | ||
124 | + | ||
125 | + /** | ||
126 | + * 错误信息 | ||
127 | + */ | ||
128 | + @Column(name = "error_message", length = 2000) | ||
129 | + private String errorMessage; | ||
130 | + | ||
131 | + /** | ||
132 | + * 生成的文章ID | ||
133 | + */ | ||
134 | + @Column(name = "generated_article_id") | ||
135 | + private Integer generatedArticleId; | ||
136 | + | ||
137 | + /** | ||
138 | + * AI配置ID | ||
139 | + */ | ||
140 | + @Column(name = "dify_api_config_id") | ||
141 | + private Integer difyApiConfigId; | ||
142 | + | ||
143 | + /** | ||
144 | + * 提示模板ID | ||
145 | + */ | ||
146 | + @Column(name = "prompt_template_id") | ||
147 | + private Integer promptTemplateId; | ||
148 | + | ||
149 | + /** | ||
150 | + * 目标字数 | ||
151 | + */ | ||
152 | + @Column(name = "target_word_count") | ||
153 | + @Builder.Default | ||
154 | + private Integer targetWordCount = 1000; | ||
155 | + | ||
156 | + /** | ||
157 | + * 生成语言 | ||
158 | + */ | ||
159 | + @Column(name = "language", length = 10) | ||
160 | + @Builder.Default | ||
161 | + private String language = "zh-CN"; | ||
162 | + | ||
163 | + /** | ||
164 | + * 是否包含FAQ | ||
165 | + */ | ||
166 | + @Column(name = "include_faq") | ||
167 | + @Builder.Default | ||
168 | + private Boolean includeFaq = false; | ||
169 | + | ||
170 | + /** | ||
171 | + * 是否生成SEO数据 | ||
172 | + */ | ||
173 | + @Column(name = "generate_seo") | ||
174 | + @Builder.Default | ||
175 | + private Boolean generateSeo = true; | ||
176 | + | ||
177 | + /** | ||
178 | + * 执行时长(毫秒) | ||
179 | + */ | ||
180 | + @Column(name = "execution_time_ms") | ||
181 | + private Long executionTimeMs; | ||
182 | + | ||
183 | + /** | ||
184 | + * 重试次数 | ||
185 | + */ | ||
186 | + @Column(name = "retry_count") | ||
187 | + @Builder.Default | ||
188 | + private Integer retryCount = 0; | ||
189 | + | ||
190 | + /** | ||
191 | + * 最大重试次数 | ||
192 | + */ | ||
193 | + @Column(name = "max_retries") | ||
194 | + @Builder.Default | ||
195 | + private Integer maxRetries = 3; | ||
196 | + | ||
197 | + /** | ||
198 | + * 任务优先级(1-10,数字越大优先级越高) | ||
199 | + */ | ||
200 | + @Column(name = "priority") | ||
201 | + @Builder.Default | ||
202 | + private Integer priority = 5; | ||
203 | + | ||
204 | + /** | ||
205 | + * 创建时间 | ||
206 | + */ | ||
207 | + @CreationTimestamp | ||
208 | + @Column(name = "created_at", updatable = false) | ||
209 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
210 | + private LocalDateTime createdAt; | ||
211 | + | ||
212 | + /** | ||
213 | + * 开始执行时间 | ||
214 | + */ | ||
215 | + @Column(name = "started_at") | ||
216 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
217 | + private LocalDateTime startedAt; | ||
218 | + | ||
219 | + /** | ||
220 | + * 完成时间 | ||
221 | + */ | ||
222 | + @Column(name = "completed_at") | ||
223 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
224 | + private LocalDateTime completedAt; | ||
225 | + | ||
226 | + /** | ||
227 | + * 更新时间 | ||
228 | + */ | ||
229 | + @UpdateTimestamp | ||
230 | + @Column(name = "updated_at") | ||
231 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
232 | + private LocalDateTime updatedAt; | ||
233 | + | ||
234 | + /** | ||
235 | + * 实体创建前的处理 | ||
236 | + */ | ||
237 | + @PrePersist | ||
238 | + protected void onCreate() { | ||
239 | + if (status == null) status = TaskStatus.PENDING; | ||
240 | + if (progress == null) progress = 0; | ||
241 | + if (aiTasteLevel == null) aiTasteLevel = AiTasteLevel.JUNIOR_HIGH; | ||
242 | + if (targetWordCount == null) targetWordCount = 1000; | ||
243 | + if (language == null) language = "zh-CN"; | ||
244 | + if (includeFaq == null) includeFaq = false; | ||
245 | + if (generateSeo == null) generateSeo = true; | ||
246 | + if (retryCount == null) retryCount = 0; | ||
247 | + if (maxRetries == null) maxRetries = 3; | ||
248 | + if (priority == null) priority = 5; | ||
249 | + } | ||
250 | + | ||
251 | + /** | ||
252 | + * 检查任务是否已完成 | ||
253 | + */ | ||
254 | + public boolean isCompleted() { | ||
255 | + return status == TaskStatus.COMPLETED || status == TaskStatus.FAILED || status == TaskStatus.CANCELLED; | ||
256 | + } | ||
257 | + | ||
258 | + /** | ||
259 | + * 检查任务是否正在运行 | ||
260 | + */ | ||
261 | + public boolean isRunning() { | ||
262 | + return status == TaskStatus.PROCESSING; | ||
263 | + } | ||
264 | + | ||
265 | + /** | ||
266 | + * 检查是否可以重试 | ||
267 | + */ | ||
268 | + public boolean canRetry() { | ||
269 | + return status == TaskStatus.FAILED && retryCount < maxRetries; | ||
270 | + } | ||
271 | + | ||
272 | + /** | ||
273 | + * 检查是否可以取消 | ||
274 | + */ | ||
275 | + public boolean isCancellable() { | ||
276 | + return status == TaskStatus.PENDING || status == TaskStatus.PROCESSING; | ||
277 | + } | ||
278 | + | ||
279 | + /** | ||
280 | + * 开始任务执行 | ||
281 | + */ | ||
282 | + public void start() { | ||
283 | + this.status = TaskStatus.PROCESSING; | ||
284 | + this.startedAt = LocalDateTime.now(); | ||
285 | + this.progress = 0; | ||
286 | + } | ||
287 | + | ||
288 | + /** | ||
289 | + * 完成任务 | ||
290 | + */ | ||
291 | + public void complete(Integer articleId) { | ||
292 | + this.status = TaskStatus.COMPLETED; | ||
293 | + this.completedAt = LocalDateTime.now(); | ||
294 | + this.progress = 100; | ||
295 | + this.generatedArticleId = articleId; | ||
296 | + | ||
297 | + if (startedAt != null) { | ||
298 | + this.executionTimeMs = java.time.Duration.between(startedAt, completedAt).toMillis(); | ||
299 | + } | ||
300 | + } | ||
301 | + | ||
302 | + /** | ||
303 | + * 任务失败 | ||
304 | + */ | ||
305 | + public void fail(String errorMessage) { | ||
306 | + this.status = TaskStatus.FAILED; | ||
307 | + this.completedAt = LocalDateTime.now(); | ||
308 | + this.errorMessage = errorMessage; | ||
309 | + | ||
310 | + if (startedAt != null) { | ||
311 | + this.executionTimeMs = java.time.Duration.between(startedAt, completedAt).toMillis(); | ||
312 | + } | ||
313 | + } | ||
314 | + | ||
315 | + /** | ||
316 | + * 取消任务 | ||
317 | + */ | ||
318 | + public void cancel(String reason) { | ||
319 | + this.status = TaskStatus.CANCELLED; | ||
320 | + this.completedAt = LocalDateTime.now(); | ||
321 | + this.errorMessage = "任务已取消: " + reason; | ||
322 | + } | ||
323 | + | ||
324 | + /** | ||
325 | + * 增加重试次数 | ||
326 | + */ | ||
327 | + public void incrementRetryCount() { | ||
328 | + this.retryCount = (retryCount == null ? 0 : retryCount) + 1; | ||
329 | + } | ||
330 | + | ||
331 | + /** | ||
332 | + * 更新进度 | ||
333 | + */ | ||
334 | + public void updateProgress(Integer progress) { | ||
335 | + if (progress >= 0 && progress <= 100) { | ||
336 | + this.progress = progress; | ||
337 | + } | ||
338 | + } | ||
339 | + | ||
340 | + /** | ||
341 | + * 获取任务执行时长(秒) | ||
342 | + */ | ||
343 | + public Long getExecutionTimeSeconds() { | ||
344 | + if (executionTimeMs == null) return null; | ||
345 | + return executionTimeMs / 1000; | ||
346 | + } | ||
347 | + | ||
348 | + /** | ||
349 | + * 获取任务优先级描述 | ||
350 | + */ | ||
351 | + public String getPriorityDescription() { | ||
352 | + if (priority == null) return "普通"; | ||
353 | + | ||
354 | + if (priority <= 3) return "低"; | ||
355 | + if (priority <= 7) return "普通"; | ||
356 | + return "高"; | ||
357 | + } | ||
358 | +} |
1 | +package com.aigeo.article.entity; | ||
2 | + | ||
3 | +import com.fasterxml.jackson.annotation.JsonFormat; | ||
4 | +import lombok.AllArgsConstructor; | ||
5 | +import lombok.Builder; | ||
6 | +import lombok.Data; | ||
7 | +import lombok.NoArgsConstructor; | ||
8 | +import org.hibernate.annotations.CreationTimestamp; | ||
9 | +import org.hibernate.annotations.UpdateTimestamp; | ||
10 | + | ||
11 | +import jakarta.persistence.*; | ||
12 | +import jakarta.validation.constraints.NotBlank; | ||
13 | +import jakarta.validation.constraints.NotNull; | ||
14 | +import java.time.LocalDateTime; | ||
15 | + | ||
16 | +/** | ||
17 | + * 文章类型实体类 | ||
18 | + * 对应数据库表:ai_article_types | ||
19 | + * | ||
20 | + * 用于定义不同类型的文章分类,如: | ||
21 | + * - 新闻资讯类文章 | ||
22 | + * - 产品介绍类文章 | ||
23 | + * - 教程指南类文章 | ||
24 | + * - SEO优化类文章 | ||
25 | + * - 行业分析类文章 | ||
26 | + * | ||
27 | + * @author AIGEO Team | ||
28 | + * @since 1.0.0 | ||
29 | + */ | ||
30 | +@Data | ||
31 | +@Entity | ||
32 | +@Builder | ||
33 | +@NoArgsConstructor | ||
34 | +@AllArgsConstructor | ||
35 | +@Table(name = "ai_article_types", indexes = { | ||
36 | + @Index(name = "uk_article_types_company_name", columnList = "company_id, name", unique = true) | ||
37 | +}) | ||
38 | +public class ArticleType { | ||
39 | + | ||
40 | + /** | ||
41 | + * 主键ID | ||
42 | + */ | ||
43 | + @Id | ||
44 | + @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
45 | + @Column(name = "id", nullable = false, updatable = false) | ||
46 | + private Integer id; | ||
47 | + | ||
48 | + /** | ||
49 | + * 公司ID(多租户字段) | ||
50 | + */ | ||
51 | + @NotNull(message = "公司ID不能为空") | ||
52 | + @Column(name = "company_id", nullable = false) | ||
53 | + private Integer companyId; | ||
54 | + | ||
55 | + /** | ||
56 | + * 文章类型名称 | ||
57 | + */ | ||
58 | + @NotBlank(message = "文章类型名称不能为空") | ||
59 | + @Column(name = "name", nullable = false, length = 100) | ||
60 | + private String name; | ||
61 | + | ||
62 | + /** | ||
63 | + * 类型描述 | ||
64 | + */ | ||
65 | + @Column(name = "description", length = 500) | ||
66 | + private String description; | ||
67 | + | ||
68 | + /** | ||
69 | + * 类型标识代码(英文标识,用于程序识别) | ||
70 | + */ | ||
71 | + @Column(name = "code", length = 50) | ||
72 | + private String code; | ||
73 | + | ||
74 | + /** | ||
75 | + * 默认标题模板 | ||
76 | + */ | ||
77 | + @Column(name = "default_title_template", length = 500) | ||
78 | + private String defaultTitleTemplate; | ||
79 | + | ||
80 | + /** | ||
81 | + * 默认内容模板 | ||
82 | + */ | ||
83 | + @Column(name = "default_content_template", columnDefinition = "TEXT") | ||
84 | + private String defaultContentTemplate; | ||
85 | + | ||
86 | + /** | ||
87 | + * 默认标签(逗号分隔) | ||
88 | + */ | ||
89 | + @Column(name = "default_tags", length = 500) | ||
90 | + private String defaultTags; | ||
91 | + | ||
92 | + /** | ||
93 | + * 推荐字数范围(最小值) | ||
94 | + */ | ||
95 | + @Column(name = "recommended_word_count_min") | ||
96 | + @Builder.Default | ||
97 | + private Integer recommendedWordCountMin = 800; | ||
98 | + | ||
99 | + /** | ||
100 | + * 推荐字数范围(最大值) | ||
101 | + */ | ||
102 | + @Column(name = "recommended_word_count_max") | ||
103 | + @Builder.Default | ||
104 | + private Integer recommendedWordCountMax = 1500; | ||
105 | + | ||
106 | + /** | ||
107 | + * SEO权重(影响搜索引擎优化的重要性,1-10分) | ||
108 | + */ | ||
109 | + @Column(name = "seo_weight") | ||
110 | + @Builder.Default | ||
111 | + private Integer seoWeight = 5; | ||
112 | + | ||
113 | + /** | ||
114 | + * 是否需要FAQ部分 | ||
115 | + */ | ||
116 | + @Column(name = "requires_faq") | ||
117 | + @Builder.Default | ||
118 | + private Boolean requiresFaq = false; | ||
119 | + | ||
120 | + /** | ||
121 | + * 是否需要结构化数据 | ||
122 | + */ | ||
123 | + @Column(name = "requires_structured_data") | ||
124 | + @Builder.Default | ||
125 | + private Boolean requiresStructuredData = false; | ||
126 | + | ||
127 | + /** | ||
128 | + * 是否需要相关链接 | ||
129 | + */ | ||
130 | + @Column(name = "requires_related_links") | ||
131 | + @Builder.Default | ||
132 | + private Boolean requiresRelatedLinks = false; | ||
133 | + | ||
134 | + /** | ||
135 | + * 排序序号(数字越小排序越靠前) | ||
136 | + */ | ||
137 | + @Column(name = "sort_order") | ||
138 | + @Builder.Default | ||
139 | + private Integer sortOrder = 0; | ||
140 | + | ||
141 | + /** | ||
142 | + * 是否启用 | ||
143 | + */ | ||
144 | + @Column(name = "is_active") | ||
145 | + @Builder.Default | ||
146 | + private Boolean isActive = true; | ||
147 | + | ||
148 | + /** | ||
149 | + * 图标标识(用于前端显示) | ||
150 | + */ | ||
151 | + @Column(name = "icon", length = 100) | ||
152 | + private String icon; | ||
153 | + | ||
154 | + /** | ||
155 | + * 颜色标识(十六进制颜色码) | ||
156 | + */ | ||
157 | + @Column(name = "color", length = 7) | ||
158 | + private String color; | ||
159 | + | ||
160 | + /** | ||
161 | + * 使用次数统计 | ||
162 | + */ | ||
163 | + @Column(name = "usage_count") | ||
164 | + @Builder.Default | ||
165 | + private Integer usageCount = 0; | ||
166 | + | ||
167 | + /** | ||
168 | + * 创建时间 | ||
169 | + */ | ||
170 | + @CreationTimestamp | ||
171 | + @Column(name = "created_at", updatable = false) | ||
172 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
173 | + private LocalDateTime createdAt; | ||
174 | + | ||
175 | + /** | ||
176 | + * 更新时间 | ||
177 | + */ | ||
178 | + @UpdateTimestamp | ||
179 | + @Column(name = "updated_at") | ||
180 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
181 | + private LocalDateTime updatedAt; | ||
182 | + | ||
183 | + /** | ||
184 | + * 实体创建前的处理 | ||
185 | + */ | ||
186 | + @PrePersist | ||
187 | + protected void onCreate() { | ||
188 | + if (isActive == null) isActive = true; | ||
189 | + if (requiresFaq == null) requiresFaq = false; | ||
190 | + if (requiresStructuredData == null) requiresStructuredData = false; | ||
191 | + if (requiresRelatedLinks == null) requiresRelatedLinks = false; | ||
192 | + if (sortOrder == null) sortOrder = 0; | ||
193 | + if (usageCount == null) usageCount = 0; | ||
194 | + if (seoWeight == null) seoWeight = 5; | ||
195 | + if (recommendedWordCountMin == null) recommendedWordCountMin = 800; | ||
196 | + if (recommendedWordCountMax == null) recommendedWordCountMax = 1500; | ||
197 | + | ||
198 | + // 自动生成code(如果为空) | ||
199 | + if (code == null && name != null) { | ||
200 | + this.code = generateCodeFromName(name); | ||
201 | + } | ||
202 | + } | ||
203 | + | ||
204 | + /** | ||
205 | + * 从名称生成代码标识 | ||
206 | + * @param name 类型名称 | ||
207 | + * @return 代码标识 | ||
208 | + */ | ||
209 | + private String generateCodeFromName(String name) { | ||
210 | + if (name == null) return null; | ||
211 | + | ||
212 | + return name.toLowerCase() | ||
213 | + .replaceAll("[^a-zA-Z0-9\u4e00-\u9fa5]", "_") // 非字母数字中文替换为下划线 | ||
214 | + .replaceAll("_+", "_") // 多个下划线合并为一个 | ||
215 | + .replaceAll("^_|_$", ""); // 移除开头和结尾的下划线 | ||
216 | + } | ||
217 | + | ||
218 | + /** | ||
219 | + * 获取推荐字数范围描述 | ||
220 | + */ | ||
221 | + public String getRecommendedWordCountRange() { | ||
222 | + return recommendedWordCountMin + "-" + recommendedWordCountMax + "字"; | ||
223 | + } | ||
224 | + | ||
225 | + /** | ||
226 | + * 检查是否为高SEO权重类型 | ||
227 | + */ | ||
228 | + public boolean isHighSeoWeight() { | ||
229 | + return seoWeight != null && seoWeight >= 7; | ||
230 | + } | ||
231 | + | ||
232 | + /** | ||
233 | + * 检查是否为复杂类型(需要多种特殊元素) | ||
234 | + */ | ||
235 | + public boolean isComplexType() { | ||
236 | + int complexity = 0; | ||
237 | + if (Boolean.TRUE.equals(requiresFaq)) complexity++; | ||
238 | + if (Boolean.TRUE.equals(requiresStructuredData)) complexity++; | ||
239 | + if (Boolean.TRUE.equals(requiresRelatedLinks)) complexity++; | ||
240 | + | ||
241 | + return complexity >= 2; | ||
242 | + } | ||
243 | + | ||
244 | + /** | ||
245 | + * 获取类型复杂度等级(1-5级) | ||
246 | + */ | ||
247 | + public int getComplexityLevel() { | ||
248 | + int level = 1; // 基础等级 | ||
249 | + | ||
250 | + if (Boolean.TRUE.equals(requiresFaq)) level++; | ||
251 | + if (Boolean.TRUE.equals(requiresStructuredData)) level++; | ||
252 | + if (Boolean.TRUE.equals(requiresRelatedLinks)) level++; | ||
253 | + | ||
254 | + // 根据字数要求调整复杂度 | ||
255 | + if (recommendedWordCountMax != null && recommendedWordCountMax > 2000) level++; | ||
256 | + | ||
257 | + return Math.min(level, 5); // 最高5级 | ||
258 | + } | ||
259 | + | ||
260 | + /** | ||
261 | + * 增加使用次数 | ||
262 | + */ | ||
263 | + public void incrementUsageCount() { | ||
264 | + this.usageCount = (usageCount == null ? 0 : usageCount) + 1; | ||
265 | + } | ||
266 | + | ||
267 | + /** | ||
268 | + * 检查是否有完整的模板配置 | ||
269 | + */ | ||
270 | + public boolean hasCompleteTemplates() { | ||
271 | + return defaultTitleTemplate != null && !defaultTitleTemplate.trim().isEmpty() && | ||
272 | + defaultContentTemplate != null && !defaultContentTemplate.trim().isEmpty(); | ||
273 | + } | ||
274 | + | ||
275 | + /** | ||
276 | + * 获取类型优先级(基于SEO权重和使用次数) | ||
277 | + */ | ||
278 | + public double getPriority() { | ||
279 | + double seoWeight = this.seoWeight != null ? this.seoWeight : 5; | ||
280 | + double usageWeight = this.usageCount != null ? Math.log(this.usageCount + 1) : 0; | ||
281 | + | ||
282 | + return seoWeight * 0.7 + usageWeight * 0.3; // SEO权重占70%,使用频率占30% | ||
283 | + } | ||
284 | +} |
1 | +package com.aigeo.article.entity; | ||
2 | + | ||
3 | +import com.aigeo.common.enums.ContentStatus; | ||
4 | +import com.fasterxml.jackson.annotation.JsonFormat; | ||
5 | +import lombok.AllArgsConstructor; | ||
6 | +import lombok.Builder; | ||
7 | +import lombok.Data; | ||
8 | +import lombok.NoArgsConstructor; | ||
9 | +import org.hibernate.annotations.CreationTimestamp; | ||
10 | +import org.hibernate.annotations.UpdateTimestamp; | ||
11 | + | ||
12 | +import jakarta.persistence.*; | ||
13 | +import jakarta.validation.constraints.NotBlank; | ||
14 | +import jakarta.validation.constraints.NotNull; | ||
15 | +import java.time.LocalDateTime; | ||
16 | + | ||
17 | +/** | ||
18 | + * 生成文章实体类 | ||
19 | + * 对应数据库表:ai_generated_articles | ||
20 | + * | ||
21 | + * 存储AI生成的文章内容,包括: | ||
22 | + * - 文章标题、内容和摘要 | ||
23 | + * - SEO相关元数据 | ||
24 | + * - 文章状态和发布信息 | ||
25 | + * - 质量评分和统计数据 | ||
26 | + * | ||
27 | + * @author AIGEO Team | ||
28 | + * @since 1.0.0 | ||
29 | + */ | ||
30 | +@Data | ||
31 | +@Entity | ||
32 | +@Builder | ||
33 | +@NoArgsConstructor | ||
34 | +@AllArgsConstructor | ||
35 | +@Table(name = "ai_generated_articles", indexes = { | ||
36 | + @Index(name = "idx_articles_company_status", columnList = "company_id, status"), | ||
37 | + @Index(name = "idx_articles_task_id", columnList = "task_id"), | ||
38 | + @Index(name = "idx_articles_slug", columnList = "slug") | ||
39 | +}) | ||
40 | +public class GeneratedArticle { | ||
41 | + | ||
42 | + /** | ||
43 | + * 主键ID | ||
44 | + */ | ||
45 | + @Id | ||
46 | + @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
47 | + @Column(name = "id", nullable = false, updatable = false) | ||
48 | + private Integer id; | ||
49 | + | ||
50 | + /** | ||
51 | + * 公司ID(多租户字段) | ||
52 | + */ | ||
53 | + @NotNull(message = "公司ID不能为空") | ||
54 | + @Column(name = "company_id", nullable = false) | ||
55 | + private Integer companyId; | ||
56 | + | ||
57 | + /** | ||
58 | + * 关联的生成任务ID | ||
59 | + */ | ||
60 | + @Column(name = "task_id") | ||
61 | + private Integer taskId; | ||
62 | + | ||
63 | + /** | ||
64 | + * 文章标题 | ||
65 | + */ | ||
66 | + @NotBlank(message = "文章标题不能为空") | ||
67 | + @Column(name = "title", nullable = false, length = 500) | ||
68 | + private String title; | ||
69 | + | ||
70 | + /** | ||
71 | + * URL友好的标题(slug) | ||
72 | + */ | ||
73 | + @Column(name = "slug", length = 500) | ||
74 | + private String slug; | ||
75 | + | ||
76 | + /** | ||
77 | + * 文章摘要/描述 | ||
78 | + */ | ||
79 | + @Column(name = "excerpt", length = 1000) | ||
80 | + private String excerpt; | ||
81 | + | ||
82 | + /** | ||
83 | + * 文章正文内容 | ||
84 | + */ | ||
85 | + @Column(name = "content", columnDefinition = "LONGTEXT") | ||
86 | + private String content; | ||
87 | + | ||
88 | + /** | ||
89 | + * SEO标题(用于<title>标签) | ||
90 | + */ | ||
91 | + @Column(name = "seo_title", length = 200) | ||
92 | + private String seoTitle; | ||
93 | + | ||
94 | + /** | ||
95 | + * SEO描述(用于meta description) | ||
96 | + */ | ||
97 | + @Column(name = "seo_description", length = 500) | ||
98 | + private String seoDescription; | ||
99 | + | ||
100 | + /** | ||
101 | + * 文章标签(JSON数组或逗号分隔) | ||
102 | + */ | ||
103 | + @Column(name = "tags", length = 1000) | ||
104 | + private String tags; | ||
105 | + | ||
106 | + /** | ||
107 | + * 文章字数统计 | ||
108 | + */ | ||
109 | + @Column(name = "word_count") | ||
110 | + private Integer wordCount; | ||
111 | + | ||
112 | + /** | ||
113 | + * 阅读时长估算(分钟) | ||
114 | + */ | ||
115 | + @Column(name = "reading_time") | ||
116 | + private Integer readingTime; | ||
117 | + | ||
118 | + /** | ||
119 | + * 文章状态 | ||
120 | + */ | ||
121 | + @Enumerated(EnumType.STRING) | ||
122 | + @Column(name = "status") | ||
123 | + @Builder.Default | ||
124 | + private ContentStatus status = ContentStatus.DRAFT; | ||
125 | + | ||
126 | + /** | ||
127 | + * 质量评分(1-100分) | ||
128 | + */ | ||
129 | + @Column(name = "quality_score") | ||
130 | + private Integer qualityScore; | ||
131 | + | ||
132 | + /** | ||
133 | + * AI置信度评分(0-1之间) | ||
134 | + */ | ||
135 | + @Column(name = "ai_confidence") | ||
136 | + private Double aiConfidence; | ||
137 | + | ||
138 | + /** | ||
139 | + * 原创性评分(0-1之间,1表示完全原创) | ||
140 | + */ | ||
141 | + @Column(name = "originality_score") | ||
142 | + private Double originalityScore; | ||
143 | + | ||
144 | + /** | ||
145 | + * SEO评分(1-100分) | ||
146 | + */ | ||
147 | + @Column(name = "seo_score") | ||
148 | + private Integer seoScore; | ||
149 | + | ||
150 | + /** | ||
151 | + * 文章特色图片URL | ||
152 | + */ | ||
153 | + @Column(name = "featured_image_url", length = 500) | ||
154 | + private String featuredImageUrl; | ||
155 | + | ||
156 | + /** | ||
157 | + * 发布时间(NULL表示未发布) | ||
158 | + */ | ||
159 | + @Column(name = "published_at") | ||
160 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
161 | + private LocalDateTime publishedAt; | ||
162 | + | ||
163 | + /** | ||
164 | + * 最后编辑的用户ID | ||
165 | + */ | ||
166 | + @Column(name = "last_edited_by") | ||
167 | + private Integer lastEditedBy; | ||
168 | + | ||
169 | + /** | ||
170 | + * 浏览次数统计 | ||
171 | + */ | ||
172 | + @Column(name = "view_count") | ||
173 | + @Builder.Default | ||
174 | + private Integer viewCount = 0; | ||
175 | + | ||
176 | + /** | ||
177 | + * 喜欢次数统计 | ||
178 | + */ | ||
179 | + @Column(name = "like_count") | ||
180 | + @Builder.Default | ||
181 | + private Integer likeCount = 0; | ||
182 | + | ||
183 | + /** | ||
184 | + * 分享次数统计 | ||
185 | + */ | ||
186 | + @Column(name = "share_count") | ||
187 | + @Builder.Default | ||
188 | + private Integer shareCount = 0; | ||
189 | + | ||
190 | + /** | ||
191 | + * 结构化数据(Schema.org JSON-LD) | ||
192 | + */ | ||
193 | + @Column(name = "structured_data", columnDefinition = "JSON") | ||
194 | + private String structuredData; | ||
195 | + | ||
196 | + /** | ||
197 | + * 元数据(JSON格式存储额外信息) | ||
198 | + */ | ||
199 | + @Column(name = "metadata", columnDefinition = "JSON") | ||
200 | + private String metadata; | ||
201 | + | ||
202 | + /** | ||
203 | + * 创建时间 | ||
204 | + */ | ||
205 | + @CreationTimestamp | ||
206 | + @Column(name = "created_at", updatable = false) | ||
207 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
208 | + private LocalDateTime createdAt; | ||
209 | + | ||
210 | + /** | ||
211 | + * 更新时间 | ||
212 | + */ | ||
213 | + @UpdateTimestamp | ||
214 | + @Column(name = "updated_at") | ||
215 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
216 | + private LocalDateTime updatedAt; | ||
217 | + | ||
218 | + /** | ||
219 | + * 实体创建前的处理 | ||
220 | + */ | ||
221 | + @PrePersist | ||
222 | + protected void onCreate() { | ||
223 | + if (status == null) status = ContentStatus.DRAFT; | ||
224 | + if (viewCount == null) viewCount = 0; | ||
225 | + if (likeCount == null) likeCount = 0; | ||
226 | + if (shareCount == null) shareCount = 0; | ||
227 | + | ||
228 | + // 生成slug | ||
229 | + if (slug == null && title != null) { | ||
230 | + this.slug = generateSlugFromTitle(title); | ||
231 | + } | ||
232 | + | ||
233 | + // 如果没有SEO标题,使用文章标题 | ||
234 | + if (seoTitle == null && title != null) { | ||
235 | + this.seoTitle = title.length() > 60 ? title.substring(0, 60) + "..." : title; | ||
236 | + } | ||
237 | + | ||
238 | + // 估算阅读时长(按平均阅读速度250字/分钟计算) | ||
239 | + if (readingTime == null && wordCount != null) { | ||
240 | + this.readingTime = Math.max(1, (int) Math.ceil(wordCount / 250.0)); | ||
241 | + } | ||
242 | + } | ||
243 | + | ||
244 | + /** | ||
245 | + * 从标题生成URL友好的slug | ||
246 | + * @param title 文章标题 | ||
247 | + * @return URL友好的slug | ||
248 | + */ | ||
249 | + private String generateSlugFromTitle(String title) { | ||
250 | + if (title == null) return null; | ||
251 | + | ||
252 | + return title.toLowerCase() | ||
253 | + .replaceAll("[^a-zA-Z0-9\u4e00-\u9fa5\\s-]", "") // 保留字母、数字、中文、空格和连字符 | ||
254 | + .replaceAll("\\s+", "-") // 空格替换为连字符 | ||
255 | + .replaceAll("-+", "-") // 多个连字符合并为一个 | ||
256 | + .replaceAll("^-|-$", ""); // 移除开头和结尾的连字符 | ||
257 | + } | ||
258 | + | ||
259 | + /** | ||
260 | + * 检查文章是否已发布 | ||
261 | + */ | ||
262 | + public boolean isPublished() { | ||
263 | + return status == ContentStatus.PUBLISHED && publishedAt != null; | ||
264 | + } | ||
265 | + | ||
266 | + /** | ||
267 | + * 检查文章是否可以发布 | ||
268 | + */ | ||
269 | + public boolean canPublish() { | ||
270 | + return status == ContentStatus.APPROVED || | ||
271 | + status == ContentStatus.GENERATED || | ||
272 | + status == ContentStatus.COMPLETED; | ||
273 | + } | ||
274 | + | ||
275 | + /** | ||
276 | + * 获取综合评分(质量+SEO+原创性的平均值) | ||
277 | + */ | ||
278 | + public Double getOverallScore() { | ||
279 | + int count = 0; | ||
280 | + double total = 0.0; | ||
281 | + | ||
282 | + if (qualityScore != null) { | ||
283 | + total += qualityScore; | ||
284 | + count++; | ||
285 | + } | ||
286 | + if (seoScore != null) { | ||
287 | + total += seoScore; | ||
288 | + count++; | ||
289 | + } | ||
290 | + if (originalityScore != null) { | ||
291 | + total += originalityScore * 100; // 转换为1-100分制 | ||
292 | + count++; | ||
293 | + } | ||
294 | + | ||
295 | + return count > 0 ? total / count : null; | ||
296 | + } | ||
297 | + | ||
298 | + /** | ||
299 | + * 获取参与度评分(基于浏览、点赞、分享数据) | ||
300 | + */ | ||
301 | + public Double getEngagementScore() { | ||
302 | + if (viewCount == 0) return 0.0; | ||
303 | + | ||
304 | + // 简单的参与度计算:(点赞数*2 + 分享数*3) / 浏览数 * 100 | ||
305 | + return ((likeCount * 2.0 + shareCount * 3.0) / viewCount) * 100; | ||
306 | + } | ||
307 | + | ||
308 | + /** | ||
309 | + * 增加浏览次数 | ||
310 | + */ | ||
311 | + public void incrementViewCount() { | ||
312 | + this.viewCount = (viewCount == null ? 0 : viewCount) + 1; | ||
313 | + } | ||
314 | + | ||
315 | + /** | ||
316 | + * 增加点赞次数 | ||
317 | + */ | ||
318 | + public void incrementLikeCount() { | ||
319 | + this.likeCount = (likeCount == null ? 0 : likeCount) + 1; | ||
320 | + } | ||
321 | + | ||
322 | + /** | ||
323 | + * 增加分享次数 | ||
324 | + */ | ||
325 | + public void incrementShareCount() { | ||
326 | + this.shareCount = (shareCount == null ? 0 : shareCount) + 1; | ||
327 | + } | ||
328 | +} |
1 | +package com.aigeo.article.entity; | ||
2 | + | ||
3 | +import com.fasterxml.jackson.annotation.JsonFormat; | ||
4 | +import lombok.AllArgsConstructor; | ||
5 | +import lombok.Builder; | ||
6 | +import lombok.Data; | ||
7 | +import lombok.NoArgsConstructor; | ||
8 | +import org.hibernate.annotations.CreationTimestamp; | ||
9 | + | ||
10 | +import jakarta.persistence.*; | ||
11 | +import jakarta.validation.constraints.NotBlank; | ||
12 | +import jakarta.validation.constraints.NotNull; | ||
13 | +import java.time.LocalDateTime; | ||
14 | + | ||
15 | +/** | ||
16 | + * 任务参考资料实体类 | ||
17 | + * 对应数据库表:ai_task_references | ||
18 | + * | ||
19 | + * 存储文章生成任务的参考资料信息,包括: | ||
20 | + * - 参考网站URL和标题 | ||
21 | + * - 参考内容摘要 | ||
22 | + * - 资料权重和可信度评分 | ||
23 | + * - 使用状态和备注信息 | ||
24 | + * | ||
25 | + * @author AIGEO Team | ||
26 | + * @since 1.0.0 | ||
27 | + */ | ||
28 | +@Data | ||
29 | +@Entity | ||
30 | +@Builder | ||
31 | +@NoArgsConstructor | ||
32 | +@AllArgsConstructor | ||
33 | +@Table(name = "ai_task_references", indexes = { | ||
34 | + @Index(name = "idx_task_refs_task_id", columnList = "task_id"), | ||
35 | + @Index(name = "idx_task_refs_url", columnList = "reference_url") | ||
36 | +}) | ||
37 | +public class TaskReference { | ||
38 | + | ||
39 | + /** | ||
40 | + * 主键ID | ||
41 | + */ | ||
42 | + @Id | ||
43 | + @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
44 | + @Column(name = "id", nullable = false, updatable = false) | ||
45 | + private Integer id; | ||
46 | + | ||
47 | + /** | ||
48 | + * 关联的生成任务ID | ||
49 | + */ | ||
50 | + @NotNull(message = "任务ID不能为空") | ||
51 | + @Column(name = "task_id", nullable = false) | ||
52 | + private Integer taskId; | ||
53 | + | ||
54 | + /** | ||
55 | + * 参考资料URL | ||
56 | + */ | ||
57 | + @NotBlank(message = "参考资料URL不能为空") | ||
58 | + @Column(name = "reference_url", nullable = false, length = 500) | ||
59 | + private String referenceUrl; | ||
60 | + | ||
61 | + /** | ||
62 | + * 参考资料标题 | ||
63 | + */ | ||
64 | + @Column(name = "reference_title", length = 500) | ||
65 | + private String referenceTitle; | ||
66 | + | ||
67 | + /** | ||
68 | + * 参考内容摘要 | ||
69 | + */ | ||
70 | + @Column(name = "content_summary", length = 2000) | ||
71 | + private String contentSummary; | ||
72 | + | ||
73 | + /** | ||
74 | + * 资料来源域名 | ||
75 | + */ | ||
76 | + @Column(name = "source_domain", length = 100) | ||
77 | + private String sourceDomain; | ||
78 | + | ||
79 | + /** | ||
80 | + * 内容类型(如:article, blog, news, academic等) | ||
81 | + */ | ||
82 | + @Column(name = "content_type", length = 50) | ||
83 | + @Builder.Default | ||
84 | + private String contentType = "article"; | ||
85 | + | ||
86 | + /** | ||
87 | + * 资料权重(影响生成结果的重要程度,1-10分) | ||
88 | + */ | ||
89 | + @Column(name = "weight") | ||
90 | + @Builder.Default | ||
91 | + private Integer weight = 5; | ||
92 | + | ||
93 | + /** | ||
94 | + * 可信度评分(1-10分) | ||
95 | + */ | ||
96 | + @Column(name = "credibility_score") | ||
97 | + @Builder.Default | ||
98 | + private Integer credibilityScore = 5; | ||
99 | + | ||
100 | + /** | ||
101 | + * 相关性评分(与目标话题的相关程度,1-10分) | ||
102 | + */ | ||
103 | + @Column(name = "relevance_score") | ||
104 | + @Builder.Default | ||
105 | + private Integer relevanceScore = 5; | ||
106 | + | ||
107 | + /** | ||
108 | + * 内容质量评分(1-10分) | ||
109 | + */ | ||
110 | + @Column(name = "quality_score") | ||
111 | + private Integer qualityScore; | ||
112 | + | ||
113 | + /** | ||
114 | + * 发布时间(参考资料的原始发布时间) | ||
115 | + */ | ||
116 | + @Column(name = "published_at") | ||
117 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
118 | + private LocalDateTime publishedAt; | ||
119 | + | ||
120 | + /** | ||
121 | + * 语言标识 | ||
122 | + */ | ||
123 | + @Column(name = "language", length = 10) | ||
124 | + @Builder.Default | ||
125 | + private String language = "zh-CN"; | ||
126 | + | ||
127 | + /** | ||
128 | + * 字数统计 | ||
129 | + */ | ||
130 | + @Column(name = "word_count") | ||
131 | + private Integer wordCount; | ||
132 | + | ||
133 | + /** | ||
134 | + * 是否已处理(是否已被AI分析处理) | ||
135 | + */ | ||
136 | + @Column(name = "is_processed") | ||
137 | + @Builder.Default | ||
138 | + private Boolean isProcessed = false; | ||
139 | + | ||
140 | + /** | ||
141 | + * 处理状态(pending, processing, completed, failed) | ||
142 | + */ | ||
143 | + @Column(name = "processing_status", length = 20) | ||
144 | + @Builder.Default | ||
145 | + private String processingStatus = "pending"; | ||
146 | + | ||
147 | + /** | ||
148 | + * 使用状态(是否在生成中被实际使用) | ||
149 | + */ | ||
150 | + @Column(name = "is_used") | ||
151 | + @Builder.Default | ||
152 | + private Boolean isUsed = false; | ||
153 | + | ||
154 | + /** | ||
155 | + * 错误信息(处理失败时的错误描述) | ||
156 | + */ | ||
157 | + @Column(name = "error_message", length = 1000) | ||
158 | + private String errorMessage; | ||
159 | + | ||
160 | + /** | ||
161 | + * 提取的关键词(JSON数组或逗号分隔) | ||
162 | + */ | ||
163 | + @Column(name = "extracted_keywords", length = 1000) | ||
164 | + private String extractedKeywords; | ||
165 | + | ||
166 | + /** | ||
167 | + * 提取的实体(人名、地名、机构名等,JSON格式) | ||
168 | + */ | ||
169 | + @Column(name = "extracted_entities", columnDefinition = "JSON") | ||
170 | + private String extractedEntities; | ||
171 | + | ||
172 | + /** | ||
173 | + * 内容分类标签 | ||
174 | + */ | ||
175 | + @Column(name = "content_tags", length = 500) | ||
176 | + private String contentTags; | ||
177 | + | ||
178 | + /** | ||
179 | + * 备注信息 | ||
180 | + */ | ||
181 | + @Column(name = "notes", length = 500) | ||
182 | + private String notes; | ||
183 | + | ||
184 | + /** | ||
185 | + * 元数据(JSON格式存储额外信息) | ||
186 | + */ | ||
187 | + @Column(name = "metadata", columnDefinition = "JSON") | ||
188 | + private String metadata; | ||
189 | + | ||
190 | + /** | ||
191 | + * 创建时间 | ||
192 | + */ | ||
193 | + @CreationTimestamp | ||
194 | + @Column(name = "created_at", updatable = false) | ||
195 | + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||
196 | + private LocalDateTime createdAt; | ||
197 | + | ||
198 | + /** | ||
199 | + * 实体创建前的处理 | ||
200 | + */ | ||
201 | + @PrePersist | ||
202 | + protected void onCreate() { | ||
203 | + if (contentType == null) contentType = "article"; | ||
204 | + if (weight == null) weight = 5; | ||
205 | + if (credibilityScore == null) credibilityScore = 5; | ||
206 | + if (relevanceScore == null) relevanceScore = 5; | ||
207 | + if (language == null) language = "zh-CN"; | ||
208 | + if (isProcessed == null) isProcessed = false; | ||
209 | + if (processingStatus == null) processingStatus = "pending"; | ||
210 | + if (isUsed == null) isUsed = false; | ||
211 | + | ||
212 | + // 从URL提取域名 | ||
213 | + if (sourceDomain == null && referenceUrl != null) { | ||
214 | + this.sourceDomain = extractDomainFromUrl(referenceUrl); | ||
215 | + } | ||
216 | + } | ||
217 | + | ||
218 | + /** | ||
219 | + * 从URL提取域名 | ||
220 | + * @param url 完整URL | ||
221 | + * @return 域名 | ||
222 | + */ | ||
223 | + private String extractDomainFromUrl(String url) { | ||
224 | + try { | ||
225 | + if (url.startsWith("http://")) { | ||
226 | + url = url.substring(7); | ||
227 | + } else if (url.startsWith("https://")) { | ||
228 | + url = url.substring(8); | ||
229 | + } | ||
230 | + | ||
231 | + int slashIndex = url.indexOf('/'); | ||
232 | + if (slashIndex != -1) { | ||
233 | + url = url.substring(0, slashIndex); | ||
234 | + } | ||
235 | + | ||
236 | + return url; | ||
237 | + } catch (Exception e) { | ||
238 | + return null; | ||
239 | + } | ||
240 | + } | ||
241 | + | ||
242 | + /** | ||
243 | + * 检查是否为高质量参考资料 | ||
244 | + */ | ||
245 | + public boolean isHighQuality() { | ||
246 | + return (credibilityScore != null && credibilityScore >= 7) && | ||
247 | + (relevanceScore != null && relevanceScore >= 7) && | ||
248 | + (qualityScore == null || qualityScore >= 7); | ||
249 | + } | ||
250 | + | ||
251 | + /** | ||
252 | + * 检查是否为权威来源 | ||
253 | + */ | ||
254 | + public boolean isAuthoritativeSource() { | ||
255 | + if (sourceDomain == null) return false; | ||
256 | + | ||
257 | + // 常见权威域名后缀 | ||
258 | + return sourceDomain.endsWith(".edu") || | ||
259 | + sourceDomain.endsWith(".gov") || | ||
260 | + sourceDomain.endsWith(".org") || | ||
261 | + credibilityScore != null && credibilityScore >= 8; | ||
262 | + } | ||
263 | + | ||
264 | + /** | ||
265 | + * 获取综合评分(权重、可信度、相关性的加权平均) | ||
266 | + */ | ||
267 | + public double getOverallScore() { | ||
268 | + double weightScore = weight != null ? weight : 5; | ||
269 | + double credibilityScore = this.credibilityScore != null ? this.credibilityScore : 5; | ||
270 | + double relevanceScore = this.relevanceScore != null ? this.relevanceScore : 5; | ||
271 | + double qualityScore = this.qualityScore != null ? this.qualityScore : 5; | ||
272 | + | ||
273 | + // 加权计算:相关性40%,可信度30%,质量20%,权重10% | ||
274 | + return relevanceScore * 0.4 + credibilityScore * 0.3 + qualityScore * 0.2 + weightScore * 0.1; | ||
275 | + } | ||
276 | + | ||
277 | + /** | ||
278 | + * 检查内容是否为近期发布 | ||
279 | + */ | ||
280 | + public boolean isRecentContent() { | ||
281 | + if (publishedAt == null) return false; | ||
282 | + | ||
283 | + LocalDateTime sixMonthsAgo = LocalDateTime.now().minusMonths(6); | ||
284 | + return publishedAt.isAfter(sixMonthsAgo); | ||
285 | + } | ||
286 | + | ||
287 | + /** | ||
288 | + * 获取内容新鲜度评分(1-10分,越新分数越高) | ||
289 | + */ | ||
290 | + public int getFreshnessScore() { | ||
291 | + if (publishedAt == null) return 5; // 默认中等分数 | ||
292 | + | ||
293 | + LocalDateTime now = LocalDateTime.now(); | ||
294 | + long daysDiff = java.time.Duration.between(publishedAt, now).toDays(); | ||
295 | + | ||
296 | + if (daysDiff <= 7) return 10; // 一周内:10分 | ||
297 | + if (daysDiff <= 30) return 9; // 一月内:9分 | ||
298 | + if (daysDiff <= 90) return 8; // 三月内:8分 | ||
299 | + if (daysDiff <= 180) return 7; // 半年内:7分 | ||
300 | + if (daysDiff <= 365) return 6; // 一年内:6分 | ||
301 | + if (daysDiff <= 730) return 4; // 两年内:4分 | ||
302 | + | ||
303 | + return 2; // 超过两年:2分 | ||
304 | + } | ||
305 | + | ||
306 | + /** | ||
307 | + * 标记为已处理 | ||
308 | + */ | ||
309 | + public void markAsProcessed() { | ||
310 | + this.isProcessed = true; | ||
311 | + this.processingStatus = "completed"; | ||
312 | + } | ||
313 | + | ||
314 | + /** | ||
315 | + * 标记处理失败 | ||
316 | + */ | ||
317 | + public void markAsFailed(String errorMessage) { | ||
318 | + this.isProcessed = false; | ||
319 | + this.processingStatus = "failed"; | ||
320 | + this.errorMessage = errorMessage; | ||
321 | + } | ||
322 | + | ||
323 | + /** | ||
324 | + * 标记为已使用 | ||
325 | + */ | ||
326 | + public void markAsUsed() { | ||
327 | + this.isUsed = true; | ||
328 | + } | ||
329 | +} |
1 | +package com.aigeo.article.repository; | ||
2 | + | ||
3 | +import com.aigeo.article.entity.ArticleGenerationTask; | ||
4 | +import com.aigeo.common.enums.TaskStatus; | ||
5 | +import com.aigeo.common.enums.AiTasteLevel; | ||
6 | +import org.springframework.data.domain.Page; | ||
7 | +import org.springframework.data.domain.Pageable; | ||
8 | +import org.springframework.data.jpa.repository.JpaRepository; | ||
9 | +import org.springframework.data.jpa.repository.Query; | ||
10 | +import org.springframework.data.repository.query.Param; | ||
11 | +import org.springframework.stereotype.Repository; | ||
12 | + | ||
13 | +import java.time.LocalDateTime; | ||
14 | +import java.util.List; | ||
15 | +import java.util.Optional; | ||
16 | + | ||
17 | +/** | ||
18 | + * 文章生成任务数据访问层 | ||
19 | + * 对应数据库表:ai_article_generation_tasks | ||
20 | + * | ||
21 | + * 负责管理AI文章生成任务的数据访问操作,包括任务创建、状态跟踪、 | ||
22 | + * 进度监控和结果查询等功能 | ||
23 | + * | ||
24 | + * @author AIGEO Team | ||
25 | + * @since 1.0.0 | ||
26 | + */ | ||
27 | +@Repository | ||
28 | +public interface ArticleGenerationTaskRepository extends JpaRepository<ArticleGenerationTask, Integer> { | ||
29 | + | ||
30 | + /** | ||
31 | + * 根据公司ID查找所有任务 | ||
32 | + * @param companyId 公司ID | ||
33 | + * @return 该公司的所有文章生成任务 | ||
34 | + */ | ||
35 | + List<ArticleGenerationTask> findByCompanyId(Integer companyId); | ||
36 | + | ||
37 | + /** | ||
38 | + * 根据用户ID查找任务 | ||
39 | + * @param userId 用户ID | ||
40 | + * @return 该用户创建的所有任务 | ||
41 | + */ | ||
42 | + List<ArticleGenerationTask> findByUserId(Integer userId); | ||
43 | + | ||
44 | + /** | ||
45 | + * 根据状态查找任务 | ||
46 | + * @param status 任务状态 | ||
47 | + * @return 处于指定状态的所有任务 | ||
48 | + */ | ||
49 | + List<ArticleGenerationTask> findByStatus(TaskStatus status); | ||
50 | + | ||
51 | + /** | ||
52 | + * 根据公司ID和状态查找任务 | ||
53 | + * @param companyId 公司ID | ||
54 | + * @param status 任务状态 | ||
55 | + * @return 匹配条件的任务列表 | ||
56 | + */ | ||
57 | + List<ArticleGenerationTask> findByCompanyIdAndStatus(Integer companyId, TaskStatus status); | ||
58 | + | ||
59 | + /** | ||
60 | + * 查找待处理的任务(按创建时间正序) | ||
61 | + * @return 待处理的任务队列 | ||
62 | + */ | ||
63 | + List<ArticleGenerationTask> findByStatusOrderByCreatedAtAsc(TaskStatus status); | ||
64 | + | ||
65 | + /** | ||
66 | + * 分页查询公司任务(按创建时间倒序) | ||
67 | + * @param companyId 公司ID | ||
68 | + * @param pageable 分页参数 | ||
69 | + * @return 分页结果 | ||
70 | + */ | ||
71 | + Page<ArticleGenerationTask> findByCompanyIdOrderByCreatedAtDesc(Integer companyId, Pageable pageable); | ||
72 | + | ||
73 | + /** | ||
74 | + * 根据关键词ID查找任务 | ||
75 | + * @param keywordId 关键词ID | ||
76 | + * @return 基于该关键词的生成任务 | ||
77 | + */ | ||
78 | + List<ArticleGenerationTask> findByKeywordId(Integer keywordId); | ||
79 | + | ||
80 | + /** | ||
81 | + * 根据AI风格等级查找任务 | ||
82 | + * @param companyId 公司ID | ||
83 | + * @param aiTasteLevel AI风格等级 | ||
84 | + * @return 使用指定风格的任务 | ||
85 | + */ | ||
86 | + List<ArticleGenerationTask> findByCompanyIdAndAiTasteLevel(Integer companyId, AiTasteLevel aiTasteLevel); | ||
87 | + | ||
88 | + /** | ||
89 | + * 查找指定时间范围内的任务 | ||
90 | + * @param companyId 公司ID | ||
91 | + * @param startTime 开始时间 | ||
92 | + * @param endTime 结束时间 | ||
93 | + * @return 时间范围内的任务 | ||
94 | + */ | ||
95 | + List<ArticleGenerationTask> findByCompanyIdAndCreatedAtBetween(Integer companyId, LocalDateTime startTime, LocalDateTime endTime); | ||
96 | + | ||
97 | + /** | ||
98 | + * 统计公司任务数量按状态分组 | ||
99 | + * @param companyId 公司ID | ||
100 | + * @param status 任务状态 | ||
101 | + * @return 指定状态的任务数量 | ||
102 | + */ | ||
103 | + @Query("SELECT COUNT(t) FROM ArticleGenerationTask t WHERE t.companyId = :companyId AND t.status = :status") | ||
104 | + long countByCompanyIdAndStatus(@Param("companyId") Integer companyId, @Param("status") TaskStatus status); | ||
105 | + | ||
106 | + /** | ||
107 | + * 查找用户最近的任务 | ||
108 | + * @param userId 用户ID | ||
109 | + * @param limit 限制数量 | ||
110 | + * @return 用户最近的任务 | ||
111 | + */ | ||
112 | + @Query("SELECT t FROM ArticleGenerationTask t WHERE t.userId = :userId ORDER BY t.createdAt DESC") | ||
113 | + List<ArticleGenerationTask> findRecentTasksByUser(@Param("userId") Integer userId, Pageable pageable); | ||
114 | + | ||
115 | + /** | ||
116 | + * 查找超时未完成的任务 | ||
117 | + * @param timeoutThreshold 超时时间阈值 | ||
118 | + * @return 超时的处理中任务 | ||
119 | + */ | ||
120 | + @Query("SELECT t FROM ArticleGenerationTask t WHERE t.status = 'PROCESSING' AND t.createdAt < :timeoutThreshold") | ||
121 | + List<ArticleGenerationTask> findTimeoutTasks(@Param("timeoutThreshold") LocalDateTime timeoutThreshold); | ||
122 | + | ||
123 | + /** | ||
124 | + * 根据公司ID和文章主题模糊搜索任务 | ||
125 | + * @param companyId 公司ID | ||
126 | + * @param articleTheme 文章主题关键词 | ||
127 | + * @return 匹配的任务列表 | ||
128 | + */ | ||
129 | + List<ArticleGenerationTask> findByCompanyIdAndArticleThemeContainingIgnoreCase(Integer companyId, String articleTheme); | ||
130 | + | ||
131 | + /** | ||
132 | + * 查找最近完成的任务 | ||
133 | + * @param companyId 公司ID(可选) | ||
134 | + * @param status 任务状态 | ||
135 | + * @param pageable 分页参数 | ||
136 | + * @return 最近完成的任务 | ||
137 | + */ | ||
138 | + @Query("SELECT t FROM ArticleGenerationTask t WHERE (:companyId IS NULL OR t.companyId = :companyId) AND t.status = :status ORDER BY t.completedAt DESC") | ||
139 | + List<ArticleGenerationTask> findRecentCompletedTasks(@Param("companyId") Integer companyId, @Param("status") TaskStatus status, Pageable pageable); | ||
140 | + | ||
141 | + /** | ||
142 | + * 根据多个条件搜索任务 | ||
143 | + * @param companyId 公司ID | ||
144 | + * @param userId 用户ID | ||
145 | + * @param status 任务状态 | ||
146 | + * @param articleTheme 文章主题 | ||
147 | + * @param startTime 开始时间 | ||
148 | + * @param endTime 结束时间 | ||
149 | + * @param pageable 分页参数 | ||
150 | + * @return 分页搜索结果 | ||
151 | + */ | ||
152 | + @Query("SELECT t FROM ArticleGenerationTask t WHERE " + | ||
153 | + "(:companyId IS NULL OR t.companyId = :companyId) AND " + | ||
154 | + "(:userId IS NULL OR t.userId = :userId) AND " + | ||
155 | + "(:status IS NULL OR t.status = :status) AND " + | ||
156 | + "(:articleTheme IS NULL OR t.articleTheme LIKE %:articleTheme%) AND " + | ||
157 | + "(:startTime IS NULL OR t.createdAt >= :startTime) AND " + | ||
158 | + "(:endTime IS NULL OR t.createdAt <= :endTime)") | ||
159 | + Page<ArticleGenerationTask> searchTasks(@Param("companyId") Integer companyId, | ||
160 | + @Param("userId") Integer userId, | ||
161 | + @Param("status") TaskStatus status, | ||
162 | + @Param("articleTheme") String articleTheme, | ||
163 | + @Param("startTime") LocalDateTime startTime, | ||
164 | + @Param("endTime") LocalDateTime endTime, | ||
165 | + Pageable pageable); | ||
166 | + | ||
167 | + /** | ||
168 | + * 查找最近更新的任务 | ||
169 | + * @param companyId 公司ID | ||
170 | + * @param since 起始时间 | ||
171 | + * @return 最近更新的任务 | ||
172 | + */ | ||
173 | + @Query("SELECT t FROM ArticleGenerationTask t WHERE (:companyId IS NULL OR t.companyId = :companyId) AND t.updatedAt >= :since ORDER BY t.updatedAt DESC") | ||
174 | + List<ArticleGenerationTask> findByUpdatedAtAfterOrderByUpdatedAtDesc(@Param("companyId") Integer companyId, @Param("since") LocalDateTime since); | ||
175 | + | ||
176 | + /** | ||
177 | + * 统计指定时间范围内的任务数量 | ||
178 | + * @param companyId 公司ID(可选) | ||
179 | + * @param status 任务状态 | ||
180 | + * @param startTime 开始时间 | ||
181 | + * @param endTime 结束时间 | ||
182 | + * @return 任务数量 | ||
183 | + */ | ||
184 | + @Query("SELECT COUNT(t) FROM ArticleGenerationTask t WHERE " + | ||
185 | + "(:companyId IS NULL OR t.companyId = :companyId) AND " + | ||
186 | + "(:status IS NULL OR t.status = :status) AND " + | ||
187 | + "t.createdAt BETWEEN :startTime AND :endTime") | ||
188 | + long countByConditions(@Param("companyId") Integer companyId, | ||
189 | + @Param("status") TaskStatus status, | ||
190 | + @Param("startTime") LocalDateTime startTime, | ||
191 | + @Param("endTime") LocalDateTime endTime); | ||
192 | + | ||
193 | + /** | ||
194 | + * 计算平均执行时长 | ||
195 | + * @param companyId 公司ID(可选) | ||
196 | + * @param startTime 开始时间 | ||
197 | + * @param endTime 结束时间 | ||
198 | + * @return 平均执行时长(毫秒) | ||
199 | + */ | ||
200 | + @Query("SELECT AVG(t.executionTimeMs) FROM ArticleGenerationTask t WHERE " + | ||
201 | + "(:companyId IS NULL OR t.companyId = :companyId) AND " + | ||
202 | + "t.executionTimeMs IS NOT NULL AND " + | ||
203 | + "t.createdAt BETWEEN :startTime AND :endTime") | ||
204 | + Double getAverageExecutionTime(@Param("companyId") Integer companyId, | ||
205 | + @Param("startTime") LocalDateTime startTime, | ||
206 | + @Param("endTime") LocalDateTime endTime); | ||
207 | + | ||
208 | + /** | ||
209 | + * 删除指定时间之前完成的任务 | ||
210 | + * @param completedBefore 完成时间阈值 | ||
211 | + * @param status 任务状态 | ||
212 | + * @return 删除的任务数量 | ||
213 | + */ | ||
214 | + @Query("DELETE FROM ArticleGenerationTask t WHERE t.status = :status AND t.completedAt < :completedBefore") | ||
215 | + int deleteCompletedTasksBefore(@Param("status") TaskStatus status, @Param("completedBefore") LocalDateTime completedBefore); | ||
216 | + | ||
217 | + long countByCompanyId(Integer companyId); | ||
218 | +} |
-
请 注册 或 登录 后发表评论