作者 徐宝林

初始化项目

正在显示 28 个修改的文件 包含 4294 行增加0 行删除

要显示太多修改。

为保证性能只显示 28 of 28+ 个文件。

  1 +{
  2 + "permissions": {
  3 + "allow": [
  4 + "Bash(find:*)",
  5 + "Bash(java:*)"
  6 + ],
  7 + "deny": [],
  8 + "ask": []
  9 + }
  10 +}
  1 +/mvnw text eol=lf
  2 +*.cmd text eol=crlf
  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/
  1 +distributionType=only-script
  2 +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
  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 "$@"
  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"
  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 +}