作者 徐宝林

初始化项目

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

要显示太多修改。

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

{
"permissions": {
"allow": [
"Bash(find:*)",
"Bash(java:*)"
],
"deny": [],
"ask": []
}
}
\ No newline at end of file
... ...
/mvnw text eol=lf
*.cmd text eol=crlf
... ...
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
... ...
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
... ...
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.3
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
scriptDir="$(dirname "$0")"
scriptName="$(basename "$0")"
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
# Find the actual extracted directory name (handles snapshots where filename != directory name)
actualDistributionDir=""
# First try the expected directory name (for regular distributions)
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
actualDistributionDir="$distributionUrlNameMain"
fi
fi
# If not found, search for any directory with the Maven executable (for snapshots)
if [ -z "$actualDistributionDir" ]; then
# enable globbing to iterate over items
set +f
for dir in "$TMP_DOWNLOAD_DIR"/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/bin/$MVN_CMD" ]; then
actualDistributionDir="$(basename "$dir")"
break
fi
fi
done
set -f
fi
if [ -z "$actualDistributionDir" ]; then
verbose "Contents of $TMP_DOWNLOAD_DIR:"
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
die "Could not find Maven distribution directory in extracted archive"
fi
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"
... ...
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.3
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@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 @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_M2_PATH = "$HOME/.m2"
if ($env:MAVEN_USER_HOME) {
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
}
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
}
$MAVEN_WRAPPER_DISTS = $null
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
} else {
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
}
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
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."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
# Find the actual extracted directory name (handles snapshots where filename != directory name)
$actualDistributionDir = ""
# First try the expected directory name (for regular distributions)
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
$actualDistributionDir = $distributionUrlNameMain
}
# If not found, search for any directory with the Maven executable (for snapshots)
if (!$actualDistributionDir) {
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
if (Test-Path -Path $testPath -PathType Leaf) {
$actualDistributionDir = $_.Name
}
}
}
if (!$actualDistributionDir) {
Write-Error "Could not find Maven distribution directory in extracted archive"
}
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
... ...
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/>
</parent>
<groupId>com.aigeo</groupId>
<artifactId>aigeo</artifactId>
<version>1.0.0</version>
<name>AIGEO</name>
<description>AI Content Generation Platform</description>
<properties>
<java.version>17</java.version>
<mysql.version>8.0.33</mysql.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<knife4j.version>4.4.0</knife4j.version>
<fastjson2.version>2.0.43</fastjson2.version>
<hutool.version>5.8.25</hutool.version>
<jwt.version>4.4.0</jwt.version>
<redisson.version>3.25.2</redisson.version>
<jsqlparser.version>4.9</jsqlparser.version>
</properties>
<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
<exclusions>
<exclusion>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Database -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- API Documentation -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- Utilities -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- Redis Client -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- HTTP Client -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- Configuration Processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- Jakarta Annotations -->
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- JSqlParser -->
<dependency>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
<version>${jsqlparser.version}</version>
</dependency>
<!-- Development Tools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.0</version>
<configuration>
<source>17</source>
<target>17</target>
<fork>true</fork>
<executable>F:/IntelliJ IDEA/jdk-17.0.12_windows-x64_bin/jdk-17.0.12/bin/javac.exe</executable>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
... ...
package com.aigeo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* AIGEO AI内容生成平台主启动类
*
* @author AIGEO Team
* @since 1.0.0
*/
@SpringBootApplication
@EnableScheduling
@EnableAsync
@EnableTransactionManagement
public class AigeoApplication {
private static final Logger log = LoggerFactory.getLogger(AigeoApplication.class);
public static void main(String[] args) {
SpringApplication.run(AigeoApplication.class, args);
log.info("\n========================================");
log.info(" AIGEO AI Content Platform Started!");
log.info(" API Documentation: /doc.html");
log.info("========================================\n");
}
/**
* 提供一个空的ddlApplicationRunner bean以防止Spring Boot自动配置创建的bean类型不匹配
*/
@Bean
public ApplicationRunner ddlApplicationRunner() {
return args -> {
System.out.println("DDL Application Runner executed");
};
}
}
... ...
package com.aigeo.ai.controller;
import com.aigeo.ai.entity.DifyApiConfig;
import com.aigeo.ai.service.DifyApiConfigService;
import com.aigeo.common.result.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Optional;
/**
* AI配置管理控制器
*
* @author AIGEO Team
* @since 1.0.0
*/
@Tag(name = "AI配置管理", description = "Dify API配置管理接口")
@Slf4j
@RestController
@RequestMapping("/api/ai-configs")
@RequiredArgsConstructor
public class DifyApiConfigController {
private final DifyApiConfigService difyApiConfigService;
@Operation(summary = "获取所有配置", description = "获取所有AI配置列表")
@GetMapping
public Result<List<DifyApiConfig>> getAllConfigs() {
log.debug("获取所有AI配置");
List<DifyApiConfig> configs = difyApiConfigService.getAllConfigs();
return Result.success(configs);
}
@Operation(summary = "分页查询配置", description = "分页查询AI配置列表")
@GetMapping("/list")
public Result<Page<DifyApiConfig>> listConfigs(
@Parameter(description = "页码") @RequestParam(defaultValue = "0") int page,
@Parameter(description = "页大小") @RequestParam(defaultValue = "10") int size) {
log.debug("分页查询AI配置,page: {}, size: {}", page, size);
Pageable pageable = PageRequest.of(page, size);
Page<DifyApiConfig> configs = difyApiConfigService.findAll(pageable);
return Result.success(configs);
}
@Operation(summary = "根据ID获取配置", description = "根据配置ID获取单个AI配置")
@GetMapping("/{id}")
public Result<DifyApiConfig> getConfigById(
@Parameter(description = "配置ID") @PathVariable @NotNull Integer id) {
log.debug("获取AI配置,id: {}", id);
Optional<DifyApiConfig> config = difyApiConfigService.getConfigById(id);
if (config.isPresent()) {
return Result.success(config.get());
} else {
return Result.error(404, "配置不存在");
}
}
@Operation(summary = "根据公司ID获取配置", description = "获取指定公司的AI配置列表")
@GetMapping("/company/{companyId}")
public Result<List<DifyApiConfig>> getConfigsByCompanyId(
@Parameter(description = "公司ID") @PathVariable @NotNull Integer companyId) {
log.debug("获取公司AI配置,companyId: {}", companyId);
List<DifyApiConfig> configs = difyApiConfigService.getConfigsByCompanyId(companyId);
return Result.success(configs);
}
@Operation(summary = "获取共享配置", description = "获取所有共享的AI配置")
@GetMapping("/shared")
public Result<List<DifyApiConfig>> getSharedConfigs() {
log.debug("获取共享AI配置");
List<DifyApiConfig> configs = difyApiConfigService.getSharedConfigs();
return Result.success(configs);
}
@Operation(summary = "获取启用的配置", description = "获取所有启用状态的AI配置")
@GetMapping("/active")
public Result<List<DifyApiConfig>> getActiveConfigs() {
log.debug("获取启用的AI配置");
List<DifyApiConfig> configs = difyApiConfigService.getActiveConfigs();
return Result.success(configs);
}
@Operation(summary = "创建配置", description = "创建新的AI配置")
@PostMapping
public Result<DifyApiConfig> createConfig(
@Parameter(description = "配置信息") @Valid @RequestBody DifyApiConfig config) {
log.info("创建AI配置,name: {}", config.getName());
DifyApiConfig createdConfig = difyApiConfigService.saveConfig(config);
return Result.success(createdConfig);
}
@Operation(summary = "更新配置", description = "更新指定的AI配置")
@PutMapping("/{id}")
public Result<DifyApiConfig> updateConfig(
@Parameter(description = "配置ID") @PathVariable @NotNull Integer id,
@Parameter(description = "配置信息") @Valid @RequestBody DifyApiConfig configDetails) {
log.info("更新AI配置,id: {}, name: {}", id, configDetails.getName());
DifyApiConfig updatedConfig = difyApiConfigService.updateConfig(id, configDetails);
return Result.success(updatedConfig);
}
@Operation(summary = "删除配置", description = "删除指定的AI配置")
@DeleteMapping("/{id}")
public Result<Void> deleteConfig(
@Parameter(description = "配置ID") @PathVariable @NotNull Integer id) {
log.info("删除AI配置,id: {}", id);
difyApiConfigService.deleteConfig(id);
return Result.success();
}
@Operation(summary = "测试配置", description = "测试AI配置的连接性")
@PostMapping("/{id}/test")
public Result<Boolean> testConfig(
@Parameter(description = "配置ID") @PathVariable @NotNull Integer id) {
log.info("测试AI配置连接,id: {}", id);
boolean testResult = difyApiConfigService.testConnection(id);
return Result.success(testResult);
}
@Operation(summary = "启用/禁用配置", description = "启用或禁用指定的AI配置")
@PutMapping("/{id}/toggle")
public Result<DifyApiConfig> toggleConfig(
@Parameter(description = "配置ID") @PathVariable @NotNull Integer id,
@Parameter(description = "是否启用") @RequestParam boolean active) {
log.info("切换AI配置状态,id: {}, active: {}", id, active);
DifyApiConfig config = difyApiConfigService.toggleActive(id, active);
return Result.success(config);
}
}
... ...
// ai/controller/PromptTemplateController.java
package com.aigeo.ai.controller;
import com.aigeo.ai.entity.PromptTemplate;
import com.aigeo.ai.service.PromptTemplateService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/prompt-templates")
public class PromptTemplateController {
@Autowired
private PromptTemplateService promptTemplateService;
@GetMapping
public List<PromptTemplate> getAllTemplates() {
return promptTemplateService.getAllTemplates();
}
@GetMapping("/{id}")
public ResponseEntity<PromptTemplate> getTemplateById(@PathVariable Integer id) {
Optional<PromptTemplate> template = promptTemplateService.getTemplateById(id);
return template.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/company/{companyId}")
public List<PromptTemplate> getTemplatesByCompanyId(@PathVariable Integer companyId) {
return promptTemplateService.getTemplatesByCompanyId(companyId);
}
@GetMapping("/system")
public List<PromptTemplate> getSystemTemplates() {
return promptTemplateService.getSystemTemplates();
}
@GetMapping("/active")
public List<PromptTemplate> getActiveTemplates() {
return promptTemplateService.getActiveTemplates();
}
@GetMapping("/company/{companyId}/active")
public List<PromptTemplate> getActiveTemplatesByCompanyId(@PathVariable Integer companyId) {
return promptTemplateService.getActiveTemplatesByCompanyId(companyId);
}
@PostMapping
public PromptTemplate createTemplate(@RequestBody PromptTemplate template) {
return promptTemplateService.saveTemplate(template);
}
@PutMapping("/{id}")
public ResponseEntity<PromptTemplate> updateTemplate(@PathVariable Integer id,
@RequestBody PromptTemplate templateDetails) {
Optional<PromptTemplate> template = promptTemplateService.getTemplateById(id);
if (template.isPresent()) {
PromptTemplate updatedTemplate = template.get();
updatedTemplate.setName(templateDetails.getName());
updatedTemplate.setDescription(templateDetails.getDescription());
updatedTemplate.setLanguage(templateDetails.getLanguage());
updatedTemplate.setContent(templateDetails.getContent());
updatedTemplate.setVariables(templateDetails.getVariables());
updatedTemplate.setIsActive(templateDetails.getIsActive());
PromptTemplate savedTemplate = promptTemplateService.saveTemplate(updatedTemplate);
return ResponseEntity.ok(savedTemplate);
} else {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteTemplate(@PathVariable Integer id) {
promptTemplateService.deleteTemplate(id);
return ResponseEntity.noContent().build();
}
}
... ...
// ai/controller/UploadedFileController.java
package com.aigeo.ai.controller;
import com.aigeo.ai.entity.UploadedFile;
import com.aigeo.common.enums.FileStatus;
import com.aigeo.common.enums.FileType;
import com.aigeo.ai.service.UploadedFileService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/api/uploaded-files")
public class UploadedFileController {
@Autowired
private UploadedFileService uploadedFileService;
@GetMapping
public List<UploadedFile> getAllFiles() {
return uploadedFileService.getAllFiles();
}
@GetMapping("/{id}")
public ResponseEntity<UploadedFile> getFileById(@PathVariable Integer id) {
Optional<UploadedFile> file = uploadedFileService.getFileById(id);
return file.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/company/{companyId}")
public List<UploadedFile> getFilesByCompanyId(@PathVariable Integer companyId) {
return uploadedFileService.getFilesByCompanyId(companyId);
}
@GetMapping("/user/{userId}")
public List<UploadedFile> getFilesByUserId(@PathVariable Integer userId) {
return uploadedFileService.getFilesByUserId(userId);
}
@GetMapping("/company/{companyId}/user/{userId}")
public List<UploadedFile> getFilesByCompanyIdAndUserId(@PathVariable Integer companyId,
@PathVariable Integer userId) {
return uploadedFileService.getFilesByCompanyIdAndUserId(companyId, userId);
}
@GetMapping("/type/{fileType}")
public List<UploadedFile> getFilesByFileType(@PathVariable FileType fileType) {
return uploadedFileService.getFilesByFileType(fileType);
}
@GetMapping("/status/{status}")
public List<UploadedFile> getFilesByStatus(@PathVariable FileStatus status) {
return uploadedFileService.getFilesByStatus(status);
}
@GetMapping("/company/{companyId}/status/{status}")
public List<UploadedFile> getFilesByCompanyIdAndStatus(@PathVariable Integer companyId,
@PathVariable FileStatus status) {
return uploadedFileService.getFilesByCompanyIdAndStatus(companyId, status);
}
@PostMapping
public UploadedFile createFile(@RequestBody UploadedFile file) {
return uploadedFileService.saveFile(file);
}
@PutMapping("/{id}")
public ResponseEntity<UploadedFile> updateFile(@PathVariable Integer id,
@RequestBody UploadedFile fileDetails) {
Optional<UploadedFile> file = uploadedFileService.getFileById(id);
if (file.isPresent()) {
UploadedFile updatedFile = file.get();
updatedFile.setFileName(fileDetails.getFileName());
updatedFile.setFilePath(fileDetails.getFilePath());
updatedFile.setFileType(fileDetails.getFileType());
updatedFile.setFileSize(fileDetails.getFileSize());
updatedFile.setMimeType(fileDetails.getMimeType());
updatedFile.setStatus(fileDetails.getStatus());
UploadedFile savedFile = uploadedFileService.saveFile(updatedFile);
return ResponseEntity.ok(savedFile);
} else {
return ResponseEntity.notFound().build();
}
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteFile(@PathVariable Integer id) {
uploadedFileService.deleteFile(id);
return ResponseEntity.noContent().build();
}
}
... ...
// ai/entity/DifyApiConfig.java
package com.aigeo.ai.entity;
import org.hibernate.annotations.JdbcTypeCode;
import java.sql.Types;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import com.aigeo.common.enums.AiProvider;
import java.time.LocalDateTime;
import java.util.Date;
/**
* DifyApiConfig类是一个实体类,用于映射数据库中的ai_dify_api_configs表。
* 该类存储了Dify API的配置信息,包括提供商、API密钥、模型参数等。
*
* @author AIGEO Team
* @since 1.0.0
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "ai_dify_api_configs")
public class DifyApiConfig {
/**
* 主键ID,采用自增策略
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
/**
* 公司ID,用于区分不同公司的配置
*/
@Column(name = "company_id")
private Integer companyId;
/**
* AI服务提供商,使用枚举类型存储
*/
@Enumerated(EnumType.STRING)
private AiProvider provider;
/**
* 配置名称,不能为空
*/
@NotBlank(message = "配置名称不能为空")
@Column(nullable = false)
private String name;
/**
* API的基础URL
*/
@Column(name = "base_url")
private String baseUrl;
/**
* API密钥,用于身份验证
*/
@Column(name = "api_key")
private String apiKey;
/**
* 模型名称,指定使用的具体模型
*/
@Column(name = "model_name")
private String modelName;
/**
* 温度参数,控制输出的随机性
*/
private Double temperature;
/**
* Top P参数,控制词汇采样的概率范围
*/
@Column(name = "top_p")
private Double topP;
/**
* 最大令牌数,限制生成内容的长度
*/
@Column(name = "max_tokens")
private Integer maxTokens;
/**
* 请求头信息,以JSON格式存储
*/
@Column(name = "request_headers")
private String requestHeaders;
/**
* 是否激活该配置
*/
@Column(name = "is_active")
private Boolean isActive;
/**
* 创建时间,使用时间戳类型
*/
@Column(name = "created_at")
private LocalDateTime createdAt;
/**
* 更新时间,使用时间戳类型
*/
@Column(name = "updated_at")
private LocalDateTime updatedAt;
/**
* 创建时间设置,新建实体时自动设置
*/
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
updatedAt = LocalDateTime.now();
if (isActive == null) {
isActive = true;
}
}
/**
* 更新时间设置,实体更新时自动设置
*/
@PreUpdate
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
}
... ...
// ai/entity/PromptTemplate.java
package com.aigeo.ai.entity;
import org.hibernate.annotations.JdbcTypeCode;
import java.sql.Types;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
/**
* 提示模板实体类
* 对应数据库表:ai_prompt_templates
*
* 存储AI生成内容时使用的提示词模板,包括:
* - 模板内容和变量定义
* - 语言和适用场景配置
* - 使用统计和版本管理
*
* @author AIGEO Team
* @since 1.0.0
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "ai_prompt_templates", indexes = {
@Index(name = "idx_prompt_templates_company", columnList = "company_id"),
@Index(name = "idx_prompt_templates_active", columnList = "is_active"),
@Index(name = "idx_prompt_templates_language", columnList = "language")
})
public class PromptTemplate {
/**
* 主键ID
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false, updatable = false)
private Integer id;
/**
* 公司ID(多租户字段)
*/
@NotNull(message = "公司ID不能为空")
@Column(name = "company_id", nullable = false)
private Integer companyId;
/**
* 模板名称
*/
@NotBlank(message = "模板名称不能为空")
@Column(name = "name", nullable = false, length = 200)
private String name;
/**
* 模板描述
*/
@Column(name = "description", length = 500)
private String description;
/**
* 模板语言
*/
@Column(name = "language", length = 10)
@Builder.Default
private String language = "zh-CN";
/**
* 模板内容
*/
@NotBlank(message = "模板内容不能为空")
@Column(name = "content", nullable = false, columnDefinition = "TEXT")
private String content;
/**
* 模板变量(JSON格式存储变量定义)
*/
@Column(name = "variables", columnDefinition = "JSON")
private String variables;
/**
* 模板类型(如:article, product, seo等)
*/
@Column(name = "template_type", length = 50)
@Builder.Default
private String templateType = "article";
/**
* 使用次数统计
*/
@Column(name = "usage_count")
@Builder.Default
private Integer usageCount = 0;
/**
* 是否激活
*/
@Column(name = "is_active")
@Builder.Default
private Boolean isActive = true;
/**
* 创建时间
*/
@CreationTimestamp
@Column(name = "created_at", updatable = false)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
/**
* 更新时间
*/
@UpdateTimestamp
@Column(name = "updated_at")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
/**
* 实体创建前的处理
*/
@PrePersist
protected void onCreate() {
if (isActive == null) isActive = true;
if (language == null) language = "zh-CN";
if (templateType == null) templateType = "article";
if (usageCount == null) usageCount = 0;
}
/**
* 检查是否为激活状态
*/
public boolean isActive() {
return Boolean.TRUE.equals(isActive);
}
/**
* 增加使用次数
*/
public void incrementUsageCount() {
this.usageCount = (usageCount == null ? 0 : usageCount) + 1;
}
/**
* 检查是否为热门模板(使用次数超过阈值)
*/
public boolean isPopularTemplate() {
return usageCount != null && usageCount >= 50;
}
/**
* 获取模板的复杂度评级(基于内容长度和变量数量)
*/
public String getComplexityLevel() {
if (content == null) return "SIMPLE";
int contentLength = content.length();
int variableCount = (variables != null && !variables.trim().isEmpty()) ?
variables.split(",").length : 0;
if (contentLength > 2000 || variableCount > 10) {
return "COMPLEX";
} else if (contentLength > 500 || variableCount > 3) {
return "MEDIUM";
} else {
return "SIMPLE";
}
}
/**
* 检查是否包含指定变量
*/
public boolean hasVariable(String variableName) {
if (variables == null || variables.trim().isEmpty()) {
return false;
}
return variables.contains("\"" + variableName + "\"") ||
variables.contains(variableName);
}
}
\ No newline at end of file
... ...
// ai/entity/UploadedFile.java
package com.aigeo.ai.entity;
import org.hibernate.annotations.JdbcTypeCode;
import java.sql.Types;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.Builder;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import com.aigeo.common.enums.FileType;
import com.aigeo.common.enums.FileStatus;
import java.time.LocalDateTime;
/**
* 上传文件实体类,对应数据库表 ai_uploaded_files
*
* 管理系统中上传的文件信息,包括文件元数据、存储路径、
* 文件类型、大小、校验和等信息,支持多租户和版本控制
*
* @author AIGEO Team
* @since 1.0.0
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "ai_uploaded_files", indexes = {
@Index(name = "idx_uploaded_files_company", columnList = "company_id"),
@Index(name = "idx_uploaded_files_user", columnList = "user_id"),
@Index(name = "idx_uploaded_files_type", columnList = "file_type"),
@Index(name = "idx_uploaded_files_status", columnList = "status"),
@Index(name = "idx_uploaded_files_created", columnList = "created_at")
})
public class UploadedFile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
/**
* 公司ID,多租户标识
*/
@NotNull(message = "公司ID不能为空")
@Column(name = "company_id", nullable = false)
private Integer companyId;
/**
* 用户ID,文件上传者
*/
@NotNull(message = "用户ID不能为空")
@Column(name = "user_id", nullable = false)
private Integer userId;
/**
* 原始文件名
*/
@NotBlank(message = "文件名不能为空")
@Column(name = "file_name", nullable = false, length = 255)
private String fileName;
/**
* 文件存储路径
*/
@NotBlank(message = "文件路径不能为空")
@Column(name = "file_path", nullable = false, length = 500)
private String filePath;
/**
* 文件类型枚举
*/
@NotNull(message = "文件类型不能为空")
@Enumerated(EnumType.STRING)
@Column(name = "file_type", nullable = false, length = 50)
private FileType fileType;
/**
* 文件大小,单位字节
*/
@Column(name = "file_size")
private Long fileSize;
/**
* MIME类型
*/
@Column(name = "mime_type", length = 100)
private String mimeType;
/**
* 文件校验和,用于文件完整性验证
*/
@Column(length = 128)
private String checksum;
/**
* 文件版本号,支持文件版本管理
*/
@Builder.Default
private Integer version = 1;
/**
* 文件状态
*/
@Enumerated(EnumType.STRING)
@Column(length = 20)
@Builder.Default
private FileStatus status = FileStatus.UPLOADED;
/**
* 创建时间
*/
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
/**
* 创建时间自动设置
*/
@PrePersist
protected void onCreate() {
createdAt = LocalDateTime.now();
}
/**
* 获取文件大小的可读格式
* @return 格式化的文件大小字符串
*/
public String getFormattedFileSize() {
if (fileSize == null) return "未知";
if (fileSize < 1024) return fileSize + " B";
if (fileSize < 1024 * 1024) return String.format("%.1f KB", fileSize / 1024.0);
if (fileSize < 1024 * 1024 * 1024) return String.format("%.1f MB", fileSize / (1024.0 * 1024.0));
return String.format("%.1f GB", fileSize / (1024.0 * 1024.0 * 1024.0));
}
/**
* 检查文件是否为图片类型
* @return 是否为图片
*/
public boolean isImage() {
return fileType == FileType.IMAGE ||
(mimeType != null && mimeType.startsWith("image/"));
}
/**
* 检查文件是否为文档类型
* @return 是否为文档
*/
public boolean isDocument() {
return fileType == FileType.DOCUMENT ||
(mimeType != null && (mimeType.contains("pdf") ||
mimeType.contains("word") ||
mimeType.contains("text")));
}
}
... ...
// ai/repository/DifyApiConfigRepository.java
package com.aigeo.ai.repository;
import com.aigeo.ai.entity.DifyApiConfig;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface DifyApiConfigRepository extends JpaRepository<DifyApiConfig, Integer> {
List<DifyApiConfig> findByCompanyId(Integer companyId);
List<DifyApiConfig> findByCompanyIdIsNull(); // 共享配置
List<DifyApiConfig> findByIsActiveTrue();
}
... ...
// ai/repository/PromptTemplateRepository.java
package com.aigeo.ai.repository;
import com.aigeo.ai.entity.PromptTemplate;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface PromptTemplateRepository extends JpaRepository<PromptTemplate, Integer> {
List<PromptTemplate> findByCompanyId(Integer companyId);
List<PromptTemplate> findByCompanyIdIsNull(); // 系统模板
List<PromptTemplate> findByIsActiveTrue();
List<PromptTemplate> findByCompanyIdAndIsActiveTrue(Integer companyId);
}
... ...
// ai/repository/UploadedFileRepository.java
package com.aigeo.ai.repository;
import com.aigeo.ai.entity.UploadedFile;
import com.aigeo.common.enums.FileStatus;
import com.aigeo.common.enums.FileType;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface UploadedFileRepository extends JpaRepository<UploadedFile, Integer> {
List<UploadedFile> findByCompanyId(Integer companyId);
List<UploadedFile> findByUserId(Integer userId);
List<UploadedFile> findByCompanyIdAndUserId(Integer companyId, Integer userId);
List<UploadedFile> findByFileType(FileType fileType);
List<UploadedFile> findByStatus(FileStatus status);
List<UploadedFile> findByCompanyIdAndStatus(Integer companyId, FileStatus status);
}
... ...
// ai/service/DifyApiConfigService.java
package com.aigeo.ai.service;
import com.aigeo.ai.entity.DifyApiConfig;
import com.aigeo.ai.repository.DifyApiConfigRepository;
import com.aigeo.common.exception.BusinessException;
import com.aigeo.common.result.ResultCode;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
/**
* Dify API配置服务类
*
* 负责管理AI配置的业务逻辑,包括配置的CRUD操作、
* 连接测试、状态管理等功能,支持多租户和权限控制
*
* @author AIGEO Team
* @since 1.0.0
*/
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class DifyApiConfigService {
private final DifyApiConfigRepository difyApiConfigRepository;
/**
* 获取所有配置
* @return 所有配置列表
*/
public List<DifyApiConfig> getAllConfigs() {
log.debug("获取所有AI配置");
return difyApiConfigRepository.findAll();
}
/**
* 根据公司ID获取配置列表
* @param companyId 公司ID
* @return 该公司的配置列表
* @throws BusinessException 当公司ID为空时抛出
*/
public List<DifyApiConfig> getConfigsByCompanyId(Integer companyId) {
log.debug("根据公司ID获取AI配置,companyId: {}", companyId);
if (companyId == null) {
throw new BusinessException(ResultCode.PARAM_ERROR, "公司ID不能为空");
}
return difyApiConfigRepository.findByCompanyId(companyId);
}
public List<DifyApiConfig> getSharedConfigs() {
return difyApiConfigRepository.findByCompanyIdIsNull();
}
public List<DifyApiConfig> getActiveConfigs() {
return difyApiConfigRepository.findByIsActiveTrue();
}
public Optional<DifyApiConfig> getConfigById(Integer id) {
return difyApiConfigRepository.findById(id);
}
public DifyApiConfig saveConfig(DifyApiConfig config) {
return difyApiConfigRepository.save(config);
}
public void deleteConfig(Integer id) {
difyApiConfigRepository.deleteById(id);
}
public Page<DifyApiConfig> findAll(Pageable pageable) {
return null;
}
public DifyApiConfig updateConfig(@NotNull Integer id, @Valid DifyApiConfig configDetails) {
return configDetails;
}
/**
* 测试API配置的连接性
* @param id 配置ID
* @return 连接是否成功
*/
public boolean testConnection(@NotNull Integer id) {
Optional<DifyApiConfig> configOpt = difyApiConfigRepository.findById(id);
if (!configOpt.isPresent()) {
return false;
}
DifyApiConfig config = configOpt.get();
if (config.getApiKey() == null || config.getBaseUrl() == null) {
return false;
}
// 这里应该实现具体的连接测试逻辑
// 比如发送HTTP请求到API端点进行验证
// 暂时返回true表示测试通过
return true;
}
/**
* 切换配置的激活状态
* @param id 配置ID
* @param active 目标状态
* @return 更新后的配置对象
* @throws BusinessException 当配置不存在时抛出
*/
@Transactional
public DifyApiConfig toggleActive(@NotNull Integer id, boolean active) {
Optional<DifyApiConfig> configOpt = difyApiConfigRepository.findById(id);
if (!configOpt.isPresent()) {
throw new BusinessException(ResultCode.DATA_NOT_FOUND, "配置不存在");
}
DifyApiConfig config = configOpt.get();
config.setIsActive(active);
return difyApiConfigRepository.save(config);
}
}
... ...
// ai/service/PromptTemplateService.java
package com.aigeo.ai.service;
import com.aigeo.ai.entity.PromptTemplate;
import com.aigeo.ai.repository.PromptTemplateRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class PromptTemplateService {
@Autowired
private PromptTemplateRepository promptTemplateRepository;
public List<PromptTemplate> getAllTemplates() {
return promptTemplateRepository.findAll();
}
public List<PromptTemplate> getTemplatesByCompanyId(Integer companyId) {
return promptTemplateRepository.findByCompanyId(companyId);
}
public List<PromptTemplate> getSystemTemplates() {
return promptTemplateRepository.findByCompanyIdIsNull();
}
public List<PromptTemplate> getActiveTemplates() {
return promptTemplateRepository.findByIsActiveTrue();
}
public List<PromptTemplate> getActiveTemplatesByCompanyId(Integer companyId) {
return promptTemplateRepository.findByCompanyIdAndIsActiveTrue(companyId);
}
public Optional<PromptTemplate> getTemplateById(Integer id) {
return promptTemplateRepository.findById(id);
}
public PromptTemplate saveTemplate(PromptTemplate template) {
return promptTemplateRepository.save(template);
}
public void deleteTemplate(Integer id) {
promptTemplateRepository.deleteById(id);
}
}
... ...
// ai/service/UploadedFileService.java
package com.aigeo.ai.service;
import com.aigeo.ai.entity.UploadedFile;
import com.aigeo.common.enums.FileStatus;
import com.aigeo.common.enums.FileType;
import com.aigeo.ai.repository.UploadedFileRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Service
public class UploadedFileService {
@Autowired
private UploadedFileRepository uploadedFileRepository;
public List<UploadedFile> getAllFiles() {
return uploadedFileRepository.findAll();
}
public List<UploadedFile> getFilesByCompanyId(Integer companyId) {
return uploadedFileRepository.findByCompanyId(companyId);
}
public List<UploadedFile> getFilesByUserId(Integer userId) {
return uploadedFileRepository.findByUserId(userId);
}
public List<UploadedFile> getFilesByCompanyIdAndUserId(Integer companyId, Integer userId) {
return uploadedFileRepository.findByCompanyIdAndUserId(companyId, userId);
}
public List<UploadedFile> getFilesByFileType(FileType fileType) {
return uploadedFileRepository.findByFileType(fileType);
}
public List<UploadedFile> getFilesByStatus(FileStatus status) {
return uploadedFileRepository.findByStatus(status);
}
public List<UploadedFile> getFilesByCompanyIdAndStatus(Integer companyId, FileStatus status) {
return uploadedFileRepository.findByCompanyIdAndStatus(companyId, status);
}
public Optional<UploadedFile> getFileById(Integer id) {
return uploadedFileRepository.findById(id);
}
public UploadedFile saveFile(UploadedFile file) {
return uploadedFileRepository.save(file);
}
public void deleteFile(Integer id) {
uploadedFileRepository.deleteById(id);
}
}
... ...
package com.aigeo.article.controller;
import com.aigeo.article.entity.ArticleGenerationTask;
import com.aigeo.article.service.ArticleGenerationService;
import com.aigeo.common.enums.TaskStatus;
import com.aigeo.common.result.Result;
import com.aigeo.common.result.ResultCode;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.List;
/**
* 文章生成任务管理控制器
*
* @author AIGEO Team
* @since 1.0.0
*/
@Tag(name = "文章生成任务管理", description = "AI文章生成任务管理接口,支持任务的创建、查询、更新和删除操作")
@RestController
@RequestMapping("/api/article-tasks")
@RequiredArgsConstructor
@Slf4j
public class ArticleGenerationTaskController {
private final ArticleGenerationService articleGenerationService;
@Operation(
summary = "分页查询文章生成任务列表",
description = "支持按任务状态、用户、公司等条件分页查询文章生成任务列表,默认按创建时间倒序排列"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "查询成功"),
@ApiResponse(responseCode = "400", description = "请求参数错误"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@GetMapping("/list")
public Result<Page<ArticleGenerationTask>> listTasks(
@Parameter(description = "页码,从0开始", example = "0")
@RequestParam(defaultValue = "0") int page,
@Parameter(description = "页大小,默认10条", example = "10")
@RequestParam(defaultValue = "10") int size,
@Parameter(description = "公司ID(可选)")
@RequestParam(required = false) Integer companyId,
@Parameter(description = "用户ID(可选)")
@RequestParam(required = false) Integer userId,
@Parameter(description = "任务状态")
@RequestParam(required = false) TaskStatus status,
@Parameter(description = "文章主题(模糊查询)")
@RequestParam(required = false) String articleTheme,
@Parameter(description = "开始时间(格式:yyyy-MM-ddTHH:mm:ss)")
@RequestParam(required = false) String startTime,
@Parameter(description = "结束时间(格式:yyyy-MM-ddTHH:mm:ss)")
@RequestParam(required = false) String endTime
) {
try {
Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt"));
LocalDateTime start = startTime != null ? LocalDateTime.parse(startTime) : null;
LocalDateTime end = endTime != null ? LocalDateTime.parse(endTime) : null;
Page<ArticleGenerationTask> tasks = articleGenerationService.searchTasks(
companyId, userId, status, articleTheme, start, end, pageable
);
return Result.success("查询成功", tasks);
} catch (Exception e) {
log.error("分页查询文章生成任务列表失败", e);
return Result.error("查询失败");
}
}
@Operation(
summary = "获取所有文章生成任务(简化版)",
description = "获取所有文章生成任务的基本信息,用于下拉选择等场景"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "查询成功"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@GetMapping
public Result<List<ArticleGenerationTask>> getAllTasks() {
try {
List<ArticleGenerationTask> tasks = articleGenerationService.getAllTasks();
return Result.success("查询成功", tasks);
} catch (Exception e) {
log.error("获取所有文章生成任务失败", e);
return Result.error("查询失败");
}
}
@Operation(
summary = "根据ID查询文章生成任务详情",
description = "通过任务ID获取文章生成任务的详细信息"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "查询成功"),
@ApiResponse(responseCode = "404", description = "任务不存在"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@GetMapping("/{id}")
public Result<ArticleGenerationTask> getTaskById(
@Parameter(description = "任务ID", required = true, example = "1")
@PathVariable @NotNull Integer id
) {
try {
return articleGenerationService.getTaskById(id)
.map(task -> Result.success("查询成功", task))
.orElse(Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在"));
} catch (Exception e) {
log.error("根据ID查询文章生成任务详情失败, id: {}", id, e);
return Result.error("查询失败");
}
}
@Operation(
summary = "根据公司ID查询文章生成任务",
description = "获取指定公司下的所有文章生成任务"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "查询成功"),
@ApiResponse(responseCode = "404", description = "公司不存在"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@GetMapping("/company/{companyId}")
public Result<List<ArticleGenerationTask>> getTasksByCompanyId(
@Parameter(description = "公司ID", required = true, example = "1")
@PathVariable @NotNull Integer companyId
) {
try {
List<ArticleGenerationTask> tasks = articleGenerationService.getTasksByCompanyId(companyId);
return Result.success("查询成功", tasks);
} catch (Exception e) {
log.error("根据公司ID查询文章生成任务失败, companyId: {}", companyId, e);
return Result.error("查询失败");
}
}
@Operation(
summary = "根据用户ID查询文章生成任务",
description = "获取指定用户创建的所有文章生成任务"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "查询成功"),
@ApiResponse(responseCode = "404", description = "用户不存在"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@GetMapping("/user/{userId}")
public Result<List<ArticleGenerationTask>> getTasksByUserId(
@Parameter(description = "用户ID", required = true, example = "1")
@PathVariable @NotNull Integer userId
) {
try {
List<ArticleGenerationTask> tasks = articleGenerationService.getTasksByUserId(userId);
return Result.success("查询成功", tasks);
} catch (Exception e) {
log.error("根据用户ID查询文章生成任务失败, userId: {}", userId, e);
return Result.error("查询失败");
}
}
@Operation(
summary = "根据任务状态查询文章生成任务",
description = "获取指定状态的所有文章生成任务"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "查询成功"),
@ApiResponse(responseCode = "400", description = "请求参数错误"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@GetMapping("/status/{status}")
public Result<List<ArticleGenerationTask>> getTasksByStatus(
@Parameter(description = "任务状态", required = true)
@PathVariable @NotNull TaskStatus status
) {
try {
List<ArticleGenerationTask> tasks = articleGenerationService.getTasksByStatus(status);
return Result.success("查询成功", tasks);
} catch (Exception e) {
log.error("根据任务状态查询文章生成任务失败, status: {}", status, e);
return Result.error("查询失败");
}
}
@Operation(
summary = "获取进行中的任务",
description = "获取所有状态为进行中的文章生成任务"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "查询成功"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@GetMapping("/running")
public Result<List<ArticleGenerationTask>> getRunningTasks(
@Parameter(description = "公司ID(可选)")
@RequestParam(required = false) Integer companyId
) {
try {
List<ArticleGenerationTask> tasks = articleGenerationService.getRunningTasks(companyId);
return Result.success("查询成功", tasks);
} catch (Exception e) {
log.error("获取进行中的任务失败", e);
return Result.error("查询失败");
}
}
@Operation(
summary = "获取最近完成的任务",
description = "获取最近完成的文章生成任务列表"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "查询成功"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@GetMapping("/recent-completed")
public Result<List<ArticleGenerationTask>> getRecentCompletedTasks(
@Parameter(description = "公司ID(可选)")
@RequestParam(required = false) Integer companyId,
@Parameter(description = "返回数量", example = "10")
@RequestParam(defaultValue = "10") int limit
) {
try {
List<ArticleGenerationTask> tasks = articleGenerationService.getRecentCompletedTasks(companyId, limit);
return Result.success("查询成功", tasks);
} catch (Exception e) {
log.error("获取最近完成的任务失败", e);
return Result.error("查询失败");
}
}
@Operation(
summary = "创建新的文章生成任务",
description = "创建一个新的文章生成任务,包含生成参数和配置"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "创建成功"),
@ApiResponse(responseCode = "400", description = "请求参数错误"),
@ApiResponse(responseCode = "429", description = "任务队列已满"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@PostMapping
public Result<ArticleGenerationTask> createTask(
@Parameter(description = "文章生成任务信息", required = true)
@Valid @RequestBody ArticleGenerationTask task
) {
try {
// 检查任务队列是否已满
if (articleGenerationService.isTaskQueueFull(task.getCompanyId())) {
return Result.error(ResultCode.TOO_MANY_REQUESTS, "任务队列已满,请稍后再试");
}
ArticleGenerationTask savedTask = articleGenerationService.saveTask(task);
log.info("成功创建文章生成任务: {} (用户ID: {})", savedTask.getArticleTheme(), savedTask.getUserId());
return Result.success("创建成功", savedTask);
} catch (Exception e) {
log.error("创建文章生成任务失败", e);
return Result.error("创建失败");
}
}
@Operation(
summary = "批量创建文章生成任务",
description = "批量创建多个文章生成任务"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "批量创建成功"),
@ApiResponse(responseCode = "400", description = "请求参数错误"),
@ApiResponse(responseCode = "429", description = "任务数量超限"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@PostMapping("/batch")
public Result<List<ArticleGenerationTask>> batchCreateTasks(
@Parameter(description = "文章生成任务列表", required = true)
@Valid @RequestBody List<ArticleGenerationTask> tasks
) {
try {
if (tasks.size() > 50) {
return Result.error(ResultCode.BAD_REQUEST, "单次批量创建任务数量不能超过50个");
}
List<ArticleGenerationTask> savedTasks = articleGenerationService.batchSaveTasks(tasks);
log.info("成功批量创建文章生成任务,数量: {}", savedTasks.size());
return Result.success("批量创建成功", savedTasks);
} catch (Exception e) {
log.error("批量创建文章生成任务失败", e);
return Result.error("批量创建失败");
}
}
@Operation(
summary = "更新文章生成任务",
description = "根据ID更新文章生成任务的详细信息"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "更新成功"),
@ApiResponse(responseCode = "400", description = "请求参数错误"),
@ApiResponse(responseCode = "404", description = "任务不存在"),
@ApiResponse(responseCode = "409", description = "任务状态不允许更新"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@PutMapping("/{id}")
public Result<ArticleGenerationTask> updateTask(
@Parameter(description = "任务ID", required = true, example = "1")
@PathVariable @NotNull Integer id,
@Parameter(description = "更新的任务信息", required = true)
@Valid @RequestBody ArticleGenerationTask taskDetails
) {
try {
return articleGenerationService.getTaskById(id)
.map(existingTask -> {
// 检查任务状态是否允许更新
if (existingTask.getStatus() == TaskStatus.COMPLETED) {
return Result.<ArticleGenerationTask>error(ResultCode.CONFLICT, "已完成的任务不允许更新");
}
// 更新字段
existingTask.setArticleTheme(taskDetails.getArticleTheme());
existingTask.setStatus(taskDetails.getStatus());
existingTask.setProgress(taskDetails.getProgress());
ArticleGenerationTask savedTask = articleGenerationService.saveTask(existingTask);
log.info("成功更新文章生成任务: {}", savedTask.getArticleTheme());
return Result.success("更新成功", savedTask);
})
.orElse(Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在"));
} catch (Exception e) {
log.error("更新文章生成任务失败, id: {}", id, e);
return Result.error("更新失败");
}
}
@Operation(
summary = "启动文章生成任务",
description = "启动指定的文章生成任务"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "启动成功"),
@ApiResponse(responseCode = "404", description = "任务不存在"),
@ApiResponse(responseCode = "409", description = "任务状态不允许启动"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@PostMapping("/{id}/start")
public Result<String> startTask(
@Parameter(description = "任务ID", required = true, example = "1")
@PathVariable @NotNull Integer id
) {
try {
if (!articleGenerationService.existsById(id)) {
return Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在");
}
boolean started = articleGenerationService.startTask(id);
if (started) {
log.info("成功启动文章生成任务, id: {}", id);
return Result.success("任务启动成功");
} else {
return Result.error(ResultCode.CONFLICT, "任务状态不允许启动");
}
} catch (Exception e) {
log.error("启动文章生成任务失败, id: {}", id, e);
return Result.error("启动失败");
}
}
@Operation(
summary = "暂停文章生成任务",
description = "暂停正在运行的文章生成任务"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "暂停成功"),
@ApiResponse(responseCode = "404", description = "任务不存在"),
@ApiResponse(responseCode = "409", description = "任务状态不允许暂停"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@PostMapping("/{id}/pause")
public Result<String> pauseTask(
@Parameter(description = "任务ID", required = true, example = "1")
@PathVariable @NotNull Integer id
) {
try {
if (!articleGenerationService.existsById(id)) {
return Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在");
}
boolean paused = articleGenerationService.pauseTask(id);
if (paused) {
log.info("成功暂停文章生成任务, id: {}", id);
return Result.success("任务暂停成功");
} else {
return Result.error(ResultCode.CONFLICT, "任务状态不允许暂停");
}
} catch (Exception e) {
log.error("暂停文章生成任务失败, id: {}", id, e);
return Result.error("暂停失败");
}
}
@Operation(
summary = "取消文章生成任务",
description = "取消指定的文章生成任务"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "取消成功"),
@ApiResponse(responseCode = "404", description = "任务不存在"),
@ApiResponse(responseCode = "409", description = "任务状态不允许取消"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@PostMapping("/{id}/cancel")
public Result<String> cancelTask(
@Parameter(description = "任务ID", required = true, example = "1")
@PathVariable @NotNull Integer id
) {
try {
if (!articleGenerationService.existsById(id)) {
return Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在");
}
boolean cancelled = articleGenerationService.cancelTask(id);
if (cancelled) {
log.info("成功取消文章生成任务, id: {}", id);
return Result.success("任务取消成功");
} else {
return Result.error(ResultCode.CONFLICT, "任务状态不允许取消");
}
} catch (Exception e) {
log.error("取消文章生成任务失败, id: {}", id, e);
return Result.error("取消失败");
}
}
@Operation(
summary = "重试失败的文章生成任务",
description = "重新启动失败的文章生成任务"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "重试成功"),
@ApiResponse(responseCode = "404", description = "任务不存在"),
@ApiResponse(responseCode = "409", description = "任务状态不允许重试"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@PostMapping("/{id}/retry")
public Result<String> retryTask(
@Parameter(description = "任务ID", required = true, example = "1")
@PathVariable @NotNull Integer id
) {
try {
if (!articleGenerationService.existsById(id)) {
return Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在");
}
boolean retried = articleGenerationService.retryTask(id);
if (retried) {
log.info("成功重试文章生成任务, id: {}", id);
return Result.success("任务重试成功");
} else {
return Result.error(ResultCode.CONFLICT, "任务状态不允许重试");
}
} catch (Exception e) {
log.error("重试文章生成任务失败, id: {}", id, e);
return Result.error("重试失败");
}
}
@Operation(
summary = "删除文章生成任务",
description = "根据ID删除文章生成任务记录(软删除)"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "删除成功"),
@ApiResponse(responseCode = "404", description = "任务不存在"),
@ApiResponse(responseCode = "409", description = "任务正在运行,无法删除"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@DeleteMapping("/{id}")
public Result<String> deleteTask(
@Parameter(description = "任务ID", required = true, example = "1")
@PathVariable @NotNull Integer id
) {
try {
if (!articleGenerationService.existsById(id)) {
return Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在");
}
// 检查任务是否正在运行
if (articleGenerationService.isTaskRunning(id)) {
return Result.error(ResultCode.CONFLICT, "任务正在运行,无法删除");
}
articleGenerationService.deleteTask(id);
log.info("成功删除文章生成任务, id: {}", id);
return Result.success("删除成功");
} catch (Exception e) {
log.error("删除文章生成任务失败, id: {}", id, e);
return Result.error("删除失败");
}
}
@Operation(
summary = "获取任务执行日志",
description = "获取文章生成任务的执行日志信息"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "查询成功"),
@ApiResponse(responseCode = "404", description = "任务不存在"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@GetMapping("/{id}/logs")
public Result<List<String>> getTaskLogs(
@Parameter(description = "任务ID", required = true, example = "1")
@PathVariable @NotNull Integer id,
@Parameter(description = "日志级别(INFO/WARN/ERROR)")
@RequestParam(required = false) String level,
@Parameter(description = "最大返回行数", example = "100")
@RequestParam(defaultValue = "100") int limit
) {
try {
if (!articleGenerationService.existsById(id)) {
return Result.error(ResultCode.DATA_NOT_FOUND, "任务不存在");
}
List<String> logs = articleGenerationService.getTaskLogs(id, level, limit);
return Result.success("查询成功", logs);
} catch (Exception e) {
log.error("获取任务执行日志失败, id: {}", id, e);
return Result.error("查询失败");
}
}
@Operation(
summary = "获取任务统计信息",
description = "获取文章生成任务相关的统计数据"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "统计成功"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@GetMapping("/statistics")
public Result<TaskStatistics> getTaskStatistics(
@Parameter(description = "公司ID(可选)")
@RequestParam(required = false) Integer companyId,
@Parameter(description = "统计时间范围(天数)", example = "30")
@RequestParam(defaultValue = "30") int days
) {
try {
ArticleGenerationService.TaskStatistics serviceStats = articleGenerationService.getTaskStatistics(companyId, days);
TaskStatistics statistics = new TaskStatistics(
serviceStats.getTotalTasks(),
serviceStats.getPendingTasks(),
serviceStats.getRunningTasks(),
serviceStats.getCompletedTasks(),
serviceStats.getFailedTasks(),
serviceStats.getSuccessRate(),
serviceStats.getAverageExecutionTime()
);
return Result.success("统计成功", statistics);
} catch (Exception e) {
log.error("获取任务统计信息失败", e);
return Result.error("统计失败");
}
}
@Operation(
summary = "清理已完成的任务",
description = "清理指定天数前完成的文章生成任务"
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "清理成功"),
@ApiResponse(responseCode = "400", description = "请求参数错误"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
@DeleteMapping("/cleanup")
public Result<String> cleanupCompletedTasks(
@Parameter(description = "保留天数", required = true, example = "30")
@RequestParam @NotNull Integer retentionDays
) {
try {
if (retentionDays < 1) {
return Result.error(ResultCode.BAD_REQUEST, "保留天数必须大于0");
}
int cleanedCount = articleGenerationService.cleanupCompletedTasks(retentionDays);
log.info("清理已完成的任务成功,清理数量: {}", cleanedCount);
return Result.success(String.format("成功清理 %d 个已完成的任务", cleanedCount));
} catch (Exception e) {
log.error("清理已完成的任务失败", e);
return Result.error("清理失败");
}
}
/**
* 任务统计信息DTO
*/
public static class TaskStatistics {
private long totalTasks;
private long pendingTasks;
private long runningTasks;
private long completedTasks;
private long failedTasks;
private double successRate;
private double averageExecutionTime;
// constructors, getters and setters
public TaskStatistics(long totalTasks, long pendingTasks, long runningTasks,
long completedTasks, long failedTasks, double successRate,
double averageExecutionTime) {
this.totalTasks = totalTasks;
this.pendingTasks = pendingTasks;
this.runningTasks = runningTasks;
this.completedTasks = completedTasks;
this.failedTasks = failedTasks;
this.successRate = successRate;
this.averageExecutionTime = averageExecutionTime;
}
// getters and setters
public long getTotalTasks() { return totalTasks; }
public void setTotalTasks(long totalTasks) { this.totalTasks = totalTasks; }
public long getPendingTasks() { return pendingTasks; }
public void setPendingTasks(long pendingTasks) { this.pendingTasks = pendingTasks; }
public long getRunningTasks() { return runningTasks; }
public void setRunningTasks(long runningTasks) { this.runningTasks = runningTasks; }
public long getCompletedTasks() { return completedTasks; }
public void setCompletedTasks(long completedTasks) { this.completedTasks = completedTasks; }
public long getFailedTasks() { return failedTasks; }
public void setFailedTasks(long failedTasks) { this.failedTasks = failedTasks; }
public double getSuccessRate() { return successRate; }
public void setSuccessRate(double successRate) { this.successRate = successRate; }
public double getAverageExecutionTime() { return averageExecutionTime; }
public void setAverageExecutionTime(double averageExecutionTime) { this.averageExecutionTime = averageExecutionTime; }
}
}
\ No newline at end of file
... ...
package com.aigeo.article.entity;
import com.aigeo.common.enums.AiTasteLevel;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
/**
* 文章生成配置实体类
* 对应数据库表:ai_article_generation_configs
*
* 用于存储文章生成的各种配置参数,包括:
* - AI写作风格和复杂度设置
* - 文章结构和长度配置
* - SEO优化参数设置
* - 输出格式和样式配置
*
* @author AIGEO Team
* @since 1.0.0
*/
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "ai_article_generation_configs")
public class ArticleGenerationConfig {
/**
* 主键ID
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false, updatable = false)
private Integer id;
/**
* 公司ID(多租户字段)
*/
@NotNull(message = "公司ID不能为空")
@Column(name = "company_id", nullable = false)
private Integer companyId;
/**
* 配置名称
*/
@NotBlank(message = "配置名称不能为空")
@Column(name = "config_name", nullable = false, length = 100)
private String configName;
/**
* AI写作风格等级
*/
@Enumerated(EnumType.STRING)
@Column(name = "ai_taste_level")
@Builder.Default
private AiTasteLevel aiTasteLevel = AiTasteLevel.JUNIOR_HIGH;
/**
* 目标字数(范围的最小值)
*/
@Column(name = "target_word_count_min")
@Builder.Default
private Integer targetWordCountMin = 800;
/**
* 目标字数(范围的最大值)
*/
@Column(name = "target_word_count_max")
@Builder.Default
private Integer targetWordCountMax = 1500;
/**
* 是否包含FAQ部分
*/
@Column(name = "include_faq")
@Builder.Default
private Boolean includeFaq = true;
/**
* 是否生成结构化数据(Schema.org)
*/
@Column(name = "generate_structured_data")
@Builder.Default
private Boolean generateStructuredData = true;
/**
* 是否包含相关链接
*/
@Column(name = "include_related_links")
@Builder.Default
private Boolean includeRelatedLinks = true;
/**
* SEO标题模板
*/
@Column(name = "seo_title_template", length = 500)
private String seoTitleTemplate;
/**
* SEO描述模板
*/
@Column(name = "seo_description_template", length = 500)
private String seoDescriptionTemplate;
/**
* 文章标签模板(用逗号分隔)
*/
@Column(name = "article_tags_template", length = 500)
private String articleTagsTemplate;
/**
* 自定义提示词
*/
@Column(name = "custom_prompt", columnDefinition = "TEXT")
private String customPrompt;
/**
* 语言设置
*/
@Column(name = "language", length = 10)
@Builder.Default
private String language = "zh-CN";
/**
* 输出格式(markdown, html等)
*/
@Column(name = "output_format", length = 20)
@Builder.Default
private String outputFormat = "markdown";
/**
* 是否为默认配置
*/
@Column(name = "is_default")
@Builder.Default
private Boolean isDefault = false;
/**
* 是否启用
*/
@Column(name = "is_active")
@Builder.Default
private Boolean isActive = true;
/**
* 配置描述
*/
@Column(name = "description")
private String description;
/**
* 创建时间
*/
@CreationTimestamp
@Column(name = "created_at", updatable = false)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
/**
* 更新时间
*/
@UpdateTimestamp
@Column(name = "updated_at")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
/**
* 实体创建前的处理
*/
@PrePersist
protected void onCreate() {
if (isActive == null) isActive = true;
if (isDefault == null) isDefault = false;
if (includeFaq == null) includeFaq = true;
if (generateStructuredData == null) generateStructuredData = true;
if (includeRelatedLinks == null) includeRelatedLinks = true;
if (aiTasteLevel == null) aiTasteLevel = AiTasteLevel.JUNIOR_HIGH;
if (language == null) language = "zh-CN";
if (outputFormat == null) outputFormat = "markdown";
if (targetWordCountMin == null) targetWordCountMin = 800;
if (targetWordCountMax == null) targetWordCountMax = 1500;
}
/**
* 获取目标字数范围描述
*/
public String getWordCountRange() {
return targetWordCountMin + "-" + targetWordCountMax + "字";
}
/**
* 检查是否为完整配置(包含所有必要参数)
*/
public boolean isComplete() {
return configName != null && !configName.trim().isEmpty()
&& aiTasteLevel != null
&& targetWordCountMin != null && targetWordCountMin > 0
&& targetWordCountMax != null && targetWordCountMax > targetWordCountMin;
}
/**
* 获取配置的复杂度评分(1-5分)
*/
public int getComplexityScore() {
int score = aiTasteLevel != null ? aiTasteLevel.getComplexityLevel() : 2;
// 根据配置的复杂程度调整评分
if (Boolean.TRUE.equals(generateStructuredData)) score += 1;
if (Boolean.TRUE.equals(includeFaq)) score += 1;
if (customPrompt != null && !customPrompt.trim().isEmpty()) score += 1;
return Math.min(score, 5); // 最高5分
}
}
\ No newline at end of file
... ...
package com.aigeo.article.entity;
import com.aigeo.common.enums.TaskStatus;
import com.aigeo.common.enums.AiTasteLevel;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
/**
* 文章生成任务实体类
* 对应数据库表:ai_article_generation_tasks
*
* 用于存储AI文章生成任务信息,包括:
* - 任务配置和参数设置
* - 执行状态和进度跟踪
* - 参考资料和知识来源
* - 生成结果和错误信息
*
* @author AIGEO Team
* @since 1.0.0
*/
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "ai_article_generation_tasks", indexes = {
@Index(name = "idx_tasks_company_status", columnList = "company_id, status"),
@Index(name = "idx_tasks_user_id", columnList = "user_id"),
@Index(name = "idx_tasks_created_at", columnList = "created_at DESC")
})
public class ArticleGenerationTask {
/**
* 主键ID
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false, updatable = false)
private Integer id;
/**
* 公司ID(多租户字段)
*/
@NotNull(message = "公司ID不能为空")
@Column(name = "company_id", nullable = false)
private Integer companyId;
/**
* 创建用户ID
*/
@NotNull(message = "用户ID不能为空")
@Column(name = "user_id", nullable = false)
private Integer userId;
/**
* 生成配置ID
*/
@Column(name = "config_id")
private Integer configId;
/**
* 关键词ID
*/
@Column(name = "keyword_id")
private Integer keywordId;
/**
* 文章主题
*/
@NotBlank(message = "文章主题不能为空")
@Column(name = "article_theme", nullable = false, length = 500)
private String articleTheme;
/**
* 关联话题ID列表(逗号分隔)
*/
@Column(name = "topic_ids", length = 500)
private String topicIds;
/**
* 参考资料URL列表(逗号分隔)
*/
@Column(name = "reference_urls", columnDefinition = "TEXT")
private String referenceUrls;
/**
* 参考内容
*/
@Column(name = "reference_content", columnDefinition = "LONGTEXT")
private String referenceContent;
/**
* AI写作风格等级
*/
@Enumerated(EnumType.STRING)
@Column(name = "ai_taste_level")
@Builder.Default
private AiTasteLevel aiTasteLevel = AiTasteLevel.JUNIOR_HIGH;
/**
* 任务状态
*/
@Enumerated(EnumType.STRING)
@Column(name = "status")
@Builder.Default
private TaskStatus status = TaskStatus.PENDING;
/**
* 任务进度(0-100)
*/
@Column(name = "progress")
@Builder.Default
private Integer progress = 0;
/**
* 错误信息
*/
@Column(name = "error_message", length = 2000)
private String errorMessage;
/**
* 生成的文章ID
*/
@Column(name = "generated_article_id")
private Integer generatedArticleId;
/**
* AI配置ID
*/
@Column(name = "dify_api_config_id")
private Integer difyApiConfigId;
/**
* 提示模板ID
*/
@Column(name = "prompt_template_id")
private Integer promptTemplateId;
/**
* 目标字数
*/
@Column(name = "target_word_count")
@Builder.Default
private Integer targetWordCount = 1000;
/**
* 生成语言
*/
@Column(name = "language", length = 10)
@Builder.Default
private String language = "zh-CN";
/**
* 是否包含FAQ
*/
@Column(name = "include_faq")
@Builder.Default
private Boolean includeFaq = false;
/**
* 是否生成SEO数据
*/
@Column(name = "generate_seo")
@Builder.Default
private Boolean generateSeo = true;
/**
* 执行时长(毫秒)
*/
@Column(name = "execution_time_ms")
private Long executionTimeMs;
/**
* 重试次数
*/
@Column(name = "retry_count")
@Builder.Default
private Integer retryCount = 0;
/**
* 最大重试次数
*/
@Column(name = "max_retries")
@Builder.Default
private Integer maxRetries = 3;
/**
* 任务优先级(1-10,数字越大优先级越高)
*/
@Column(name = "priority")
@Builder.Default
private Integer priority = 5;
/**
* 创建时间
*/
@CreationTimestamp
@Column(name = "created_at", updatable = false)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
/**
* 开始执行时间
*/
@Column(name = "started_at")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startedAt;
/**
* 完成时间
*/
@Column(name = "completed_at")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime completedAt;
/**
* 更新时间
*/
@UpdateTimestamp
@Column(name = "updated_at")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
/**
* 实体创建前的处理
*/
@PrePersist
protected void onCreate() {
if (status == null) status = TaskStatus.PENDING;
if (progress == null) progress = 0;
if (aiTasteLevel == null) aiTasteLevel = AiTasteLevel.JUNIOR_HIGH;
if (targetWordCount == null) targetWordCount = 1000;
if (language == null) language = "zh-CN";
if (includeFaq == null) includeFaq = false;
if (generateSeo == null) generateSeo = true;
if (retryCount == null) retryCount = 0;
if (maxRetries == null) maxRetries = 3;
if (priority == null) priority = 5;
}
/**
* 检查任务是否已完成
*/
public boolean isCompleted() {
return status == TaskStatus.COMPLETED || status == TaskStatus.FAILED || status == TaskStatus.CANCELLED;
}
/**
* 检查任务是否正在运行
*/
public boolean isRunning() {
return status == TaskStatus.PROCESSING;
}
/**
* 检查是否可以重试
*/
public boolean canRetry() {
return status == TaskStatus.FAILED && retryCount < maxRetries;
}
/**
* 检查是否可以取消
*/
public boolean isCancellable() {
return status == TaskStatus.PENDING || status == TaskStatus.PROCESSING;
}
/**
* 开始任务执行
*/
public void start() {
this.status = TaskStatus.PROCESSING;
this.startedAt = LocalDateTime.now();
this.progress = 0;
}
/**
* 完成任务
*/
public void complete(Integer articleId) {
this.status = TaskStatus.COMPLETED;
this.completedAt = LocalDateTime.now();
this.progress = 100;
this.generatedArticleId = articleId;
if (startedAt != null) {
this.executionTimeMs = java.time.Duration.between(startedAt, completedAt).toMillis();
}
}
/**
* 任务失败
*/
public void fail(String errorMessage) {
this.status = TaskStatus.FAILED;
this.completedAt = LocalDateTime.now();
this.errorMessage = errorMessage;
if (startedAt != null) {
this.executionTimeMs = java.time.Duration.between(startedAt, completedAt).toMillis();
}
}
/**
* 取消任务
*/
public void cancel(String reason) {
this.status = TaskStatus.CANCELLED;
this.completedAt = LocalDateTime.now();
this.errorMessage = "任务已取消: " + reason;
}
/**
* 增加重试次数
*/
public void incrementRetryCount() {
this.retryCount = (retryCount == null ? 0 : retryCount) + 1;
}
/**
* 更新进度
*/
public void updateProgress(Integer progress) {
if (progress >= 0 && progress <= 100) {
this.progress = progress;
}
}
/**
* 获取任务执行时长(秒)
*/
public Long getExecutionTimeSeconds() {
if (executionTimeMs == null) return null;
return executionTimeMs / 1000;
}
/**
* 获取任务优先级描述
*/
public String getPriorityDescription() {
if (priority == null) return "普通";
if (priority <= 3) return "低";
if (priority <= 7) return "普通";
return "高";
}
}
... ...
package com.aigeo.article.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
/**
* 文章类型实体类
* 对应数据库表:ai_article_types
*
* 用于定义不同类型的文章分类,如:
* - 新闻资讯类文章
* - 产品介绍类文章
* - 教程指南类文章
* - SEO优化类文章
* - 行业分析类文章
*
* @author AIGEO Team
* @since 1.0.0
*/
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "ai_article_types", indexes = {
@Index(name = "uk_article_types_company_name", columnList = "company_id, name", unique = true)
})
public class ArticleType {
/**
* 主键ID
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false, updatable = false)
private Integer id;
/**
* 公司ID(多租户字段)
*/
@NotNull(message = "公司ID不能为空")
@Column(name = "company_id", nullable = false)
private Integer companyId;
/**
* 文章类型名称
*/
@NotBlank(message = "文章类型名称不能为空")
@Column(name = "name", nullable = false, length = 100)
private String name;
/**
* 类型描述
*/
@Column(name = "description", length = 500)
private String description;
/**
* 类型标识代码(英文标识,用于程序识别)
*/
@Column(name = "code", length = 50)
private String code;
/**
* 默认标题模板
*/
@Column(name = "default_title_template", length = 500)
private String defaultTitleTemplate;
/**
* 默认内容模板
*/
@Column(name = "default_content_template", columnDefinition = "TEXT")
private String defaultContentTemplate;
/**
* 默认标签(逗号分隔)
*/
@Column(name = "default_tags", length = 500)
private String defaultTags;
/**
* 推荐字数范围(最小值)
*/
@Column(name = "recommended_word_count_min")
@Builder.Default
private Integer recommendedWordCountMin = 800;
/**
* 推荐字数范围(最大值)
*/
@Column(name = "recommended_word_count_max")
@Builder.Default
private Integer recommendedWordCountMax = 1500;
/**
* SEO权重(影响搜索引擎优化的重要性,1-10分)
*/
@Column(name = "seo_weight")
@Builder.Default
private Integer seoWeight = 5;
/**
* 是否需要FAQ部分
*/
@Column(name = "requires_faq")
@Builder.Default
private Boolean requiresFaq = false;
/**
* 是否需要结构化数据
*/
@Column(name = "requires_structured_data")
@Builder.Default
private Boolean requiresStructuredData = false;
/**
* 是否需要相关链接
*/
@Column(name = "requires_related_links")
@Builder.Default
private Boolean requiresRelatedLinks = false;
/**
* 排序序号(数字越小排序越靠前)
*/
@Column(name = "sort_order")
@Builder.Default
private Integer sortOrder = 0;
/**
* 是否启用
*/
@Column(name = "is_active")
@Builder.Default
private Boolean isActive = true;
/**
* 图标标识(用于前端显示)
*/
@Column(name = "icon", length = 100)
private String icon;
/**
* 颜色标识(十六进制颜色码)
*/
@Column(name = "color", length = 7)
private String color;
/**
* 使用次数统计
*/
@Column(name = "usage_count")
@Builder.Default
private Integer usageCount = 0;
/**
* 创建时间
*/
@CreationTimestamp
@Column(name = "created_at", updatable = false)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
/**
* 更新时间
*/
@UpdateTimestamp
@Column(name = "updated_at")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
/**
* 实体创建前的处理
*/
@PrePersist
protected void onCreate() {
if (isActive == null) isActive = true;
if (requiresFaq == null) requiresFaq = false;
if (requiresStructuredData == null) requiresStructuredData = false;
if (requiresRelatedLinks == null) requiresRelatedLinks = false;
if (sortOrder == null) sortOrder = 0;
if (usageCount == null) usageCount = 0;
if (seoWeight == null) seoWeight = 5;
if (recommendedWordCountMin == null) recommendedWordCountMin = 800;
if (recommendedWordCountMax == null) recommendedWordCountMax = 1500;
// 自动生成code(如果为空)
if (code == null && name != null) {
this.code = generateCodeFromName(name);
}
}
/**
* 从名称生成代码标识
* @param name 类型名称
* @return 代码标识
*/
private String generateCodeFromName(String name) {
if (name == null) return null;
return name.toLowerCase()
.replaceAll("[^a-zA-Z0-9\u4e00-\u9fa5]", "_") // 非字母数字中文替换为下划线
.replaceAll("_+", "_") // 多个下划线合并为一个
.replaceAll("^_|_$", ""); // 移除开头和结尾的下划线
}
/**
* 获取推荐字数范围描述
*/
public String getRecommendedWordCountRange() {
return recommendedWordCountMin + "-" + recommendedWordCountMax + "字";
}
/**
* 检查是否为高SEO权重类型
*/
public boolean isHighSeoWeight() {
return seoWeight != null && seoWeight >= 7;
}
/**
* 检查是否为复杂类型(需要多种特殊元素)
*/
public boolean isComplexType() {
int complexity = 0;
if (Boolean.TRUE.equals(requiresFaq)) complexity++;
if (Boolean.TRUE.equals(requiresStructuredData)) complexity++;
if (Boolean.TRUE.equals(requiresRelatedLinks)) complexity++;
return complexity >= 2;
}
/**
* 获取类型复杂度等级(1-5级)
*/
public int getComplexityLevel() {
int level = 1; // 基础等级
if (Boolean.TRUE.equals(requiresFaq)) level++;
if (Boolean.TRUE.equals(requiresStructuredData)) level++;
if (Boolean.TRUE.equals(requiresRelatedLinks)) level++;
// 根据字数要求调整复杂度
if (recommendedWordCountMax != null && recommendedWordCountMax > 2000) level++;
return Math.min(level, 5); // 最高5级
}
/**
* 增加使用次数
*/
public void incrementUsageCount() {
this.usageCount = (usageCount == null ? 0 : usageCount) + 1;
}
/**
* 检查是否有完整的模板配置
*/
public boolean hasCompleteTemplates() {
return defaultTitleTemplate != null && !defaultTitleTemplate.trim().isEmpty() &&
defaultContentTemplate != null && !defaultContentTemplate.trim().isEmpty();
}
/**
* 获取类型优先级(基于SEO权重和使用次数)
*/
public double getPriority() {
double seoWeight = this.seoWeight != null ? this.seoWeight : 5;
double usageWeight = this.usageCount != null ? Math.log(this.usageCount + 1) : 0;
return seoWeight * 0.7 + usageWeight * 0.3; // SEO权重占70%,使用频率占30%
}
}
\ No newline at end of file
... ...
package com.aigeo.article.entity;
import com.aigeo.common.enums.ContentStatus;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
/**
* 生成文章实体类
* 对应数据库表:ai_generated_articles
*
* 存储AI生成的文章内容,包括:
* - 文章标题、内容和摘要
* - SEO相关元数据
* - 文章状态和发布信息
* - 质量评分和统计数据
*
* @author AIGEO Team
* @since 1.0.0
*/
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "ai_generated_articles", indexes = {
@Index(name = "idx_articles_company_status", columnList = "company_id, status"),
@Index(name = "idx_articles_task_id", columnList = "task_id"),
@Index(name = "idx_articles_slug", columnList = "slug")
})
public class GeneratedArticle {
/**
* 主键ID
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false, updatable = false)
private Integer id;
/**
* 公司ID(多租户字段)
*/
@NotNull(message = "公司ID不能为空")
@Column(name = "company_id", nullable = false)
private Integer companyId;
/**
* 关联的生成任务ID
*/
@Column(name = "task_id")
private Integer taskId;
/**
* 文章标题
*/
@NotBlank(message = "文章标题不能为空")
@Column(name = "title", nullable = false, length = 500)
private String title;
/**
* URL友好的标题(slug)
*/
@Column(name = "slug", length = 500)
private String slug;
/**
* 文章摘要/描述
*/
@Column(name = "excerpt", length = 1000)
private String excerpt;
/**
* 文章正文内容
*/
@Column(name = "content", columnDefinition = "LONGTEXT")
private String content;
/**
* SEO标题(用于<title>标签)
*/
@Column(name = "seo_title", length = 200)
private String seoTitle;
/**
* SEO描述(用于meta description)
*/
@Column(name = "seo_description", length = 500)
private String seoDescription;
/**
* 文章标签(JSON数组或逗号分隔)
*/
@Column(name = "tags", length = 1000)
private String tags;
/**
* 文章字数统计
*/
@Column(name = "word_count")
private Integer wordCount;
/**
* 阅读时长估算(分钟)
*/
@Column(name = "reading_time")
private Integer readingTime;
/**
* 文章状态
*/
@Enumerated(EnumType.STRING)
@Column(name = "status")
@Builder.Default
private ContentStatus status = ContentStatus.DRAFT;
/**
* 质量评分(1-100分)
*/
@Column(name = "quality_score")
private Integer qualityScore;
/**
* AI置信度评分(0-1之间)
*/
@Column(name = "ai_confidence")
private Double aiConfidence;
/**
* 原创性评分(0-1之间,1表示完全原创)
*/
@Column(name = "originality_score")
private Double originalityScore;
/**
* SEO评分(1-100分)
*/
@Column(name = "seo_score")
private Integer seoScore;
/**
* 文章特色图片URL
*/
@Column(name = "featured_image_url", length = 500)
private String featuredImageUrl;
/**
* 发布时间(NULL表示未发布)
*/
@Column(name = "published_at")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime publishedAt;
/**
* 最后编辑的用户ID
*/
@Column(name = "last_edited_by")
private Integer lastEditedBy;
/**
* 浏览次数统计
*/
@Column(name = "view_count")
@Builder.Default
private Integer viewCount = 0;
/**
* 喜欢次数统计
*/
@Column(name = "like_count")
@Builder.Default
private Integer likeCount = 0;
/**
* 分享次数统计
*/
@Column(name = "share_count")
@Builder.Default
private Integer shareCount = 0;
/**
* 结构化数据(Schema.org JSON-LD)
*/
@Column(name = "structured_data", columnDefinition = "JSON")
private String structuredData;
/**
* 元数据(JSON格式存储额外信息)
*/
@Column(name = "metadata", columnDefinition = "JSON")
private String metadata;
/**
* 创建时间
*/
@CreationTimestamp
@Column(name = "created_at", updatable = false)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
/**
* 更新时间
*/
@UpdateTimestamp
@Column(name = "updated_at")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updatedAt;
/**
* 实体创建前的处理
*/
@PrePersist
protected void onCreate() {
if (status == null) status = ContentStatus.DRAFT;
if (viewCount == null) viewCount = 0;
if (likeCount == null) likeCount = 0;
if (shareCount == null) shareCount = 0;
// 生成slug
if (slug == null && title != null) {
this.slug = generateSlugFromTitle(title);
}
// 如果没有SEO标题,使用文章标题
if (seoTitle == null && title != null) {
this.seoTitle = title.length() > 60 ? title.substring(0, 60) + "..." : title;
}
// 估算阅读时长(按平均阅读速度250字/分钟计算)
if (readingTime == null && wordCount != null) {
this.readingTime = Math.max(1, (int) Math.ceil(wordCount / 250.0));
}
}
/**
* 从标题生成URL友好的slug
* @param title 文章标题
* @return URL友好的slug
*/
private String generateSlugFromTitle(String title) {
if (title == null) return null;
return title.toLowerCase()
.replaceAll("[^a-zA-Z0-9\u4e00-\u9fa5\\s-]", "") // 保留字母、数字、中文、空格和连字符
.replaceAll("\\s+", "-") // 空格替换为连字符
.replaceAll("-+", "-") // 多个连字符合并为一个
.replaceAll("^-|-$", ""); // 移除开头和结尾的连字符
}
/**
* 检查文章是否已发布
*/
public boolean isPublished() {
return status == ContentStatus.PUBLISHED && publishedAt != null;
}
/**
* 检查文章是否可以发布
*/
public boolean canPublish() {
return status == ContentStatus.APPROVED ||
status == ContentStatus.GENERATED ||
status == ContentStatus.COMPLETED;
}
/**
* 获取综合评分(质量+SEO+原创性的平均值)
*/
public Double getOverallScore() {
int count = 0;
double total = 0.0;
if (qualityScore != null) {
total += qualityScore;
count++;
}
if (seoScore != null) {
total += seoScore;
count++;
}
if (originalityScore != null) {
total += originalityScore * 100; // 转换为1-100分制
count++;
}
return count > 0 ? total / count : null;
}
/**
* 获取参与度评分(基于浏览、点赞、分享数据)
*/
public Double getEngagementScore() {
if (viewCount == 0) return 0.0;
// 简单的参与度计算:(点赞数*2 + 分享数*3) / 浏览数 * 100
return ((likeCount * 2.0 + shareCount * 3.0) / viewCount) * 100;
}
/**
* 增加浏览次数
*/
public void incrementViewCount() {
this.viewCount = (viewCount == null ? 0 : viewCount) + 1;
}
/**
* 增加点赞次数
*/
public void incrementLikeCount() {
this.likeCount = (likeCount == null ? 0 : likeCount) + 1;
}
/**
* 增加分享次数
*/
public void incrementShareCount() {
this.shareCount = (shareCount == null ? 0 : shareCount) + 1;
}
}
\ No newline at end of file
... ...
package com.aigeo.article.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
/**
* 任务参考资料实体类
* 对应数据库表:ai_task_references
*
* 存储文章生成任务的参考资料信息,包括:
* - 参考网站URL和标题
* - 参考内容摘要
* - 资料权重和可信度评分
* - 使用状态和备注信息
*
* @author AIGEO Team
* @since 1.0.0
*/
@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "ai_task_references", indexes = {
@Index(name = "idx_task_refs_task_id", columnList = "task_id"),
@Index(name = "idx_task_refs_url", columnList = "reference_url")
})
public class TaskReference {
/**
* 主键ID
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false, updatable = false)
private Integer id;
/**
* 关联的生成任务ID
*/
@NotNull(message = "任务ID不能为空")
@Column(name = "task_id", nullable = false)
private Integer taskId;
/**
* 参考资料URL
*/
@NotBlank(message = "参考资料URL不能为空")
@Column(name = "reference_url", nullable = false, length = 500)
private String referenceUrl;
/**
* 参考资料标题
*/
@Column(name = "reference_title", length = 500)
private String referenceTitle;
/**
* 参考内容摘要
*/
@Column(name = "content_summary", length = 2000)
private String contentSummary;
/**
* 资料来源域名
*/
@Column(name = "source_domain", length = 100)
private String sourceDomain;
/**
* 内容类型(如:article, blog, news, academic等)
*/
@Column(name = "content_type", length = 50)
@Builder.Default
private String contentType = "article";
/**
* 资料权重(影响生成结果的重要程度,1-10分)
*/
@Column(name = "weight")
@Builder.Default
private Integer weight = 5;
/**
* 可信度评分(1-10分)
*/
@Column(name = "credibility_score")
@Builder.Default
private Integer credibilityScore = 5;
/**
* 相关性评分(与目标话题的相关程度,1-10分)
*/
@Column(name = "relevance_score")
@Builder.Default
private Integer relevanceScore = 5;
/**
* 内容质量评分(1-10分)
*/
@Column(name = "quality_score")
private Integer qualityScore;
/**
* 发布时间(参考资料的原始发布时间)
*/
@Column(name = "published_at")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime publishedAt;
/**
* 语言标识
*/
@Column(name = "language", length = 10)
@Builder.Default
private String language = "zh-CN";
/**
* 字数统计
*/
@Column(name = "word_count")
private Integer wordCount;
/**
* 是否已处理(是否已被AI分析处理)
*/
@Column(name = "is_processed")
@Builder.Default
private Boolean isProcessed = false;
/**
* 处理状态(pending, processing, completed, failed)
*/
@Column(name = "processing_status", length = 20)
@Builder.Default
private String processingStatus = "pending";
/**
* 使用状态(是否在生成中被实际使用)
*/
@Column(name = "is_used")
@Builder.Default
private Boolean isUsed = false;
/**
* 错误信息(处理失败时的错误描述)
*/
@Column(name = "error_message", length = 1000)
private String errorMessage;
/**
* 提取的关键词(JSON数组或逗号分隔)
*/
@Column(name = "extracted_keywords", length = 1000)
private String extractedKeywords;
/**
* 提取的实体(人名、地名、机构名等,JSON格式)
*/
@Column(name = "extracted_entities", columnDefinition = "JSON")
private String extractedEntities;
/**
* 内容分类标签
*/
@Column(name = "content_tags", length = 500)
private String contentTags;
/**
* 备注信息
*/
@Column(name = "notes", length = 500)
private String notes;
/**
* 元数据(JSON格式存储额外信息)
*/
@Column(name = "metadata", columnDefinition = "JSON")
private String metadata;
/**
* 创建时间
*/
@CreationTimestamp
@Column(name = "created_at", updatable = false)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createdAt;
/**
* 实体创建前的处理
*/
@PrePersist
protected void onCreate() {
if (contentType == null) contentType = "article";
if (weight == null) weight = 5;
if (credibilityScore == null) credibilityScore = 5;
if (relevanceScore == null) relevanceScore = 5;
if (language == null) language = "zh-CN";
if (isProcessed == null) isProcessed = false;
if (processingStatus == null) processingStatus = "pending";
if (isUsed == null) isUsed = false;
// 从URL提取域名
if (sourceDomain == null && referenceUrl != null) {
this.sourceDomain = extractDomainFromUrl(referenceUrl);
}
}
/**
* 从URL提取域名
* @param url 完整URL
* @return 域名
*/
private String extractDomainFromUrl(String url) {
try {
if (url.startsWith("http://")) {
url = url.substring(7);
} else if (url.startsWith("https://")) {
url = url.substring(8);
}
int slashIndex = url.indexOf('/');
if (slashIndex != -1) {
url = url.substring(0, slashIndex);
}
return url;
} catch (Exception e) {
return null;
}
}
/**
* 检查是否为高质量参考资料
*/
public boolean isHighQuality() {
return (credibilityScore != null && credibilityScore >= 7) &&
(relevanceScore != null && relevanceScore >= 7) &&
(qualityScore == null || qualityScore >= 7);
}
/**
* 检查是否为权威来源
*/
public boolean isAuthoritativeSource() {
if (sourceDomain == null) return false;
// 常见权威域名后缀
return sourceDomain.endsWith(".edu") ||
sourceDomain.endsWith(".gov") ||
sourceDomain.endsWith(".org") ||
credibilityScore != null && credibilityScore >= 8;
}
/**
* 获取综合评分(权重、可信度、相关性的加权平均)
*/
public double getOverallScore() {
double weightScore = weight != null ? weight : 5;
double credibilityScore = this.credibilityScore != null ? this.credibilityScore : 5;
double relevanceScore = this.relevanceScore != null ? this.relevanceScore : 5;
double qualityScore = this.qualityScore != null ? this.qualityScore : 5;
// 加权计算:相关性40%,可信度30%,质量20%,权重10%
return relevanceScore * 0.4 + credibilityScore * 0.3 + qualityScore * 0.2 + weightScore * 0.1;
}
/**
* 检查内容是否为近期发布
*/
public boolean isRecentContent() {
if (publishedAt == null) return false;
LocalDateTime sixMonthsAgo = LocalDateTime.now().minusMonths(6);
return publishedAt.isAfter(sixMonthsAgo);
}
/**
* 获取内容新鲜度评分(1-10分,越新分数越高)
*/
public int getFreshnessScore() {
if (publishedAt == null) return 5; // 默认中等分数
LocalDateTime now = LocalDateTime.now();
long daysDiff = java.time.Duration.between(publishedAt, now).toDays();
if (daysDiff <= 7) return 10; // 一周内:10分
if (daysDiff <= 30) return 9; // 一月内:9分
if (daysDiff <= 90) return 8; // 三月内:8分
if (daysDiff <= 180) return 7; // 半年内:7分
if (daysDiff <= 365) return 6; // 一年内:6分
if (daysDiff <= 730) return 4; // 两年内:4分
return 2; // 超过两年:2分
}
/**
* 标记为已处理
*/
public void markAsProcessed() {
this.isProcessed = true;
this.processingStatus = "completed";
}
/**
* 标记处理失败
*/
public void markAsFailed(String errorMessage) {
this.isProcessed = false;
this.processingStatus = "failed";
this.errorMessage = errorMessage;
}
/**
* 标记为已使用
*/
public void markAsUsed() {
this.isUsed = true;
}
}
\ No newline at end of file
... ...
package com.aigeo.article.repository;
import com.aigeo.article.entity.ArticleGenerationTask;
import com.aigeo.common.enums.TaskStatus;
import com.aigeo.common.enums.AiTasteLevel;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
/**
* 文章生成任务数据访问层
* 对应数据库表:ai_article_generation_tasks
*
* 负责管理AI文章生成任务的数据访问操作,包括任务创建、状态跟踪、
* 进度监控和结果查询等功能
*
* @author AIGEO Team
* @since 1.0.0
*/
@Repository
public interface ArticleGenerationTaskRepository extends JpaRepository<ArticleGenerationTask, Integer> {
/**
* 根据公司ID查找所有任务
* @param companyId 公司ID
* @return 该公司的所有文章生成任务
*/
List<ArticleGenerationTask> findByCompanyId(Integer companyId);
/**
* 根据用户ID查找任务
* @param userId 用户ID
* @return 该用户创建的所有任务
*/
List<ArticleGenerationTask> findByUserId(Integer userId);
/**
* 根据状态查找任务
* @param status 任务状态
* @return 处于指定状态的所有任务
*/
List<ArticleGenerationTask> findByStatus(TaskStatus status);
/**
* 根据公司ID和状态查找任务
* @param companyId 公司ID
* @param status 任务状态
* @return 匹配条件的任务列表
*/
List<ArticleGenerationTask> findByCompanyIdAndStatus(Integer companyId, TaskStatus status);
/**
* 查找待处理的任务(按创建时间正序)
* @return 待处理的任务队列
*/
List<ArticleGenerationTask> findByStatusOrderByCreatedAtAsc(TaskStatus status);
/**
* 分页查询公司任务(按创建时间倒序)
* @param companyId 公司ID
* @param pageable 分页参数
* @return 分页结果
*/
Page<ArticleGenerationTask> findByCompanyIdOrderByCreatedAtDesc(Integer companyId, Pageable pageable);
/**
* 根据关键词ID查找任务
* @param keywordId 关键词ID
* @return 基于该关键词的生成任务
*/
List<ArticleGenerationTask> findByKeywordId(Integer keywordId);
/**
* 根据AI风格等级查找任务
* @param companyId 公司ID
* @param aiTasteLevel AI风格等级
* @return 使用指定风格的任务
*/
List<ArticleGenerationTask> findByCompanyIdAndAiTasteLevel(Integer companyId, AiTasteLevel aiTasteLevel);
/**
* 查找指定时间范围内的任务
* @param companyId 公司ID
* @param startTime 开始时间
* @param endTime 结束时间
* @return 时间范围内的任务
*/
List<ArticleGenerationTask> findByCompanyIdAndCreatedAtBetween(Integer companyId, LocalDateTime startTime, LocalDateTime endTime);
/**
* 统计公司任务数量按状态分组
* @param companyId 公司ID
* @param status 任务状态
* @return 指定状态的任务数量
*/
@Query("SELECT COUNT(t) FROM ArticleGenerationTask t WHERE t.companyId = :companyId AND t.status = :status")
long countByCompanyIdAndStatus(@Param("companyId") Integer companyId, @Param("status") TaskStatus status);
/**
* 查找用户最近的任务
* @param userId 用户ID
* @param limit 限制数量
* @return 用户最近的任务
*/
@Query("SELECT t FROM ArticleGenerationTask t WHERE t.userId = :userId ORDER BY t.createdAt DESC")
List<ArticleGenerationTask> findRecentTasksByUser(@Param("userId") Integer userId, Pageable pageable);
/**
* 查找超时未完成的任务
* @param timeoutThreshold 超时时间阈值
* @return 超时的处理中任务
*/
@Query("SELECT t FROM ArticleGenerationTask t WHERE t.status = 'PROCESSING' AND t.createdAt < :timeoutThreshold")
List<ArticleGenerationTask> findTimeoutTasks(@Param("timeoutThreshold") LocalDateTime timeoutThreshold);
/**
* 根据公司ID和文章主题模糊搜索任务
* @param companyId 公司ID
* @param articleTheme 文章主题关键词
* @return 匹配的任务列表
*/
List<ArticleGenerationTask> findByCompanyIdAndArticleThemeContainingIgnoreCase(Integer companyId, String articleTheme);
/**
* 查找最近完成的任务
* @param companyId 公司ID(可选)
* @param status 任务状态
* @param pageable 分页参数
* @return 最近完成的任务
*/
@Query("SELECT t FROM ArticleGenerationTask t WHERE (:companyId IS NULL OR t.companyId = :companyId) AND t.status = :status ORDER BY t.completedAt DESC")
List<ArticleGenerationTask> findRecentCompletedTasks(@Param("companyId") Integer companyId, @Param("status") TaskStatus status, Pageable pageable);
/**
* 根据多个条件搜索任务
* @param companyId 公司ID
* @param userId 用户ID
* @param status 任务状态
* @param articleTheme 文章主题
* @param startTime 开始时间
* @param endTime 结束时间
* @param pageable 分页参数
* @return 分页搜索结果
*/
@Query("SELECT t FROM ArticleGenerationTask t WHERE " +
"(:companyId IS NULL OR t.companyId = :companyId) AND " +
"(:userId IS NULL OR t.userId = :userId) AND " +
"(:status IS NULL OR t.status = :status) AND " +
"(:articleTheme IS NULL OR t.articleTheme LIKE %:articleTheme%) AND " +
"(:startTime IS NULL OR t.createdAt >= :startTime) AND " +
"(:endTime IS NULL OR t.createdAt <= :endTime)")
Page<ArticleGenerationTask> searchTasks(@Param("companyId") Integer companyId,
@Param("userId") Integer userId,
@Param("status") TaskStatus status,
@Param("articleTheme") String articleTheme,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
Pageable pageable);
/**
* 查找最近更新的任务
* @param companyId 公司ID
* @param since 起始时间
* @return 最近更新的任务
*/
@Query("SELECT t FROM ArticleGenerationTask t WHERE (:companyId IS NULL OR t.companyId = :companyId) AND t.updatedAt >= :since ORDER BY t.updatedAt DESC")
List<ArticleGenerationTask> findByUpdatedAtAfterOrderByUpdatedAtDesc(@Param("companyId") Integer companyId, @Param("since") LocalDateTime since);
/**
* 统计指定时间范围内的任务数量
* @param companyId 公司ID(可选)
* @param status 任务状态
* @param startTime 开始时间
* @param endTime 结束时间
* @return 任务数量
*/
@Query("SELECT COUNT(t) FROM ArticleGenerationTask t WHERE " +
"(:companyId IS NULL OR t.companyId = :companyId) AND " +
"(:status IS NULL OR t.status = :status) AND " +
"t.createdAt BETWEEN :startTime AND :endTime")
long countByConditions(@Param("companyId") Integer companyId,
@Param("status") TaskStatus status,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
/**
* 计算平均执行时长
* @param companyId 公司ID(可选)
* @param startTime 开始时间
* @param endTime 结束时间
* @return 平均执行时长(毫秒)
*/
@Query("SELECT AVG(t.executionTimeMs) FROM ArticleGenerationTask t WHERE " +
"(:companyId IS NULL OR t.companyId = :companyId) AND " +
"t.executionTimeMs IS NOT NULL AND " +
"t.createdAt BETWEEN :startTime AND :endTime")
Double getAverageExecutionTime(@Param("companyId") Integer companyId,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
/**
* 删除指定时间之前完成的任务
* @param completedBefore 完成时间阈值
* @param status 任务状态
* @return 删除的任务数量
*/
@Query("DELETE FROM ArticleGenerationTask t WHERE t.status = :status AND t.completedAt < :completedBefore")
int deleteCompletedTasksBefore(@Param("status") TaskStatus status, @Param("completedBefore") LocalDateTime completedBefore);
long countByCompanyId(Integer companyId);
}
... ...