初始化dev
This commit is contained in:
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
19
.idea/compiler.xml
generated
Normal file
19
.idea/compiler.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="true" />
|
||||
<profile name="Maven default annotation processors profile" enabled="true">
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<module name="AI-Interview" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||
<module name="AI-Interview" options="-parameters" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/encodings.xml
generated
Normal file
6
.idea/encodings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
30
.idea/jarRepositories.xml
generated
Normal file
30
.idea/jarRepositories.xml
generated
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="spring-milestones" />
|
||||
<option name="name" value="Spring Milestones" />
|
||||
<option name="url" value="https://repo.spring.io/milestone" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Central Repository" />
|
||||
<option name="url" value="http://maven.aliyun.com/nexus/content/repositories/central/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="spring-snapshots" />
|
||||
<option name="name" value="Spring Snapshots" />
|
||||
<option name="url" value="https://repo.spring.io/snapshot" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
12
.idea/misc.xml
generated
Normal file
12
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK" />
|
||||
</project>
|
||||
27
HELP.md
Normal file
27
HELP.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Getting Started
|
||||
|
||||
### Reference Documentation
|
||||
For further reference, please consider the following sections:
|
||||
|
||||
* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html)
|
||||
* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.4.10-SNAPSHOT/maven-plugin)
|
||||
* [Create an OCI image](https://docs.spring.io/spring-boot/3.4.10-SNAPSHOT/maven-plugin/build-image.html)
|
||||
* [Spring Data JDBC](https://docs.spring.io/spring-boot/3.4.10-SNAPSHOT/reference/data/sql.html#data.sql.jdbc)
|
||||
* [Spring Web](https://docs.spring.io/spring-boot/3.4.10-SNAPSHOT/reference/web/servlet.html)
|
||||
|
||||
### Guides
|
||||
The following guides illustrate how to use some features concretely:
|
||||
|
||||
* [Using Spring Data JDBC](https://github.com/spring-projects/spring-data-examples/tree/master/jdbc/basics)
|
||||
* [Accessing data with MySQL](https://spring.io/guides/gs/accessing-data-mysql/)
|
||||
* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/)
|
||||
* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/)
|
||||
* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/)
|
||||
|
||||
### Maven Parent overrides
|
||||
|
||||
Due to Maven's design, elements are inherited from the parent POM to the project POM.
|
||||
While most of the inheritance is fine, it also inherits unwanted elements like `<license>` and `<developers>` from the parent.
|
||||
To prevent this, the project POM contains empty overrides for these elements.
|
||||
If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides.
|
||||
|
||||
135
ai-interview-ard.vue
Normal file
135
ai-interview-ard.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="dashboard-container">
|
||||
<!-- 欢迎横幅 -->
|
||||
<el-card shadow="never" class="welcome-banner">
|
||||
<div class="welcome-content">
|
||||
<div class="welcome-text">
|
||||
<h2>欢迎回来!</h2>
|
||||
<p>准备好开始您的下一次模拟面试了吗?在这里管理您的题库,不断提升面试技巧。</p>
|
||||
</div>
|
||||
<img src="/src/assets/dashboard-hero.svg" alt="仪表盘插图" class="welcome-illustration" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 功能导航 -->
|
||||
<div class="feature-grid">
|
||||
<router-link to="/interview" class="feature-card-link">
|
||||
<el-card shadow="hover" class="feature-card">
|
||||
<div class="card-content">
|
||||
<el-icon class="card-icon" style="background-color: #ecf5ff; color: #409eff;"><ChatLineRound /></el-icon>
|
||||
<div class="text-content">
|
||||
<h3>开始模拟面试</h3>
|
||||
<p>上传简历,与AI进行实战演练</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/question-bank" class="feature-card-link">
|
||||
<el-card shadow="hover" class="feature-card">
|
||||
<div class="card-content">
|
||||
<el-icon class="card-icon" style="background-color: #f0f9eb; color: #67c23a;"><MessageBox /></el-icon>
|
||||
<div class="text-content">
|
||||
<h3>题库管理</h3>
|
||||
<p>新增、编辑和导入您的面试题库</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</router-link>
|
||||
|
||||
<router-link to="/history" class="feature-card-link">
|
||||
<el-card shadow="hover" class="feature-card">
|
||||
<div class="card-content">
|
||||
<el-icon class="card-icon" style="background-color: #fdf6ec; color: #e6a23c;"><Finished /></el-icon>
|
||||
<div class="text-content">
|
||||
<h3>面试历史</h3>
|
||||
<p>查看过往的面试记录与AI复盘报告</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 导入Element Plus图标
|
||||
import { ChatLineRound, MessageBox, Finished } from '@element-plus/icons-vue';
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 仪表盘容器 */
|
||||
.dashboard-container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
/* 欢迎横幅 */
|
||||
.welcome-banner {
|
||||
border: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.welcome-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.welcome-text h2 {
|
||||
font-size: 1.8em;
|
||||
margin-top: 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.welcome-text p {
|
||||
color: #606266;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.welcome-illustration {
|
||||
width: 200px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* 功能网格布局 */
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.feature-card-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.feature-card .card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
transition: transform 0.3s, box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.feature-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.card-icon {
|
||||
font-size: 32px;
|
||||
padding: 15px;
|
||||
border-radius: 50%;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.text-content h3 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #303133;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.text-content p {
|
||||
margin: 0;
|
||||
color: #909399;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
</style>
|
||||
259
mvnw
vendored
Normal file
259
mvnw
vendored
Normal file
@@ -0,0 +1,259 @@
|
||||
#!/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.2
|
||||
#
|
||||
# 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:]'
|
||||
}
|
||||
|
||||
# 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 <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.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${0##*/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
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
149
mvnw.cmd
vendored
Normal file
149
mvnw.cmd
vendored
Normal file
@@ -0,0 +1,149 @@
|
||||
<# : 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.2
|
||||
@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) { "/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_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
|
||||
}
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::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
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -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"
|
||||
188
pom.xml
Normal file
188
pom.xml
Normal file
@@ -0,0 +1,188 @@
|
||||
<?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.4.10-SNAPSHOT</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.qingqiu</groupId>
|
||||
<artifactId>AI-Interview</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>AI-Interview</name>
|
||||
<description>AI-Interview</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<!-- MyBatis-Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-jsqlparser</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId> <!-- 用于 WebClient -->
|
||||
</dependency>
|
||||
|
||||
<!-- <!– Spring AI Dependencies –>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.springframework.ai</groupId>-->
|
||||
<!-- <artifactId>spring-ai-openai-spring-boot-starter</artifactId>-->
|
||||
<!-- <exclusions>-->
|
||||
<!-- <exclusion>-->
|
||||
<!-- <groupId>com.alibaba.cloud.ai</groupId>-->
|
||||
<!-- <artifactId>spring-ai-alibaba-autoconfigure</artifactId>-->
|
||||
<!-- </exclusion>-->
|
||||
<!-- <exclusion>-->
|
||||
<!-- <groupId>org.springframework.ai</groupId>-->
|
||||
<!-- <artifactId>spring-ai-core</artifactId>-->
|
||||
<!-- </exclusion>-->
|
||||
<!-- </exclusions>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>dashscope-sdk-java</artifactId>
|
||||
<version>2.21.5</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.25</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.18.0</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.53</version>
|
||||
</dependency>
|
||||
<!-- For PDF Parsing -->
|
||||
<dependency>
|
||||
<groupId>org.apache.pdfbox</groupId>
|
||||
<artifactId>pdfbox</artifactId>
|
||||
<version>3.0.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- For Markdown Parsing -->
|
||||
<dependency>
|
||||
<groupId>com.atlassian.commonmark</groupId>
|
||||
<artifactId>commonmark</artifactId>
|
||||
<version>0.17.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<dependencyManagement>
|
||||
<!-- <dependencies>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.springframework.ai</groupId>-->
|
||||
<!-- <artifactId>spring-ai-bom</artifactId>-->
|
||||
<!-- <version>1.0.0-M5</version> <!– 或最新版本 –>-->
|
||||
<!-- <type>pom</type>-->
|
||||
<!-- <scope>import</scope>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- </dependencies>-->
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-bom</artifactId>
|
||||
<version>3.5.9</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<!-- maven 打包时跳过测试 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>true</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>spring-milestones</id>
|
||||
<name>Spring Milestones</name>
|
||||
<url>https://repo.spring.io/milestone</url>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
</repositories>
|
||||
<pluginRepositories>
|
||||
<pluginRepository>
|
||||
<id>spring-snapshots</id>
|
||||
<name>Spring Snapshots</name>
|
||||
<url>https://repo.spring.io/snapshot</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
</releases>
|
||||
</pluginRepository>
|
||||
</pluginRepositories>
|
||||
|
||||
</project>
|
||||
6
sql/.idea/.gitignore
generated
vendored
Normal file
6
sql/.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.qingqiu.interview;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
|
||||
@ComponentScan("com.qingqiu")
|
||||
@SpringBootApplication
|
||||
public class AiInterviewApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AiInterviewApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
22
src/main/java/com/qingqiu/interview/ai/entity/Message.java
Normal file
22
src/main/java/com/qingqiu/interview/ai/entity/Message.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package com.qingqiu.interview.ai.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Message implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String role;
|
||||
|
||||
private String content;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.qingqiu.interview.ai.factory;
|
||||
|
||||
import com.qingqiu.interview.ai.service.AIClientService;
|
||||
|
||||
public interface AIClientFactory {
|
||||
AIClientService createAIClient();
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.qingqiu.interview.ai.factory;
|
||||
|
||||
import com.qingqiu.interview.ai.service.AIClientService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class AIClientManager {
|
||||
|
||||
private final Map<String, AIClientFactory> factories;
|
||||
|
||||
public AIClientManager(Map<String, AIClientFactory> factories) {
|
||||
this.factories = factories;
|
||||
}
|
||||
|
||||
public AIClientService getClient(String aiType) {
|
||||
String factoryName = aiType + "ClientFactory";
|
||||
AIClientFactory factory = factories.get(factoryName);
|
||||
if (factory == null) {
|
||||
throw new IllegalArgumentException("不支持的AI type: " + aiType);
|
||||
}
|
||||
return factory.createAIClient();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.qingqiu.interview.ai.factory;
|
||||
|
||||
import com.qingqiu.interview.ai.service.AIClientService;
|
||||
import com.qingqiu.interview.ai.service.impl.DeepSeekClientServiceImpl;
|
||||
import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DeepSeekClientFactory implements AIClientFactory{
|
||||
@Override
|
||||
public AIClientService createAIClient() {
|
||||
return SpringApplicationContextUtil.getBean(DeepSeekClientServiceImpl.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.qingqiu.interview.ai.factory;
|
||||
|
||||
import com.qingqiu.interview.ai.service.AIClientService;
|
||||
import com.qingqiu.interview.ai.service.impl.QwenClientServiceImpl;
|
||||
import com.qingqiu.interview.common.utils.SpringApplicationContextUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class QwenClientFactory implements AIClientFactory{
|
||||
@Override
|
||||
public AIClientService createAIClient() {
|
||||
return SpringApplicationContextUtil.getBean(QwenClientServiceImpl.class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.qingqiu.interview.ai.service;
|
||||
|
||||
import com.alibaba.dashscope.common.Message;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public abstract class AIClientService {
|
||||
public abstract String chatCompletion(String prompt);
|
||||
|
||||
public String chatCompletion(List<Message> messages) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.qingqiu.interview.ai.service.impl;
|
||||
|
||||
import com.alibaba.dashscope.common.Message;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.qingqiu.interview.ai.service.AIClientService;
|
||||
import com.qingqiu.interview.common.service.HttpService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.qingqiu.interview.common.utils.AIUtils.createUserMessage;
|
||||
|
||||
/**
|
||||
* deepseek 接入
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DeepSeekClientServiceImpl extends AIClientService {
|
||||
|
||||
private final HttpService httpService;
|
||||
|
||||
@Value("${deepseek.api-url}")
|
||||
private String apiUrl;
|
||||
|
||||
@Value("${deepseek.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
|
||||
@Override
|
||||
public String chatCompletion(String prompt) {
|
||||
return chatCompletion(Collections.singletonList(createUserMessage(prompt)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatCompletion(List<Message> messages) {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("type", "json_object");
|
||||
Map<String, Object> requestBody = Map.of(
|
||||
"model", "deepseek-chat",
|
||||
"messages", messages,
|
||||
"max_tokens", 8192,
|
||||
"response_format", Map.of("type", "json_object")
|
||||
);
|
||||
String res = httpService.postWithAuth(
|
||||
apiUrl,
|
||||
requestBody,
|
||||
String.class,
|
||||
"Bearer " + apiKey
|
||||
).block();
|
||||
if (StringUtils.isNotBlank(res)) {
|
||||
JSONObject jsonRes = JSONObject.parse(res);
|
||||
JSONArray choices = jsonRes.getJSONArray("choices");
|
||||
JSONObject resContent = choices.getJSONObject(0);
|
||||
JSONObject message = resContent.getJSONObject("message");
|
||||
return message.getString("content");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.qingqiu.interview.ai.service.impl;
|
||||
|
||||
import com.alibaba.dashscope.aigc.generation.Generation;
|
||||
import com.alibaba.dashscope.aigc.generation.GenerationParam;
|
||||
import com.alibaba.dashscope.aigc.generation.GenerationResult;
|
||||
import com.alibaba.dashscope.common.Message;
|
||||
import com.alibaba.dashscope.exception.ApiException;
|
||||
import com.alibaba.dashscope.exception.InputRequiredException;
|
||||
import com.alibaba.dashscope.exception.NoApiKeyException;
|
||||
import com.qingqiu.interview.ai.service.AIClientService;
|
||||
import com.qingqiu.interview.common.res.ResultCode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static com.qingqiu.interview.common.constants.QwenModelConstant.QWEN_PLUS_LATEST;
|
||||
import static com.qingqiu.interview.common.utils.AIUtils.createUserMessage;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class QwenClientServiceImpl extends AIClientService {
|
||||
|
||||
@Value("${dashscope.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
private final Generation generation;
|
||||
|
||||
@Override
|
||||
public String chatCompletion(String prompt) {
|
||||
return chatCompletion(Collections.singletonList(createUserMessage(prompt)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatCompletion(List<Message> messages) {
|
||||
|
||||
GenerationParam param = GenerationParam.builder()
|
||||
.model(QWEN_PLUS_LATEST) // 可根据需要更换模型
|
||||
.messages(messages)
|
||||
.resultFormat(GenerationParam.ResultFormat.MESSAGE)
|
||||
.apiKey(apiKey)
|
||||
.build();
|
||||
|
||||
GenerationResult result = null;
|
||||
try {
|
||||
result = generation.call(param);
|
||||
return result.getOutput().getChoices().get(0).getMessage().getContent();
|
||||
} catch (NoApiKeyException e) {
|
||||
log.error("没有api key,请先确认配置!");
|
||||
throw new com.qingqiu.interview.common.ex.ApiException(ResultCode.INTERNAL);
|
||||
} catch (ApiException | InputRequiredException e) {
|
||||
log.error("调用AI服务失败", e);
|
||||
throw new com.qingqiu.interview.common.ex.ApiException(ResultCode.INTERNAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.qingqiu.interview.common.constants;
|
||||
|
||||
/**
|
||||
* AI策略常量
|
||||
*/
|
||||
public class AIStrategyConstant {
|
||||
|
||||
public static final String DEEPSEEK = "deepSeek";
|
||||
public static final String QWEN = "qwen";
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.qingqiu.interview.common.constants;
|
||||
|
||||
public class QwenModelConstant {
|
||||
|
||||
public static final String QWEN_MAX = "qwen-max";
|
||||
public static final String QWEN_MAX_LATEST = "qwen-max-latest";
|
||||
|
||||
public static final String QWEN_PLUS = "qwen-plus";
|
||||
public static final String QWEN_PLUS_LATEST = "qwen-plus-latest";
|
||||
public static final String DEEPSEEK_3_1 = "deepseek-v3.1";
|
||||
public static final String DEEPSEEK_3 = "deepseek-v3";
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.qingqiu.interview.common.ex;
|
||||
|
||||
|
||||
import com.qingqiu.interview.common.res.ResultCode;
|
||||
|
||||
public class ApiException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Object object;
|
||||
|
||||
private ResultCode resultCode;
|
||||
|
||||
public ApiException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public ApiException(String msg, Object object) {
|
||||
super(msg);
|
||||
this.object = object;
|
||||
}
|
||||
|
||||
public ApiException(String msg, Throwable cause) {
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
|
||||
public ApiException(ResultCode resultCode) {
|
||||
super(resultCode.getMessage());
|
||||
this.resultCode = resultCode;
|
||||
}
|
||||
|
||||
public ApiException(ResultCode resultCode, Object object) {
|
||||
super(resultCode.getMessage());
|
||||
this.resultCode = resultCode;
|
||||
this.object = object;
|
||||
}
|
||||
|
||||
|
||||
public Object getObject() {
|
||||
return object;
|
||||
}
|
||||
|
||||
public ResultCode getResultCode() {
|
||||
return resultCode;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.qingqiu.interview.common.ex;
|
||||
|
||||
import com.qingqiu.interview.common.res.R;
|
||||
import com.qingqiu.interview.common.res.ResultCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.MissingRequestHeaderException;
|
||||
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.support.MissingServletRequestPartException;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 全局异常处理
|
||||
*
|
||||
* @author Administrator
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalErrorHandler {
|
||||
|
||||
private static final Map<Integer, ResultCode> ERROR_CODE_MAP =
|
||||
new HashMap<>(ResultCode.values().length);
|
||||
|
||||
static {
|
||||
for (ResultCode ResultCode : ResultCode.values()) {
|
||||
ERROR_CODE_MAP.put(ResultCode.getCode(), ResultCode);
|
||||
}
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public R<?> exception(Exception e) {
|
||||
log.error("系统异常", e);
|
||||
e.printStackTrace();
|
||||
return R.error(ResultCode.INTERNAL);
|
||||
}
|
||||
|
||||
@ExceptionHandler(ApiException.class)
|
||||
public R apiException(ApiException ex) {
|
||||
return R.error(ex.getResultCode());
|
||||
}
|
||||
|
||||
// /**
|
||||
// * sa的权限验证错误处理
|
||||
// * @param ex
|
||||
// * @return
|
||||
// */
|
||||
// @ExceptionHandler({NotPermissionException.class, NotRoleException.class})
|
||||
// public R notPermissionException(Exception ex) {
|
||||
// return R.error(ex.getMessage());
|
||||
// }
|
||||
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R<?> methodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||
return R.error(ResultCode.FORBIDDEN.getCode(), Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MissingServletRequestParameterException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R<?> missingServletRequestParameterException(MissingServletRequestParameterException e) {
|
||||
String message = String.format("%s不能为空", e.getParameterName());
|
||||
return R.error(HttpStatus.BAD_REQUEST.value(), message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(MissingServletRequestPartException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R<?> missingServletRequestPartException(MissingServletRequestPartException e) {
|
||||
String message = String.format("%s不能为空", e.getRequestPartName());
|
||||
return R.error(ResultCode.FORBIDDEN.getCode(), message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(BindException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R<?> bindException(BindException e) {
|
||||
return R.error(ResultCode.BAD_REQUEST.getCode(), e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MissingRequestHeaderException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R<?> missingRequestHeaderException(MissingRequestHeaderException e) {
|
||||
return R.error(HttpStatus.BAD_REQUEST.value(), String.format("缺少请求头%s", e.getHeaderName()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public R<?> httpMessageNotReadableException(HttpMessageNotReadableException e) {
|
||||
e.printStackTrace();
|
||||
return R.error(ResultCode.BAD_REQUEST);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.qingqiu.interview.common.res;
|
||||
|
||||
/**
|
||||
* 封装API的错误码
|
||||
* Created by macro on 2019/4/19.
|
||||
*/
|
||||
public interface IErrorCode {
|
||||
Integer getCode();
|
||||
|
||||
String getMessage();
|
||||
}
|
||||
131
src/main/java/com/qingqiu/interview/common/res/R.java
Normal file
131
src/main/java/com/qingqiu/interview/common/res/R.java
Normal file
@@ -0,0 +1,131 @@
|
||||
package com.qingqiu.interview.common.res;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 通用返回对象
|
||||
* Created by huangpeng 2023/9/27.
|
||||
*/
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class R<T> {
|
||||
private Integer code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
|
||||
/**
|
||||
* 成功返回结果
|
||||
*
|
||||
*/
|
||||
public static <T> R<T> success() {
|
||||
return new R<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功返回结果
|
||||
*
|
||||
* @param data 获取的数据
|
||||
*/
|
||||
public static <T> R<T> success(T data) {
|
||||
return new R<T>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功返回结果
|
||||
*
|
||||
* @param data 获取的数据
|
||||
* @param message 提示信息
|
||||
*/
|
||||
public static <T> R<T> success(T data, String message) {
|
||||
return new R<T>(ResultCode.SUCCESS.getCode(), message, data);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 失败返回结果
|
||||
* @param errorCode 错误码
|
||||
* @param message 错误信息
|
||||
*/
|
||||
public static <T> R<T> error(Integer errorCode, String message) {
|
||||
return new R<T>(errorCode, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败返回结果
|
||||
* @param errorCode 错误码
|
||||
*/
|
||||
public static <T> R<T> error(IErrorCode errorCode) {
|
||||
return new R<T>(errorCode.getCode(), errorCode.getMessage(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败返回结果
|
||||
* @param resultCode 错误码
|
||||
*/
|
||||
public static <T> R<T> error(ResultCode resultCode, T data) {
|
||||
R<T> commonResult = new R<>();
|
||||
commonResult.setMessage(resultCode.getMessage());
|
||||
commonResult.setCode(resultCode.getCode());
|
||||
commonResult.setData(data);
|
||||
return commonResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败返回结果
|
||||
* @param errorCode 错误码
|
||||
* @param message 错误信息
|
||||
*/
|
||||
public static <T> R<T> error(IErrorCode errorCode, String message) {
|
||||
return new R<T>(errorCode.getCode(), message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败返回结果
|
||||
* @param message 提示信息
|
||||
*/
|
||||
public static <T> R<T> error(String message) {
|
||||
return new R<T>(ResultCode.INTERNAL.getCode(), message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败返回结果
|
||||
*/
|
||||
public static <T> R<T> error() {
|
||||
return error(ResultCode.INTERNAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数验证失败返回结果
|
||||
*/
|
||||
public static <T> R<T> validateerror() {
|
||||
return error(ResultCode.METHOD_ARGUMENT_NOT_VALID);
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数验证失败返回结果
|
||||
* @param message 提示信息
|
||||
*/
|
||||
public static <T> R<T> validateerror(String message) {
|
||||
return new R<T>(ResultCode.METHOD_ARGUMENT_NOT_VALID.getCode(), message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 未登录返回结果
|
||||
*/
|
||||
public static <T> R<T> unauthorized(T data) {
|
||||
return new R<T>(ResultCode.UNAUTHORIZED.getCode(), ResultCode.UNAUTHORIZED.getMessage(), data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 未授权返回结果
|
||||
*/
|
||||
public static <T> R<T> forbidden(T data) {
|
||||
return new R<T>(ResultCode.FORBIDDEN.getCode(), ResultCode.FORBIDDEN.getMessage(), data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.qingqiu.interview.common.res;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 枚举了一些常用API操作码
|
||||
* Created by huangpeng on 2023/9/27.
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public enum ResultCode implements IErrorCode {
|
||||
SUCCESS(0, "操作成功"),
|
||||
FAILED(-1, "操作失败"),
|
||||
SERVER_ERROR(5, "服务器出了点小差"),
|
||||
BAD_REQUEST(400, "请求参数格式错误"),
|
||||
VALIDATE_FAILED(404, "参数检验失败"),
|
||||
UNAUTHORIZED(401, "暂未登录或session已经过期"),
|
||||
FORBIDDEN(403, "没有相关权限"),
|
||||
UPLOAD_ERROR(-103, "文件上传错误"),
|
||||
INTERNAL(500, "服务器繁忙,请稍后再试"),
|
||||
METHOD_ARGUMENT_NOT_VALID(2, "参数校验失败"),
|
||||
HTTP_MESSAGE_NOT_READABLE(3, "请求参数格式有误"),
|
||||
USERNAME_OR_PASSWORD_IS_INCORRECT(4, "用户名或密码错误"),
|
||||
USER_NOT_FOUND(6, "用户不存在!"),
|
||||
USER_EXIST(7, "用户已存在!"),
|
||||
NEED_LOGIN_USER(10, "请先登录!"),
|
||||
LOGIN_STATUS_EXPIRED(10010, "登录状态已过期!"),
|
||||
TOKEN_VALID_FAILED(11, "token校验失败!"),
|
||||
|
||||
CONVERTING_PRODUCT_IMAGE_TYPES_FAILED(1001, "转换图片类型失败!"),
|
||||
|
||||
PRODUCT_LOW_STOCK(2001, "商品库存不足!"),
|
||||
ORDER_IN_THE_QUEUE(2002, "订单正在处理中,请稍后..."),
|
||||
ORDER_CREATE_FAILED(2003, "订单创建失败,请稍后重试..."),
|
||||
|
||||
VERIFICATION_CODE_EXPIRED(3001, "验证码已过期,请重新获取验证码!"),
|
||||
VERIFICATION_CODE_ERROR(3002, "验证码错误!"),
|
||||
SEND_SMS_FREQUENT(3003, "短信发送过于频繁,请稍后再试!"),
|
||||
;
|
||||
private Integer code;
|
||||
private String message;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ResultCode{" + "code='" + code + '\'' + ", message='" + message + '\'' + "} " + super.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.qingqiu.interview.common.service;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface HttpService {
|
||||
|
||||
<T> Mono<T> post(String url, Object requestBody, Class<T> responseType);
|
||||
|
||||
<T> Mono<T> postWithAuth(
|
||||
String url,
|
||||
Object requestBody,
|
||||
Class<T> responseType,
|
||||
String authHeader
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.qingqiu.interview.common.service.impl;
|
||||
|
||||
import com.qingqiu.interview.common.service.HttpService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class HttpServiceImpl implements HttpService {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> post(String url, Object requestBody, Class<T> responseType) {
|
||||
return webClient.post()
|
||||
.uri(url)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(requestBody)
|
||||
.retrieve()
|
||||
.bodyToMono(responseType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> postWithAuth(String url, Object requestBody, Class<T> responseType, String authHeader) {
|
||||
return webClient.post()
|
||||
.uri(url)
|
||||
.header("Authorization", authHeader)
|
||||
.header("Accept", MediaType.APPLICATION_JSON_VALUE)
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.bodyValue(requestBody)
|
||||
.retrieve()
|
||||
.bodyToMono(responseType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.qingqiu.interview.common.utils;
|
||||
|
||||
import com.alibaba.dashscope.common.Message;
|
||||
import com.alibaba.dashscope.common.Role;
|
||||
|
||||
public class AIUtils {
|
||||
|
||||
public static Message createMessage(String role, String content) {
|
||||
return Message.builder()
|
||||
.role(role)
|
||||
.content(content)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static Message createUserMessage(String prompt) {
|
||||
return createMessage(Role.USER.getValue(), prompt);
|
||||
}
|
||||
|
||||
public static Message createAIMessage(String prompt) {
|
||||
return createMessage(Role.ASSISTANT.getValue(), prompt);
|
||||
}
|
||||
|
||||
public static Message createSystemMessage(String prompt) {
|
||||
return createMessage(Role.SYSTEM.getValue(), prompt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.qingqiu.interview.common.utils;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class SpringApplicationContextUtil implements ApplicationContextAware {
|
||||
private static ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
SpringApplicationContextUtil.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
public static <T> T getBean(Class<T> beanClass) {
|
||||
return applicationContext.getBean(beanClass);
|
||||
}
|
||||
|
||||
public static Object getBean(String beanName) {
|
||||
return applicationContext.getBean(beanName);
|
||||
}
|
||||
|
||||
public static <T> T getBean(String beanName, Class<T> beanClass) {
|
||||
return applicationContext.getBean(beanName, beanClass);
|
||||
}
|
||||
|
||||
public static boolean containsBean(String beanName) {
|
||||
return applicationContext.containsBean(beanName);
|
||||
}
|
||||
|
||||
public static boolean isSingleton(String beanName) {
|
||||
return applicationContext.isSingleton(beanName);
|
||||
}
|
||||
|
||||
public static Class<?> getType(String beanName) {
|
||||
return applicationContext.getType(beanName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.qingqiu.interview.config;
|
||||
|
||||
import com.alibaba.dashscope.aigc.generation.Generation;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class DashScopeConfig {
|
||||
|
||||
@Bean
|
||||
public Generation generation() {
|
||||
return new Generation();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.qingqiu.interview.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JavaTimeModule module = new JavaTimeModule();
|
||||
|
||||
// 配置 LocalDateTime 的序列化和反序列化格式
|
||||
module.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
module.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
|
||||
mapper.registerModule(module);
|
||||
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
return mapper;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.qingqiu.interview.config;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@MapperScan("com.qingqiu.interview.**.mapper")
|
||||
public class MyBatisPlusConfig implements MetaObjectHandler {
|
||||
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
log.info("开始插入填充...");
|
||||
this.strictInsertFill(metaObject, "createdTime", LocalDateTime.class, LocalDateTime.now());
|
||||
this.strictInsertFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
log.info("开始更新填充...");
|
||||
this.strictUpdateFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.qingqiu.interview.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class WebClientConfig {
|
||||
|
||||
@Bean
|
||||
public WebClient createWebClient() {
|
||||
return WebClient.builder()
|
||||
.filter(logRequest()) // 请求日志
|
||||
.filter(logResponse()) // 响应日志
|
||||
.filter(errorHandler())
|
||||
.build(); // 统一错误处理
|
||||
}
|
||||
|
||||
private ExchangeFilterFunction logRequest() {
|
||||
return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
|
||||
log.info("请求: {} {}", clientRequest.method(), clientRequest.url());
|
||||
return Mono.just(clientRequest);
|
||||
});
|
||||
}
|
||||
|
||||
private ExchangeFilterFunction logResponse() {
|
||||
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
|
||||
log.info("返回: {}", clientResponse.statusCode());
|
||||
return Mono.just(clientResponse);
|
||||
});
|
||||
}
|
||||
|
||||
private ExchangeFilterFunction errorHandler() {
|
||||
return ExchangeFilterFunction.ofResponseProcessor(response -> {
|
||||
if (response.statusCode().isError()) {
|
||||
return response.bodyToMono(String.class)
|
||||
.flatMap(errorBody -> Mono.error(new HttpClientErrorException(
|
||||
response.statusCode(),
|
||||
"接口报错: " + errorBody
|
||||
)));
|
||||
}
|
||||
return Mono.just(response);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.qingqiu.interview.controller;
|
||||
|
||||
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* ai会话记录 前端控制器
|
||||
* </p>
|
||||
*
|
||||
* @author huangpeng
|
||||
* @since 2025-08-30
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/ai-session-log")
|
||||
public class AiSessionLogController {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.qingqiu.interview.controller;
|
||||
|
||||
import com.qingqiu.interview.dto.ApiResponse;
|
||||
import com.qingqiu.interview.dto.DashboardStatsResponse;
|
||||
import com.qingqiu.interview.service.DashboardService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 仪表盘数据统计接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dashboard")
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardController {
|
||||
|
||||
private final DashboardService dashboardService;
|
||||
|
||||
/**
|
||||
* 获取仪表盘所有统计数据
|
||||
*/
|
||||
@PostMapping("/stats")
|
||||
public ApiResponse<DashboardStatsResponse> getDashboardStats() {
|
||||
return ApiResponse.success(dashboardService.getDashboardStats());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.qingqiu.interview.controller;
|
||||
|
||||
import com.qingqiu.interview.dto.*;
|
||||
import com.qingqiu.interview.entity.InterviewSession;
|
||||
import com.qingqiu.interview.service.InterviewService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 面试流程相关接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/interview")
|
||||
@RequiredArgsConstructor
|
||||
public class InterviewController {
|
||||
|
||||
private final InterviewService interviewService;
|
||||
|
||||
/**
|
||||
* 开始新的面试会话
|
||||
*/
|
||||
@PostMapping("/start")
|
||||
public ApiResponse<InterviewResponse> startInterview(
|
||||
@RequestParam("resume") MultipartFile resume,
|
||||
@Validated @ModelAttribute InterviewStartRequest request) throws IOException {
|
||||
InterviewResponse response = interviewService.startInterview(resume, request);
|
||||
return ApiResponse.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 继续面试会话(用户回答)
|
||||
*/
|
||||
@PostMapping("/chat")
|
||||
public ApiResponse<InterviewResponse> continueInterview(@Validated @RequestBody ChatRequest request) {
|
||||
InterviewResponse response = interviewService.continueInterview(request);
|
||||
return ApiResponse.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有面试会话列表
|
||||
*/
|
||||
@PostMapping("/get-history-list")
|
||||
public ApiResponse<java.util.List<InterviewSession>> getInterviewHistoryList() {
|
||||
return ApiResponse.success(interviewService.getInterviewSessions());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单次面试的详细复盘报告
|
||||
*/
|
||||
@PostMapping("/get-report-detail")
|
||||
public ApiResponse<InterviewReportResponse> getInterviewReportDetail(@RequestBody SessionRequest request) {
|
||||
return ApiResponse.success(interviewService.getInterviewReport(request.getSessionId()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.qingqiu.interview.controller;
|
||||
|
||||
|
||||
import com.qingqiu.interview.common.res.R;
|
||||
import com.qingqiu.interview.dto.QuestionProgressPageParams;
|
||||
import com.qingqiu.interview.service.IInterviewQuestionProgressService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 面试问题进度跟踪表 前端控制器
|
||||
* </p>
|
||||
*
|
||||
* @author huangpeng
|
||||
* @since 2025-08-30
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/interview-question-progress")
|
||||
@RequiredArgsConstructor
|
||||
public class InterviewQuestionProgressController {
|
||||
|
||||
private final IInterviewQuestionProgressService service;
|
||||
|
||||
/**
|
||||
* 面试问题进度列表
|
||||
* @param params 查询参数
|
||||
* @return data
|
||||
*/
|
||||
@PostMapping("/page")
|
||||
public R<?> pageList(@RequestBody QuestionProgressPageParams params) {
|
||||
return R.success(service.pageList(params));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.qingqiu.interview.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.qingqiu.interview.common.res.R;
|
||||
import com.qingqiu.interview.dto.ApiResponse;
|
||||
import com.qingqiu.interview.dto.QuestionPageParams;
|
||||
import com.qingqiu.interview.entity.Question;
|
||||
import com.qingqiu.interview.service.QuestionService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 题库管理相关接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/question")
|
||||
@RequiredArgsConstructor
|
||||
public class QuestionController {
|
||||
|
||||
private final QuestionService questionService;
|
||||
|
||||
/**
|
||||
* 分页查询题库
|
||||
*/
|
||||
@PostMapping("/page")
|
||||
public ApiResponse<Page<Question>> getQuestionPage(@RequestBody QuestionPageParams params) {
|
||||
return ApiResponse.success(questionService.getQuestionPage(params));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增题目
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
public ApiResponse<Object> addQuestion(@Validated @RequestBody Question question) {
|
||||
questionService.addQuestion(question);
|
||||
return ApiResponse.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新题目
|
||||
*/
|
||||
@PostMapping("/update")
|
||||
public ApiResponse<Object> updateQuestion(@Validated @RequestBody Question question) {
|
||||
questionService.updateQuestion(question);
|
||||
return ApiResponse.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除题目
|
||||
*/
|
||||
@PostMapping("/delete")
|
||||
public ApiResponse<Object> deleteQuestion(@RequestBody Question question) {
|
||||
questionService.deleteQuestion(question.getId());
|
||||
return ApiResponse.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* AI批量导入题目
|
||||
*/
|
||||
@PostMapping("/import-by-ai")
|
||||
public ApiResponse<Object> importQuestionsByAi(@RequestParam("file") MultipartFile file) throws IOException {
|
||||
questionService.importQuestionsFromFile(file);
|
||||
return ApiResponse.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验数据
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/check-question-data")
|
||||
public R<?> checkQuestionData() {
|
||||
questionService.useAiCheckQuestionData();
|
||||
return R.success();
|
||||
}
|
||||
}
|
||||
30
src/main/java/com/qingqiu/interview/dto/ApiResponse.java
Normal file
30
src/main/java/com/qingqiu/interview/dto/ApiResponse.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.qingqiu.interview.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ApiResponse<T> {
|
||||
|
||||
private int code;
|
||||
private String message;
|
||||
private T data;
|
||||
|
||||
public static <T> ApiResponse<T> success(T data) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.setCode(0);
|
||||
response.setMessage("Success");
|
||||
response.setData(data);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> success() {
|
||||
return success(null);
|
||||
}
|
||||
|
||||
public static <T> ApiResponse<T> error(int code, String message) {
|
||||
ApiResponse<T> response = new ApiResponse<>();
|
||||
response.setCode(code);
|
||||
response.setMessage(message);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
17
src/main/java/com/qingqiu/interview/dto/ChatRequest.java
Normal file
17
src/main/java/com/qingqiu/interview/dto/ChatRequest.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.qingqiu.interview.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ChatRequest {
|
||||
|
||||
@NotBlank(message = "会话ID不能为空")
|
||||
private String sessionId;
|
||||
|
||||
@NotBlank(message = "用户回答不能为空")
|
||||
private String userAnswer;
|
||||
@NotNull(message = "当前问题ID不能为空")
|
||||
private Long currentQuestionId;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.qingqiu.interview.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 仪表盘数据统计响应DTO
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class DashboardStatsResponse {
|
||||
|
||||
// --- 核心KPI数据 ---
|
||||
private long totalInterviews; // 面试总次数
|
||||
private long totalQuestions; // 题库总题数
|
||||
private double recentAverageScore; // 近期平均分
|
||||
|
||||
// --- 图表数据 ---
|
||||
private List<CategoryStat> questionCategoryStats; // 题库分类统计 (饼图)
|
||||
private List<DailyStat> recentInterviewStats; // 近期面试次数 (柱状图)
|
||||
private List<CategoryScoreStat> categoryAverageScores; // 各技术分类平均分 (雷达图)
|
||||
private List<WeakestQuestionStat> weakestQuestions; // 表现最差的题目Top5 (列表)
|
||||
|
||||
/**
|
||||
* 分类统计内部类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public static class CategoryStat {
|
||||
private String name; // 分类名
|
||||
private long value; // 数量
|
||||
|
||||
public CategoryStat(String name, long value) {
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 每日统计内部类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public static class DailyStat {
|
||||
private String date; // 日期
|
||||
private long count; // 次数
|
||||
|
||||
public DailyStat(String date, long count) {
|
||||
this.date = date;
|
||||
this.count = count;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 各技术分类平均分 (雷达图)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public static class CategoryScoreStat {
|
||||
private String name; // 分类名
|
||||
private Double value; // 平均分
|
||||
}
|
||||
|
||||
/**
|
||||
* 表现最差的题目 (列表)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public static class WeakestQuestionStat {
|
||||
private Long questionId;
|
||||
private String questionContent;
|
||||
private Double averageScore;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.qingqiu.interview.dto;
|
||||
|
||||
import com.qingqiu.interview.entity.InterviewMessage;
|
||||
import com.qingqiu.interview.entity.InterviewSession;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用于封装详细面试报告的数据传输对象
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class InterviewReportResponse {
|
||||
|
||||
// 面试会话的整体详情
|
||||
private InterviewSession sessionDetails;
|
||||
|
||||
// 每个问题的详细问答和评估列表
|
||||
private List<QuestionDetail> questionDetails;
|
||||
|
||||
private List<InterviewMessage> messages;
|
||||
|
||||
private Long currentQuestionId;
|
||||
|
||||
/**
|
||||
* 单个问题的详情内部类
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public static class QuestionDetail {
|
||||
private Long questionId;
|
||||
private String questionContent;
|
||||
private String userAnswer;
|
||||
private String aiFeedback;
|
||||
private String suggestions;
|
||||
private BigDecimal score;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.qingqiu.interview.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class InterviewResponse {
|
||||
|
||||
private String sessionId;
|
||||
private String message;
|
||||
private String messageType; // QUESTION, ANSWER, SYSTEM
|
||||
private String sender; // AI, USER, SYSTEM
|
||||
private Integer currentQuestionIndex;
|
||||
private Integer totalQuestions;
|
||||
private String status; // ACTIVE, COMPLETED, TERMINATED
|
||||
private Long currentQuestionId;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.qingqiu.interview.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class InterviewStartRequest {
|
||||
|
||||
@NotBlank(message = "候选人姓名不能为空")
|
||||
private String candidateName;
|
||||
|
||||
|
||||
|
||||
// 简历文件通过MultipartFile单独传递
|
||||
}
|
||||
20
src/main/java/com/qingqiu/interview/dto/PageBaseParams.java
Normal file
20
src/main/java/com/qingqiu/interview/dto/PageBaseParams.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.qingqiu.interview.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
public class PageBaseParams implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Integer current;
|
||||
|
||||
private Integer size;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.qingqiu.interview.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Accessors(chain = true)
|
||||
public class QuestionPageParams extends PageBaseParams{
|
||||
|
||||
private String content;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.qingqiu.interview.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class QuestionProgressPageParams extends PageBaseParams{
|
||||
private String questionName;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.qingqiu.interview.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public class SessionHistoryResponse {
|
||||
|
||||
private String sessionId;
|
||||
private String candidateName;
|
||||
private String aiModel;
|
||||
private String status;
|
||||
private Integer totalQuestions;
|
||||
private Integer currentQuestionIndex;
|
||||
private LocalDateTime createdTime;
|
||||
private List<MessageDto> messages;
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class MessageDto {
|
||||
private String messageType;
|
||||
private String sender;
|
||||
private String content;
|
||||
private Integer messageOrder;
|
||||
private LocalDateTime createdTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.qingqiu.interview.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class SessionRequest {
|
||||
private String sessionId;
|
||||
}
|
||||
|
||||
58
src/main/java/com/qingqiu/interview/entity/AiSessionLog.java
Normal file
58
src/main/java/com/qingqiu/interview/entity/AiSessionLog.java
Normal file
@@ -0,0 +1,58 @@
|
||||
package com.qingqiu.interview.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* ai会话记录
|
||||
* </p>
|
||||
*
|
||||
* @author huangpeng
|
||||
* @since 2025-08-30
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("ai_session_log")
|
||||
public class AiSessionLog implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* user 或者 assistant
|
||||
*/
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 输入内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 生成的会话token
|
||||
*/
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.qingqiu.interview.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("interview_evaluation")
|
||||
public class InterviewEvaluation {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@TableField("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@TableField("question_id")
|
||||
private Long questionId;
|
||||
|
||||
@TableField("user_answer")
|
||||
private String userAnswer;
|
||||
|
||||
@TableField("ai_feedback")
|
||||
private String aiFeedback;
|
||||
|
||||
@TableField("score")
|
||||
private BigDecimal score;
|
||||
|
||||
@TableField("evaluation_criteria")
|
||||
private String evaluationCriteria;
|
||||
|
||||
@TableField(value = "created_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.qingqiu.interview.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("interview_message")
|
||||
public class InterviewMessage {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@TableField("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@TableField("message_type")
|
||||
private String messageType;
|
||||
|
||||
@TableField("sender")
|
||||
private String sender;
|
||||
|
||||
@TableField("content")
|
||||
private String content;
|
||||
|
||||
@TableField("question_id")
|
||||
private Long questionId;
|
||||
|
||||
@TableField("message_order")
|
||||
private Integer messageOrder;
|
||||
|
||||
@TableField(value = "created_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
public enum MessageType {
|
||||
QUESTION, ANSWER, SYSTEM
|
||||
}
|
||||
|
||||
public enum Sender {
|
||||
AI, USER, SYSTEM
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package com.qingqiu.interview.entity;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 面试问题进度
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("interview_question_progress")
|
||||
public class InterviewQuestionProgress {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 问题id
|
||||
*/
|
||||
@TableField("question_id")
|
||||
private Long questionId;
|
||||
|
||||
/**
|
||||
* 问题内容
|
||||
*/
|
||||
@TableField("question_content")
|
||||
private String questionContent;
|
||||
|
||||
/**
|
||||
* 面试会话ID
|
||||
*/
|
||||
@TableField("session_id")
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 候选人名称
|
||||
*/
|
||||
@TableField("candidate_name")
|
||||
private String candidateName;
|
||||
|
||||
/**
|
||||
* AI模型
|
||||
*/
|
||||
@TableField("ai_model")
|
||||
private String aiModel;
|
||||
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
@TableField("status")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 问题总数
|
||||
*/
|
||||
@TableField("total_questions")
|
||||
private Integer totalQuestions;
|
||||
|
||||
/**
|
||||
* 当前问题成绩
|
||||
*/
|
||||
@TableField("score")
|
||||
private BigDecimal score;
|
||||
|
||||
/**
|
||||
* 最终报告
|
||||
*/
|
||||
@TableField("final_report")
|
||||
private String finalReport;
|
||||
|
||||
/**
|
||||
* ai返回意见
|
||||
*/
|
||||
@TableField("feedback")
|
||||
private String feedback;
|
||||
|
||||
/**
|
||||
* 建议
|
||||
*/
|
||||
@TableField("suggestions")
|
||||
private String suggestions;
|
||||
|
||||
/**
|
||||
* AI返回答案
|
||||
*/
|
||||
@TableField("ai_answer")
|
||||
private String aiAnswer;
|
||||
|
||||
/**
|
||||
* 用户答案
|
||||
*/
|
||||
@TableField("user_answer")
|
||||
private String userAnswer;
|
||||
|
||||
@TableField(value = "created_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
@TableLogic
|
||||
@TableField("deleted")
|
||||
private Integer deleted;
|
||||
|
||||
public enum Status {
|
||||
DEFAULT, ACTIVE, COMPLETED, TERMINATED
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.qingqiu.interview.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("interview_session")
|
||||
public class InterviewSession implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@TableField("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@TableField("candidate_name")
|
||||
private String candidateName;
|
||||
|
||||
@TableField("resume_content")
|
||||
private String resumeContent;
|
||||
|
||||
@TableField("extracted_skills")
|
||||
private String extractedSkills;
|
||||
|
||||
@TableField("ai_model")
|
||||
private String aiModel;
|
||||
|
||||
@TableField("status")
|
||||
private String status;
|
||||
|
||||
@TableField("total_questions")
|
||||
private Integer totalQuestions;
|
||||
|
||||
@TableField("current_question_index")
|
||||
private Integer currentQuestionIndex;
|
||||
|
||||
@TableField("score")
|
||||
private BigDecimal score;
|
||||
|
||||
@TableField("selected_question_ids")
|
||||
private String selectedQuestionIds;
|
||||
|
||||
@TableField("final_report")
|
||||
private String finalReport;
|
||||
|
||||
@TableField(value = "created_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
@TableLogic
|
||||
@TableField("deleted")
|
||||
private Integer deleted;
|
||||
|
||||
public enum Status {
|
||||
ACTIVE, COMPLETED, TERMINATED
|
||||
}
|
||||
}
|
||||
41
src/main/java/com/qingqiu/interview/entity/Question.java
Normal file
41
src/main/java/com/qingqiu/interview/entity/Question.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.qingqiu.interview.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("question")
|
||||
public class Question {
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
|
||||
@TableField("content")
|
||||
private String content;
|
||||
|
||||
@TableField("category")
|
||||
private String category;
|
||||
|
||||
@TableField("difficulty")
|
||||
private String difficulty;
|
||||
|
||||
@TableField("tags")
|
||||
private String tags;
|
||||
|
||||
@TableField(value = "created_time", fill = FieldFill.INSERT)
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
@TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
@TableLogic
|
||||
@TableField("deleted")
|
||||
private Integer deleted;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.qingqiu.interview.mapper;
|
||||
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.qingqiu.interview.entity.AiSessionLog;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* ai会话记录 Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author huangpeng
|
||||
* @since 2025-08-30
|
||||
*/
|
||||
@Mapper
|
||||
public interface AiSessionLogMapper extends BaseMapper<AiSessionLog> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.qingqiu.interview.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.qingqiu.interview.entity.InterviewEvaluation;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface InterviewEvaluationMapper extends BaseMapper<InterviewEvaluation> {
|
||||
|
||||
List<InterviewEvaluation> selectBySessionId(@Param("sessionId") String sessionId);
|
||||
|
||||
InterviewEvaluation selectBySessionIdAndQuestionId(@Param("sessionId") String sessionId, @Param("questionId") Long questionId);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.qingqiu.interview.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.qingqiu.interview.entity.InterviewMessage;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface InterviewMessageMapper extends BaseMapper<InterviewMessage> {
|
||||
|
||||
List<InterviewMessage> selectBySessionIdOrderByOrder(@Param("sessionId") String sessionId);
|
||||
|
||||
InterviewMessage selectLatestBySessionId(@Param("sessionId") String sessionId);
|
||||
|
||||
int selectMaxOrderBySessionId(@Param("sessionId") String sessionId);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.qingqiu.interview.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.qingqiu.interview.entity.InterviewQuestionProgress;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 面试问题进度跟踪表 Mapper 接口
|
||||
* </p>
|
||||
*
|
||||
* @author huangpeng
|
||||
* @since 2025-08-30
|
||||
*/
|
||||
public interface InterviewQuestionProgressMapper extends BaseMapper<InterviewQuestionProgress> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.qingqiu.interview.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.qingqiu.interview.entity.InterviewSession;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface InterviewSessionMapper extends BaseMapper<InterviewSession> {
|
||||
|
||||
InterviewSession selectBySessionId(@Param("sessionId") String sessionId);
|
||||
|
||||
List<InterviewSession> selectActiveSessionsByModel(@Param("aiModel") String aiModel);
|
||||
|
||||
int updateSessionStatus(@Param("sessionId") String sessionId, @Param("status") String status);
|
||||
|
||||
List<com.qingqiu.interview.dto.DashboardStatsResponse.DailyStat> countRecentInterviews(@Param("days") int days);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.qingqiu.interview.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.qingqiu.interview.entity.Question;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface QuestionMapper extends BaseMapper<Question> {
|
||||
|
||||
List<Question> selectByCategory(@Param("category") String category);
|
||||
|
||||
List<Question> selectByCategories(@Param("categories") List<String> categories);
|
||||
|
||||
List<Question> selectRandomByCategories(@Param("categories") List<String> categories, @Param("limit") int limit);
|
||||
|
||||
Question selectByContent(@Param("content") String content);
|
||||
|
||||
List<com.qingqiu.interview.dto.DashboardStatsResponse.CategoryStat> countByCategory();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.qingqiu.interview.service;
|
||||
|
||||
import com.qingqiu.interview.mapper.InterviewSessionMapper;
|
||||
import com.qingqiu.interview.mapper.QuestionMapper;
|
||||
import com.qingqiu.interview.dto.DashboardStatsResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardService {
|
||||
|
||||
private final QuestionMapper questionMapper;
|
||||
private final InterviewSessionMapper sessionMapper;
|
||||
|
||||
public DashboardStatsResponse getDashboardStats() {
|
||||
DashboardStatsResponse stats = new DashboardStatsResponse();
|
||||
|
||||
// 1. 获取核心KPI
|
||||
stats.setTotalQuestions(questionMapper.selectCount(null));
|
||||
stats.setTotalInterviews(sessionMapper.selectCount(null));
|
||||
|
||||
// 2. 获取题库分类统计
|
||||
stats.setQuestionCategoryStats(questionMapper.countByCategory());
|
||||
|
||||
// 3. 获取最近7天的面试统计,并补全没有数据的日期
|
||||
List<DashboardStatsResponse.DailyStat> recentStats = sessionMapper.countRecentInterviews(7);
|
||||
stats.setRecentInterviewStats(fillMissingDates(recentStats, 7));
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充最近几天内没有面试数据的日期,补0
|
||||
*/
|
||||
private List<DashboardStatsResponse.DailyStat> fillMissingDates(List<DashboardStatsResponse.DailyStat> existingStats, int days) {
|
||||
Map<String, Long> statsMap = existingStats.stream()
|
||||
.collect(Collectors.toMap(DashboardStatsResponse.DailyStat::getDate, DashboardStatsResponse.DailyStat::getCount));
|
||||
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
return IntStream.range(0, days)
|
||||
.mapToObj(i -> LocalDate.now().minusDays(i))
|
||||
.map(date -> {
|
||||
String dateString = date.format(formatter);
|
||||
long count = statsMap.getOrDefault(dateString, 0L);
|
||||
return new DashboardStatsResponse.DailyStat(dateString, count);
|
||||
})
|
||||
.sorted((d1, d2) -> d1.getDate().compareTo(d2.getDate())) // 按日期升序排序
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.qingqiu.interview.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.qingqiu.interview.entity.AiSessionLog;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* ai会话记录 服务类
|
||||
* </p>
|
||||
*
|
||||
* @author huangpeng
|
||||
* @since 2025-08-30
|
||||
*/
|
||||
public interface IAiSessionLogService extends IService<AiSessionLog> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.qingqiu.interview.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.qingqiu.interview.dto.QuestionProgressPageParams;
|
||||
import com.qingqiu.interview.entity.InterviewQuestionProgress;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 面试问题进度跟踪表 服务类
|
||||
* </p>
|
||||
*
|
||||
* @author huangpeng
|
||||
* @since 2025-08-30
|
||||
*/
|
||||
public interface IInterviewQuestionProgressService extends IService<InterviewQuestionProgress> {
|
||||
Page<InterviewQuestionProgress> pageList(QuestionProgressPageParams params);
|
||||
}
|
||||
@@ -0,0 +1,654 @@
|
||||
package com.qingqiu.interview.service;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qingqiu.interview.dto.*;
|
||||
import com.qingqiu.interview.entity.*;
|
||||
import com.qingqiu.interview.mapper.*;
|
||||
import com.qingqiu.interview.service.llm.LlmService;
|
||||
import com.qingqiu.interview.service.parser.DocumentParser;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.qingqiu.interview.common.constants.QwenModelConstant.QWEN_MAX;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class InterviewService {
|
||||
|
||||
private final LlmService llmService; // Changed to a single service
|
||||
private final List<DocumentParser> documentParserList;
|
||||
private final QuestionMapper questionMapper;
|
||||
private final InterviewSessionMapper sessionMapper;
|
||||
private final InterviewMessageMapper messageMapper;
|
||||
private final InterviewEvaluationMapper evaluationMapper;
|
||||
private final InterviewQuestionProgressMapper questionProgressMapper;
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
|
||||
private Map<String, DocumentParser> documentParsers;
|
||||
|
||||
private static final int MAX_QUESTIONS_PER_INTERVIEW = 10;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
this.documentParsers = documentParserList.stream()
|
||||
.collect(Collectors.toMap(DocumentParser::getSupportedType, Function.identity()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始新的面试会话
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public InterviewResponse startInterview(MultipartFile resume, InterviewStartRequest request) throws IOException {
|
||||
log.info("开始新面试会话,候选人: {}, AI模型: qwen-max", request.getCandidateName());
|
||||
|
||||
// 1. 解析简历
|
||||
String resumeContent = parseResume(resume);
|
||||
|
||||
|
||||
// 2. 创建会话 并发送AI请求 让其从题库中智能抽题
|
||||
String sessionId = UUID.randomUUID().toString();
|
||||
List<Question> selectedQuestions = selectQuestionsByAi(resumeContent, sessionId);
|
||||
if (selectedQuestions.isEmpty()) {
|
||||
throw new IllegalStateException("AI未能成功选取题目,请检查AI服务或题库。");
|
||||
}
|
||||
|
||||
// 生成面试问题进度数据
|
||||
if (CollectionUtil.isNotEmpty(selectedQuestions)) {
|
||||
for (Question question : selectedQuestions) {
|
||||
InterviewQuestionProgress progress =
|
||||
new InterviewQuestionProgress()
|
||||
.setSessionId(sessionId)
|
||||
.setQuestionId(question.getId())
|
||||
.setQuestionContent(question.getContent())
|
||||
.setStatus(InterviewQuestionProgress.Status.DEFAULT.name())
|
||||
.setTotalQuestions(selectedQuestions.size())
|
||||
.setScore(BigDecimal.ZERO)
|
||||
.setAiModel(QWEN_MAX)
|
||||
.setCandidateName(request.getCandidateName());
|
||||
questionProgressMapper.insert(progress);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 3. 保存AI选择的题目ID列表
|
||||
List<Long> selectedQuestionIds = selectedQuestions.stream().map(Question::getId).collect(Collectors.toList());
|
||||
String selectedQuestionIdsJson = objectMapper.writeValueAsString(selectedQuestionIds);
|
||||
|
||||
InterviewSession session = createSession(sessionId, request, resumeContent, selectedQuestionIdsJson);
|
||||
session.setTotalQuestions(selectedQuestions.size()); // 更新会话中的总问题数
|
||||
sessionMapper.updateById(session); // 更新数据库
|
||||
|
||||
// 4. 生成第一个问题
|
||||
Question firstQuestion = selectedQuestions.get(0);
|
||||
String firstQuestionContent = generateFirstQuestion(session, firstQuestion, sessionId);
|
||||
// 激活问题
|
||||
questionProgressMapper.update(
|
||||
new LambdaUpdateWrapper<InterviewQuestionProgress>()
|
||||
.set(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.ACTIVE.name())
|
||||
.eq(InterviewQuestionProgress::getQuestionId, firstQuestion.getId())
|
||||
.eq(InterviewQuestionProgress::getSessionId, sessionId)
|
||||
);
|
||||
|
||||
// 5. 保存消息记录
|
||||
saveMessage(sessionId, InterviewMessage.MessageType.QUESTION.name(),
|
||||
InterviewMessage.Sender.AI.name(), firstQuestionContent, firstQuestion.getId(), 1);
|
||||
|
||||
// 6. 返回响应
|
||||
return new InterviewResponse()
|
||||
.setSessionId(sessionId)
|
||||
.setMessage(firstQuestionContent)
|
||||
.setMessageType(InterviewMessage.MessageType.QUESTION.name())
|
||||
.setSender(InterviewMessage.Sender.AI.name())
|
||||
.setCurrentQuestionIndex(1)
|
||||
.setCurrentQuestionId(firstQuestion.getId())
|
||||
.setTotalQuestions(selectedQuestions.size())
|
||||
.setStatus(InterviewSession.Status.ACTIVE.name());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户回答并生成下一个问题
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public InterviewResponse continueInterview(ChatRequest request) {
|
||||
log.info("继续面试会话: {}", request.getSessionId());
|
||||
|
||||
InterviewSession session = sessionMapper.selectBySessionId(request.getSessionId());
|
||||
if (session == null) {
|
||||
throw new IllegalArgumentException("会话不存在: " + request.getSessionId());
|
||||
}
|
||||
|
||||
if (!InterviewSession.Status.ACTIVE.name().equals(session.getStatus())) {
|
||||
throw new IllegalStateException("会话已结束");
|
||||
}
|
||||
|
||||
|
||||
// 1. 保存用户回答
|
||||
int nextOrder = messageMapper.selectMaxOrderBySessionId(request.getSessionId()) + 1;
|
||||
saveMessage(request.getSessionId(), InterviewMessage.MessageType.ANSWER.name(),
|
||||
InterviewMessage.Sender.USER.name(), request.getUserAnswer(), null, nextOrder);
|
||||
// 检查是否结束面试
|
||||
InterviewQuestionProgress progress = questionProgressMapper.selectOne(
|
||||
new LambdaQueryWrapper<InterviewQuestionProgress>()
|
||||
.eq(InterviewQuestionProgress::getSessionId, request.getSessionId())
|
||||
.orderByDesc(InterviewQuestionProgress::getCreatedTime)
|
||||
.last("limit 1")
|
||||
);
|
||||
if (Objects.nonNull(progress) && Objects.equals(progress.getQuestionId(), request.getCurrentQuestionId())) {
|
||||
|
||||
}
|
||||
// 2. 评估回答
|
||||
Long currentQuestionId = evaluateAnswer(session, request.getUserAnswer());
|
||||
// 比对返回的id是否与当前id一致
|
||||
if (currentQuestionId.equals(0L)) {
|
||||
return finishInterview(session);
|
||||
}
|
||||
InterviewQuestionProgress nextQuestionProgress = questionProgressMapper.selectOne(
|
||||
new LambdaQueryWrapper<InterviewQuestionProgress>()
|
||||
.eq(InterviewQuestionProgress::getSessionId, request.getSessionId())
|
||||
.eq(InterviewQuestionProgress::getQuestionId, currentQuestionId)
|
||||
.orderByDesc(InterviewQuestionProgress::getCreatedTime)
|
||||
.last("limit 1")
|
||||
);
|
||||
// 将ai返回的内容拼装返回给页面
|
||||
// 查询数据
|
||||
InterviewQuestionProgress currentQuestionData = questionProgressMapper.selectOne(
|
||||
new LambdaQueryWrapper<InterviewQuestionProgress>()
|
||||
.eq(InterviewQuestionProgress::getSessionId, request.getSessionId())
|
||||
.eq(InterviewQuestionProgress::getQuestionId, request.getCurrentQuestionId())
|
||||
.orderByDesc(InterviewQuestionProgress::getCreatedTime)
|
||||
.last("limit 1")
|
||||
);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (Objects.nonNull(currentQuestionData)) {
|
||||
if (StringUtils.isNotBlank(currentQuestionData.getFeedback())) {
|
||||
sb.append(currentQuestionData.getFeedback()).append("\n");
|
||||
}
|
||||
if (StringUtils.isNotBlank(currentQuestionData.getSuggestions())) {
|
||||
sb.append(currentQuestionData.getSuggestions()).append("\n");
|
||||
}
|
||||
if (StringUtils.isNotBlank(currentQuestionData.getAiAnswer())) {
|
||||
sb.append(currentQuestionData.getAiAnswer()).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (!currentQuestionId.equals(request.getCurrentQuestionId())) {
|
||||
// 5. 生成并保存AI的提问消息
|
||||
String nextQuestionContent = String.format("好的,下一个问题是:%s", nextQuestionProgress.getQuestionContent());
|
||||
sb.append(nextQuestionContent);
|
||||
int messageOrder = messageMapper.selectMaxOrderBySessionId(session.getSessionId()) + 1;
|
||||
saveMessage(session.getSessionId(), InterviewMessage.MessageType.QUESTION.name(),
|
||||
InterviewMessage.Sender.AI.name(), nextQuestionContent, currentQuestionId, messageOrder);
|
||||
}
|
||||
// 6. 返回响应
|
||||
return new InterviewResponse()
|
||||
.setSessionId(session.getSessionId())
|
||||
.setMessage(sb.toString())
|
||||
.setMessageType(InterviewMessage.MessageType.QUESTION.name())
|
||||
.setSender(InterviewMessage.Sender.AI.name())
|
||||
.setCurrentQuestionIndex(session.getCurrentQuestionIndex())
|
||||
.setTotalQuestions(session.getTotalQuestions())
|
||||
.setCurrentQuestionId(currentQuestionId)
|
||||
.setStatus(InterviewSession.Status.ACTIVE.name());
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入题库(使用AI自动分类)
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* 获取会话历史
|
||||
*/
|
||||
public SessionHistoryResponse getSessionHistory(String sessionId) {
|
||||
InterviewSession session = sessionMapper.selectBySessionId(sessionId);
|
||||
if (session == null) {
|
||||
throw new IllegalArgumentException("会话不存在: " + sessionId);
|
||||
}
|
||||
|
||||
List<InterviewMessage> messages = messageMapper.selectBySessionIdOrderByOrder(sessionId);
|
||||
List<SessionHistoryResponse.MessageDto> messageDtos = messages.stream()
|
||||
.map(msg -> new SessionHistoryResponse.MessageDto()
|
||||
.setMessageType(msg.getMessageType())
|
||||
.setSender(msg.getSender())
|
||||
.setContent(msg.getContent())
|
||||
.setMessageOrder(msg.getMessageOrder())
|
||||
.setCreatedTime(msg.getCreatedTime()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return new SessionHistoryResponse()
|
||||
.setSessionId(sessionId)
|
||||
.setCandidateName(session.getCandidateName())
|
||||
.setAiModel(session.getAiModel())
|
||||
.setStatus(session.getStatus())
|
||||
.setTotalQuestions(session.getTotalQuestions())
|
||||
.setCurrentQuestionIndex(session.getCurrentQuestionIndex())
|
||||
.setCreatedTime(session.getCreatedTime())
|
||||
.setMessages(messageDtos);
|
||||
}
|
||||
|
||||
private String parseResume(MultipartFile resume) throws IOException {
|
||||
String fileExtension = getFileExtension(resume.getOriginalFilename());
|
||||
DocumentParser parser = documentParsers.get(fileExtension);
|
||||
if (parser == null) {
|
||||
throw new IllegalArgumentException("不支持的简历文件类型: " + fileExtension);
|
||||
}
|
||||
return parser.parse(resume.getInputStream());
|
||||
}
|
||||
|
||||
private List<Question> selectQuestionsByAi(String resumeContent, String sessionId) throws JsonProcessingException {
|
||||
// 1. 获取全部题库
|
||||
List<Question> allQuestions = questionMapper.selectList(null);
|
||||
String questionBankJson = objectMapper.writeValueAsString(allQuestions);
|
||||
|
||||
// 2. 构建发送给AI的提示
|
||||
String prompt = String.format("""
|
||||
你是一位专业的面试官。请根据以下候选人的简历内容,从提供的题库中,精心挑选出 %d 道最相关的题目进行面试。
|
||||
|
||||
要求:
|
||||
1. 题目必须严格从【题库JSON】中选择。
|
||||
2. 挑选的题目应根据候选人的简历内容来抽取。
|
||||
3. 返回一个只包含所选题目ID的JSON数组,格式为:{"question_ids": [1, 5, 23, ...]}。
|
||||
4. 不要返回任何多余的代码,包括markdown形式的代码,我只需要JSON对象,请严格按照api接口形式返回
|
||||
5. 不要返回任何额外的解释或文字,只返回JSON对象。
|
||||
6. 严格按照前后端分离的接口形式返回JSON数据给我,不要返回"```json```"
|
||||
7. 请保证返回数据的完整性,不要返回不完整的数据,否则我的JSON解析会报错!!!
|
||||
|
||||
【候选人简历】:
|
||||
%s
|
||||
|
||||
【题库JSON】:
|
||||
%s
|
||||
""", MAX_QUESTIONS_PER_INTERVIEW, resumeContent, questionBankJson);
|
||||
|
||||
// 3. 调用AI服务
|
||||
String aiResponse = llmService.chat(prompt);
|
||||
log.info("AI抽题响应: {}", aiResponse);
|
||||
|
||||
// 4. 解析AI返回的题目ID
|
||||
List<Long> selectedIds = new ArrayList<>();
|
||||
try {
|
||||
JsonNode rootNode = objectMapper.readTree(aiResponse);
|
||||
JsonNode idsNode = rootNode.get("question_ids");
|
||||
if (idsNode != null && idsNode.isArray()) {
|
||||
for (JsonNode idNode : idsNode) {
|
||||
selectedIds.add(idNode.asLong());
|
||||
}
|
||||
}
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("解析AI返回的题目ID列表失败", e);
|
||||
return Collections.emptyList(); // 解析失败则返回空列表
|
||||
}
|
||||
|
||||
if (selectedIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 5. 根据ID从数据库中获取完整的题目信息,并保持AI选择的顺序
|
||||
List<Question> finalQuestions = questionMapper.selectBatchIds(selectedIds);
|
||||
finalQuestions.sort(Comparator.comparing(q -> selectedIds.indexOf(q.getId()))); // 保持AI返回的顺序
|
||||
|
||||
return finalQuestions;
|
||||
}
|
||||
|
||||
private InterviewSession createSession(String sessionId, InterviewStartRequest request,
|
||||
String resumeContent, String selectedQuestionIdsJson) {
|
||||
InterviewSession session = new InterviewSession()
|
||||
.setSessionId(sessionId)
|
||||
.setCandidateName(request.getCandidateName())
|
||||
.setResumeContent(resumeContent)
|
||||
.setSelectedQuestionIds(selectedQuestionIdsJson)
|
||||
.setAiModel("qwen-max") // Hardcoded to qwen-max
|
||||
.setStatus(InterviewSession.Status.ACTIVE.name())
|
||||
.setCurrentQuestionIndex(0);
|
||||
|
||||
sessionMapper.insert(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
private String generateFirstQuestion(InterviewSession session, Question question, String sessionId) {
|
||||
String prompt = String.format("""
|
||||
你是一位专业的技术面试官。现在要开始面试,候选人是 %s。
|
||||
|
||||
第一个问题是:%s
|
||||
|
||||
请以友好但专业的语气提出这个问题,可以适当添加一些引导性的话语。
|
||||
""", session.getCandidateName(), question.getContent());
|
||||
|
||||
return this.llmService.chat(prompt, sessionId);
|
||||
}
|
||||
|
||||
private void saveMessage(String sessionId, String messageType, String sender,
|
||||
String content, Long questionId, int order) {
|
||||
InterviewMessage message = new InterviewMessage()
|
||||
.setSessionId(sessionId)
|
||||
.setMessageType(messageType)
|
||||
.setSender(sender)
|
||||
.setContent(content)
|
||||
.setQuestionId(questionId)
|
||||
.setMessageOrder(order);
|
||||
|
||||
messageMapper.insert(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 评估答案
|
||||
*
|
||||
* @param session 会话数据
|
||||
* @param userAnswer 用户回答
|
||||
* @return 当前问题id
|
||||
*/
|
||||
private Long evaluateAnswer(InterviewSession session, String userAnswer) {
|
||||
// 根据会话id查询当前会话所有问题
|
||||
List<InterviewQuestionProgress> interviewQuestionProgresses = questionProgressMapper.selectList(
|
||||
new LambdaQueryWrapper<InterviewQuestionProgress>()
|
||||
.eq(InterviewQuestionProgress::getSessionId, session.getSessionId())
|
||||
.orderByAsc(InterviewQuestionProgress::getCreatedTime)
|
||||
);
|
||||
if (CollectionUtil.isEmpty(interviewQuestionProgresses)) {
|
||||
throw new RuntimeException("当前会话没有任何可询问的问题!");
|
||||
}
|
||||
|
||||
// 1. 获取当前正在回答的问题
|
||||
InterviewQuestionProgress currentQuestionProgress = null;
|
||||
for (InterviewQuestionProgress interviewQuestionProgress : interviewQuestionProgresses) {
|
||||
if (interviewQuestionProgress.getStatus().equals(InterviewQuestionProgress.Status.ACTIVE.name())) {
|
||||
currentQuestionProgress = interviewQuestionProgress;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (Objects.isNull(currentQuestionProgress)) {
|
||||
throw new RuntimeException("当前没有正在回答的问题");
|
||||
}
|
||||
Long currentQuestionId = currentQuestionProgress.getQuestionId();
|
||||
|
||||
|
||||
List<String> questionIds = interviewQuestionProgresses.stream()
|
||||
.map(data -> {
|
||||
return data.getQuestionId().toString();
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
String join = String.join(",", questionIds);
|
||||
// 2. 构建评估提示
|
||||
String prompt = String.format("""
|
||||
你是一位资深的技术面试官。请根据以下问题和候选人的回答,进行一次专业的评估。
|
||||
|
||||
要求:
|
||||
1. 对回答的质量进行打分,分数范围为1-5分。
|
||||
2. 给出简洁、专业的评语。
|
||||
3. 提出具体的改进建议以及你认为应该回答的答案。
|
||||
4. 以严格的JSON格式返回,不要包含任何额外的解释文字。格式如下:
|
||||
{
|
||||
"score": 4.5,
|
||||
"feedback": "回答基本正确,但可以更深入...",
|
||||
"suggestions": "可以补充关于XXX方面的知识点...",
|
||||
"answer": "关于当前问题,您应该这样回答xxx",
|
||||
"currentQuestionId": xxx
|
||||
}
|
||||
5. 不要返回任何多余字符,请严格按照api接口格式的JSON数据进行返回,不要包含"```json```"
|
||||
6. 如果你认为面试人对当前问题回答不完美,可以继续对当前问题进行补充提问,但不要修改currentQuestionId
|
||||
7. 如果你认为面试人对当前问题回答已经比较好了,或者面试人回答不上来了,请你根据questionIds数据顺序选择下一个问题,并修改currentQuestionId进行返回
|
||||
8. 如果所有问题都已回答完成,请将currentQuestionId设置为0
|
||||
{
|
||||
"questionIds": %s,
|
||||
"currentQuestionId": %s
|
||||
}
|
||||
【面试问题】:
|
||||
%s
|
||||
|
||||
【候选人回答】:
|
||||
%s
|
||||
""", join, currentQuestionProgress.getQuestionId(), currentQuestionProgress.getQuestionContent(), userAnswer);
|
||||
|
||||
// 3. 调用AI进行评估
|
||||
String aiResponse = llmService.chat(prompt, session.getSessionId());
|
||||
log.info("AI评估响应: {}", aiResponse);
|
||||
|
||||
// 4. 解析AI响应并存储评估结果
|
||||
try {
|
||||
JsonNode rootNode = objectMapper.readTree(aiResponse);
|
||||
InterviewEvaluation evaluation = new InterviewEvaluation()
|
||||
.setSessionId(session.getSessionId())
|
||||
.setQuestionId(currentQuestionId)
|
||||
.setUserAnswer(userAnswer)
|
||||
.setScore(new java.math.BigDecimal(rootNode.get("score").asText()))
|
||||
.setAiFeedback(rootNode.get("feedback").asText())
|
||||
.setEvaluationCriteria(rootNode.get("suggestions").asText()); // 暂时复用这个字段存建议
|
||||
JsonNode currentQuestionId1 = rootNode.get("currentQuestionId");
|
||||
JsonNode aiAnswerNode = rootNode.get("answer");
|
||||
if (Objects.nonNull(currentQuestionId1)) {
|
||||
String text = currentQuestionId1.asText();
|
||||
if (StringUtils.isNoneBlank(text)) {
|
||||
currentQuestionProgress
|
||||
.setScore(new BigDecimal(rootNode.get("score").asText()))
|
||||
.setSuggestions(rootNode.get("suggestions").asText())
|
||||
.setFeedback(rootNode.get("feedback").asText())
|
||||
.setAiAnswer(Objects.nonNull(aiAnswerNode) ? aiAnswerNode.asText() : null)
|
||||
.setUserAnswer(userAnswer)
|
||||
;
|
||||
if (!StrUtil.equals(text, currentQuestionProgress.getQuestionId().toString())) {
|
||||
currentQuestionProgress.setStatus(InterviewQuestionProgress.Status.COMPLETED.name());
|
||||
questionProgressMapper.updateById(currentQuestionProgress);
|
||||
questionProgressMapper.update(
|
||||
new LambdaUpdateWrapper<InterviewQuestionProgress>()
|
||||
.set(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.ACTIVE.name())
|
||||
.eq(InterviewQuestionProgress::getSessionId, session.getSessionId())
|
||||
.eq(InterviewQuestionProgress::getQuestionId, Long.valueOf(text))
|
||||
);
|
||||
} else if (text.equals("0")) {
|
||||
currentQuestionProgress.setStatus(InterviewQuestionProgress.Status.COMPLETED.name());
|
||||
questionProgressMapper.updateById(currentQuestionProgress);
|
||||
}
|
||||
currentQuestionId = Long.valueOf(text);
|
||||
}
|
||||
}
|
||||
evaluationMapper.insert(evaluation);
|
||||
log.info("成功存储对问题ID {} 的评估结果", currentQuestionId);
|
||||
return currentQuestionId;
|
||||
} catch (Exception e) {
|
||||
log.error("解析或存储AI评估结果失败", e);
|
||||
throw new RuntimeException("解析或存储AI评估结果失败");
|
||||
}
|
||||
}
|
||||
|
||||
private InterviewResponse finishInterview(InterviewSession session) {
|
||||
// 1. 获取本次面试的所有评估数据
|
||||
List<InterviewEvaluation> evaluations = evaluationMapper.selectBySessionId(session.getSessionId());
|
||||
|
||||
// 2. 构建生成最终报告的提示
|
||||
String prompt = buildFinalReportPrompt(session, evaluations);
|
||||
|
||||
// 3. 调用AI生成报告
|
||||
String finalReportJson = llmService.chat(prompt, session.getSessionId());
|
||||
log.info("AI生成的最终面试报告: {}", finalReportJson);
|
||||
|
||||
// 4. 更新会话状态和最终报告
|
||||
session.setStatus(InterviewSession.Status.COMPLETED.name());
|
||||
session.setFinalReport(finalReportJson);
|
||||
sessionMapper.updateById(session);
|
||||
|
||||
// 5. 返回结束信息
|
||||
return new InterviewResponse()
|
||||
.setSessionId(session.getSessionId())
|
||||
.setMessage("面试已结束,感谢您的参与!AI正在生成您的面试报告,请稍后在面试历史中查看。")
|
||||
.setMessageType(InterviewMessage.MessageType.SYSTEM.name())
|
||||
.setSender(InterviewMessage.Sender.SYSTEM.name())
|
||||
.setCurrentQuestionId(null)
|
||||
.setStatus(InterviewSession.Status.COMPLETED.name());
|
||||
}
|
||||
|
||||
private String buildFinalReportPrompt(InterviewSession session, List<InterviewEvaluation> evaluations) {
|
||||
StringBuilder historyBuilder = new StringBuilder();
|
||||
for (InterviewEvaluation eval : evaluations) {
|
||||
Question q = questionMapper.selectById(eval.getQuestionId());
|
||||
historyBuilder.append(String.format("\n【问题】: %s\n【回答】: %s\n【AI单题反馈】: %s\n【AI单题建议】: %s\n【AI单题评分】: %s/5.0\n",
|
||||
q.getContent(), eval.getUserAnswer(), eval.getAiFeedback(), eval.getEvaluationCriteria(), eval.getScore()));
|
||||
}
|
||||
|
||||
return String.format("""
|
||||
你是一位资深的HR和技术总监。请根据以下候选人的简历、完整的面试问答历史和AI对每一题的初步评估,给出一份全面、专业、有深度的最终面试报告。
|
||||
|
||||
要求:
|
||||
1. **综合评价**: 对候选人的整体表现给出一个总结性的评语,点出其核心亮点和主要不足。
|
||||
2. **技术能力评估**: 分点阐述候选人在不同技术领域(如Java基础, Spring, 数据库等)的掌握程度。
|
||||
3. **改进建议**: 给出3-5条具体的、可操作的学习和改进建议。
|
||||
4. **综合得分**: 给出一个1-100分的最终综合得分。
|
||||
5. **录用建议**: 给出明确的录用建议(如:强烈推荐、推荐、待考虑、不推荐)。
|
||||
6. 以严格的JSON格式返回,不要包含任何额外的解释文字。格式如下:
|
||||
{
|
||||
"overallScore": 85,
|
||||
"overallFeedback": "候选人Java基础扎实,但在高并发场景下的经验有所欠缺...",
|
||||
"technicalAssessment": {
|
||||
"Java基础": "掌握良好,对集合框架理解深入。",
|
||||
"Spring框架": "熟悉基本使用,但对底层原理理解不足。",
|
||||
"数据库": "能够编写常规SQL,但在索引优化方面知识欠缺。"
|
||||
},
|
||||
"suggestions": [
|
||||
"深入学习Spring AOP和事务管理的实现原理。",
|
||||
"系统学习MySQL索引优化和查询性能分析。",
|
||||
"通过实际项目积累高并发处理经验。"
|
||||
],
|
||||
"hiringRecommendation": "推荐"
|
||||
}
|
||||
|
||||
【候选人简历摘要】:
|
||||
%s
|
||||
|
||||
【面试问答与评估历史】:
|
||||
%s
|
||||
""", session.getResumeContent(), historyBuilder.toString());
|
||||
}
|
||||
|
||||
private InterviewResponse generateNextQuestion(InterviewSession session) {
|
||||
try {
|
||||
// 1. 解析出AI选择的题目ID列表
|
||||
List<Long> selectedQuestionIds = objectMapper.readValue(session.getSelectedQuestionIds(), new com.fasterxml.jackson.core.type.TypeReference<List<Long>>() {
|
||||
});
|
||||
|
||||
// 2. 获取下一个问题的索引
|
||||
int nextQuestionIndex = session.getCurrentQuestionIndex(); // 数据库中存的是已回答问题的数量
|
||||
if (nextQuestionIndex >= selectedQuestionIds.size()) {
|
||||
return finishInterview(session); // 如果没有更多问题,则结束面试
|
||||
}
|
||||
|
||||
// 3. 获取下一个问题的ID并从数据库查询
|
||||
Long nextQuestionId = selectedQuestionIds.get(nextQuestionIndex);
|
||||
Question nextQuestion = questionMapper.selectById(nextQuestionId);
|
||||
if (nextQuestion == null) {
|
||||
log.error("无法找到ID为 {} 的问题,跳过此问题。", nextQuestionId);
|
||||
// 更新会话状态并尝试下一个问题
|
||||
session.setCurrentQuestionIndex(nextQuestionIndex + 1);
|
||||
sessionMapper.updateById(session);
|
||||
return generateNextQuestion(session); // 递归调用以获取再下一个问题
|
||||
}
|
||||
|
||||
// 4. 更新会话状态(当前问题索引+1)
|
||||
session.setCurrentQuestionIndex(nextQuestionIndex + 1);
|
||||
sessionMapper.updateById(session);
|
||||
|
||||
// 5. 生成并保存AI的提问消息
|
||||
String questionContent = String.format("好的,下一个问题是:%s", nextQuestion.getContent());
|
||||
int messageOrder = messageMapper.selectMaxOrderBySessionId(session.getSessionId()) + 1;
|
||||
saveMessage(session.getSessionId(), InterviewMessage.MessageType.QUESTION.name(),
|
||||
InterviewMessage.Sender.AI.name(), questionContent, nextQuestion.getId(), messageOrder);
|
||||
|
||||
// 6. 返回响应
|
||||
return new InterviewResponse()
|
||||
.setSessionId(session.getSessionId())
|
||||
.setMessage(questionContent)
|
||||
.setMessageType(InterviewMessage.MessageType.QUESTION.name())
|
||||
.setSender(InterviewMessage.Sender.AI.name())
|
||||
.setCurrentQuestionIndex(session.getCurrentQuestionIndex())
|
||||
.setTotalQuestions(session.getTotalQuestions())
|
||||
.setStatus(InterviewSession.Status.ACTIVE.name());
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("解析会话中的题目ID列表失败", e);
|
||||
return finishInterview(session); // 解析失败则直接结束面试
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取所有面试会话列表
|
||||
*/
|
||||
public List<InterviewSession> getInterviewSessions() {
|
||||
log.info("Fetching all interview sessions");
|
||||
return sessionMapper.selectList(null); // 实际中可能需要分页
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详细的面试复盘报告
|
||||
*/
|
||||
public InterviewReportResponse getInterviewReport(String sessionId) {
|
||||
log.info("Fetching interview report for session id: {}", sessionId);
|
||||
|
||||
InterviewSession session = sessionMapper.selectBySessionId(sessionId);
|
||||
if (session == null) {
|
||||
throw new IllegalArgumentException("找不到ID为 " + sessionId + " 的面试会话。");
|
||||
}
|
||||
|
||||
List<InterviewEvaluation> evaluations = evaluationMapper.selectBySessionId(sessionId);
|
||||
|
||||
List<InterviewReportResponse.QuestionDetail> questionDetails = evaluations.stream().map(eval -> {
|
||||
Question question = questionMapper.selectById(eval.getQuestionId());
|
||||
InterviewReportResponse.QuestionDetail detail = new InterviewReportResponse.QuestionDetail();
|
||||
detail.setQuestionId(eval.getQuestionId());
|
||||
detail.setQuestionContent(question != null ? question.getContent() : "题目已不存在");
|
||||
detail.setUserAnswer(eval.getUserAnswer());
|
||||
detail.setAiFeedback(eval.getAiFeedback());
|
||||
detail.setSuggestions(eval.getEvaluationCriteria());
|
||||
detail.setScore(eval.getScore());
|
||||
return detail;
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
InterviewReportResponse report = new InterviewReportResponse();
|
||||
report.setSessionDetails(session);
|
||||
report.setQuestionDetails(questionDetails);
|
||||
List<InterviewMessage> interviewMessages = messageMapper.selectList(
|
||||
new LambdaQueryWrapper<InterviewMessage>()
|
||||
.eq(InterviewMessage::getSessionId, sessionId)
|
||||
);
|
||||
// 获取当前面试的 问题
|
||||
InterviewQuestionProgress progress = questionProgressMapper.selectOne(
|
||||
new LambdaQueryWrapper<InterviewQuestionProgress>()
|
||||
.eq(InterviewQuestionProgress::getSessionId, sessionId)
|
||||
.eq(InterviewQuestionProgress::getStatus, InterviewQuestionProgress.Status.ACTIVE.name())
|
||||
.last("LIMIT 1")
|
||||
);
|
||||
if (Objects.nonNull(progress)) {
|
||||
report.setCurrentQuestionId(progress.getQuestionId());
|
||||
}
|
||||
report.setMessages(interviewMessages);
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
private String getFileExtension(String fileName) {
|
||||
if (fileName == null || fileName.lastIndexOf('.') == -1) {
|
||||
return "";
|
||||
}
|
||||
return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.qingqiu.interview.service;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.qingqiu.interview.entity.Question;
|
||||
import com.qingqiu.interview.service.llm.LlmService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class QuestionClassificationService {
|
||||
|
||||
private final LlmService llmService;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 使用AI对题目进行分类
|
||||
*/
|
||||
public List<Question> classifyQuestions(String rawContent) {
|
||||
log.info("开始使用AI分类题目,内容长度: {}", rawContent.length());
|
||||
|
||||
String prompt = buildClassificationPrompt(rawContent);
|
||||
String aiResponse = llmService.chat(prompt);
|
||||
|
||||
log.info("AI分类响应: {}", aiResponse);
|
||||
|
||||
return parseAiResponse(aiResponse);
|
||||
}
|
||||
|
||||
private String buildClassificationPrompt(String content) {
|
||||
return """
|
||||
请分析以下面试题内容,将其分类并提取信息。请严格按照以下JSON格式返回结果:
|
||||
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"content": "题目内容",
|
||||
"category": "分类(如:Java基础、Spring框架、数据库、算法、系统设计等)",
|
||||
"difficulty": "难度(Easy、Medium、Hard)",
|
||||
"tags": "相关标签,用逗号分隔"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
分类规则:
|
||||
1. category应该是具体的技术领域,如:Java基础、Spring框架、MySQL、Redis、算法与数据结构、系统设计、网络协议等
|
||||
2. difficulty根据题目复杂度判断:Easy(基础概念)、Medium(实际应用)、Hard(深入原理或复杂场景)
|
||||
3. tags包含更细粒度的标签,如:多线程、JVM、事务、索引等
|
||||
4. 如果内容包含多个独立的题目,请分别提取
|
||||
5. 只返回JSON,不要其他解释文字
|
||||
|
||||
待分析内容:
|
||||
""" + content;
|
||||
}
|
||||
|
||||
private List<Question> parseAiResponse(String aiResponse) {
|
||||
List<Question> questions = new ArrayList<>();
|
||||
|
||||
try {
|
||||
// 清理响应,移除可能的markdown标记
|
||||
String cleanResponse = aiResponse.trim();
|
||||
if (cleanResponse.startsWith("```json")) {
|
||||
cleanResponse = cleanResponse.substring(7);
|
||||
}
|
||||
if (cleanResponse.endsWith("```")) {
|
||||
cleanResponse = cleanResponse.substring(0, cleanResponse.length() - 3);
|
||||
}
|
||||
cleanResponse = cleanResponse.trim();
|
||||
|
||||
JsonNode rootNode = objectMapper.readTree(cleanResponse);
|
||||
JsonNode questionsNode = rootNode.get("questions");
|
||||
|
||||
if (questionsNode != null && questionsNode.isArray()) {
|
||||
for (JsonNode questionNode : questionsNode) {
|
||||
Question question = new Question()
|
||||
.setContent(getTextValue(questionNode, "content"))
|
||||
.setCategory(getTextValue(questionNode, "category"))
|
||||
.setDifficulty(getTextValue(questionNode, "difficulty"))
|
||||
.setTags(getTextValue(questionNode, "tags"));
|
||||
|
||||
if (isValidQuestion(question)) {
|
||||
questions.add(question);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("成功解析出 {} 个题目", questions.size());
|
||||
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("解析AI响应失败: {}", e.getMessage());
|
||||
log.error("原始响应: {}", aiResponse);
|
||||
|
||||
// 降级处理:如果AI返回格式不正确,尝试简单分割
|
||||
questions.addAll(fallbackParsing(aiResponse));
|
||||
}
|
||||
|
||||
return questions;
|
||||
}
|
||||
|
||||
private String getTextValue(JsonNode node, String fieldName) {
|
||||
JsonNode fieldNode = node.get(fieldName);
|
||||
return fieldNode != null ? fieldNode.asText("") : "";
|
||||
}
|
||||
|
||||
private boolean isValidQuestion(Question question) {
|
||||
return question.getContent() != null && !question.getContent().trim().isEmpty()
|
||||
&& question.getCategory() != null && !question.getCategory().trim().isEmpty();
|
||||
}
|
||||
|
||||
private List<Question> fallbackParsing(String content) {
|
||||
log.warn("使用降级解析策略");
|
||||
List<Question> questions = new ArrayList<>();
|
||||
|
||||
// 简单的降级策略:按行分割,每行作为一个题目
|
||||
String[] lines = content.split("\n");
|
||||
for (String line : lines) {
|
||||
line = line.trim();
|
||||
if (!line.isEmpty() && line.length() > 10) { // 过滤太短的内容
|
||||
Question question = new Question()
|
||||
.setContent(line)
|
||||
.setCategory("未分类")
|
||||
.setDifficulty("Medium")
|
||||
.setTags("待分类");
|
||||
questions.add(question);
|
||||
}
|
||||
}
|
||||
|
||||
return questions;
|
||||
}
|
||||
}
|
||||
177
src/main/java/com/qingqiu/interview/service/QuestionService.java
Normal file
177
src/main/java/com/qingqiu/interview/service/QuestionService.java
Normal file
@@ -0,0 +1,177 @@
|
||||
package com.qingqiu.interview.service;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.qingqiu.interview.dto.QuestionPageParams;
|
||||
import com.qingqiu.interview.entity.Question;
|
||||
import com.qingqiu.interview.mapper.QuestionMapper;
|
||||
import com.qingqiu.interview.service.llm.LlmService;
|
||||
import com.qingqiu.interview.service.parser.DocumentParser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class QuestionService {
|
||||
|
||||
private final QuestionMapper questionMapper;
|
||||
private final QuestionClassificationService classificationService;
|
||||
private final List<DocumentParser> documentParserList; // This will be injected by Spring
|
||||
private final LlmService llmService;
|
||||
|
||||
/**
|
||||
* 分页查询题库
|
||||
*/
|
||||
public Page<Question> getQuestionPage(QuestionPageParams params) {
|
||||
log.info("分页查询题库,当前页: {}, 每页数量: {}", params.getCurrent(), params.getSize());
|
||||
return questionMapper.selectPage(
|
||||
Page.of(params.getCurrent(), params.getSize()),
|
||||
new LambdaQueryWrapper<Question>()
|
||||
.like(StringUtils.isNotBlank(params.getContent()), Question::getContent, params.getContent())
|
||||
.orderByDesc(Question::getCreatedTime)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增题目,并进行重复校验
|
||||
*/
|
||||
public void addQuestion(Question question) {
|
||||
validateQuestion(question.getContent(), null);
|
||||
log.info("新增题目: {}", question.getContent());
|
||||
questionMapper.insert(question);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新题目,并进行重复校验
|
||||
*/
|
||||
public void updateQuestion(Question question) {
|
||||
validateQuestion(question.getContent(), question.getId());
|
||||
log.info("更新题目ID: {}", question.getId());
|
||||
questionMapper.updateById(question);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除题目
|
||||
*/
|
||||
public void deleteQuestion(Long id) {
|
||||
log.info("删除题目ID: {}", id);
|
||||
questionMapper.deleteById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* AI批量导入题库,并进行去重
|
||||
*/
|
||||
public void importQuestionsFromFile(MultipartFile file) throws IOException {
|
||||
log.info("开始从文件导入题库: {}", file.getOriginalFilename());
|
||||
String fileExtension = getFileExtension(file.getOriginalFilename());
|
||||
DocumentParser parser = documentParserList.stream()
|
||||
.filter(p -> p.getSupportedType().equals(fileExtension))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("不支持的文件类型: " + fileExtension));
|
||||
|
||||
String content = parser.parse(file.getInputStream());
|
||||
List<Question> questionsFromAi = classificationService.classifyQuestions(content);
|
||||
|
||||
int newQuestionsCount = 0;
|
||||
for (Question question : questionsFromAi) {
|
||||
try {
|
||||
validateQuestion(question.getContent(), null);
|
||||
questionMapper.insert(question);
|
||||
newQuestionsCount++;
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("跳过重复题目: {}", question.getContent());
|
||||
}
|
||||
}
|
||||
log.info("成功导入 {} 个新题目,跳过 {} 个重复题目。", newQuestionsCount, questionsFromAi.size() - newQuestionsCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用AI检查题库中的数据是否重复
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void useAiCheckQuestionData() {
|
||||
// 查询数据库
|
||||
List<Question> questions = questionMapper.selectList(
|
||||
new LambdaQueryWrapper<Question>()
|
||||
.orderByDesc(Question::getCreatedTime)
|
||||
);
|
||||
// 组装prompt
|
||||
if (CollectionUtil.isEmpty(questions)) {
|
||||
return;
|
||||
}
|
||||
String prompt = getPrompt(questions);
|
||||
log.info("发送内容: {}", prompt);
|
||||
// 验证token上下文长度
|
||||
Integer promptTokens = llmService.getPromptTokens(prompt);
|
||||
log.info("当前prompt长度: {}", promptTokens);
|
||||
String chat = llmService.chat(prompt);
|
||||
// 调用AI
|
||||
log.info("AI返回内容: {}", chat);
|
||||
JSONObject parse = JSONObject.parse(chat);
|
||||
JSONArray questionsIds = parse.getJSONArray("questions");
|
||||
List<Long> list = questionsIds.toList(Long.class);
|
||||
questionMapper.delete(
|
||||
new LambdaQueryWrapper<Question>()
|
||||
.notIn(Question::getId, list)
|
||||
);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static String getPrompt(List<Question> questions) {
|
||||
JSONArray jsonArray = new JSONArray();
|
||||
for (Question question : questions) {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("id", question.getId());
|
||||
jsonObject.put("content", question.getContent());
|
||||
jsonArray.add(jsonObject);
|
||||
}
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("data", jsonArray);
|
||||
return String.format("""
|
||||
请对以下数据进行重复校验,如果题目内容相似,请只保留1条数据,并返回对应数据的id。请严格按照以下JSON格式返回结果:
|
||||
|
||||
{
|
||||
"questions": [1, 2, 3, .....]
|
||||
}
|
||||
|
||||
分类规则:
|
||||
1. 只返回JSON,不要其他解释文字
|
||||
2. 请严格按照API接口形式返回,不要返回任何额外的文字内容,包括'```json```'!!!!
|
||||
3. 请严格按照网络接口的形式返回JSON数据!!!
|
||||
数据如下:
|
||||
%s
|
||||
""", jsonObject.toJSONString());
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验题目内容是否重复
|
||||
*
|
||||
* @param content 题目内容
|
||||
* @param currentId 当前题目ID,更新时传入,用于排除自身
|
||||
*/
|
||||
private void validateQuestion(String content, Long currentId) {
|
||||
Question existingQuestion = questionMapper.selectByContent(content);
|
||||
if (existingQuestion != null && (currentId == null || !existingQuestion.getId().equals(currentId))) {
|
||||
throw new IllegalArgumentException("题目内容已存在,请勿重复添加。");
|
||||
}
|
||||
}
|
||||
|
||||
private String getFileExtension(String fileName) {
|
||||
if (fileName == null || fileName.lastIndexOf('.') == -1) {
|
||||
return "";
|
||||
}
|
||||
return fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.qingqiu.interview.service.impl;
|
||||
|
||||
import com.qingqiu.interview.entity.AiSessionLog;
|
||||
import com.qingqiu.interview.mapper.AiSessionLogMapper;
|
||||
import com.qingqiu.interview.service.IAiSessionLogService;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* ai会话记录 服务实现类
|
||||
* </p>
|
||||
*
|
||||
* @author huangpeng
|
||||
* @since 2025-08-30
|
||||
*/
|
||||
@Service
|
||||
public class AiSessionLogServiceImpl extends ServiceImpl<AiSessionLogMapper, AiSessionLog> implements IAiSessionLogService {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.qingqiu.interview.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.qingqiu.interview.dto.QuestionProgressPageParams;
|
||||
import com.qingqiu.interview.entity.InterviewQuestionProgress;
|
||||
import com.qingqiu.interview.mapper.InterviewQuestionProgressMapper;
|
||||
import com.qingqiu.interview.service.IInterviewQuestionProgressService;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* 面试问题进度跟踪表 服务实现类
|
||||
* </p>
|
||||
*
|
||||
* @author huangpeng
|
||||
* @since 2025-08-30
|
||||
*/
|
||||
@Service
|
||||
public class InterviewQuestionProgressServiceImpl extends ServiceImpl<InterviewQuestionProgressMapper, InterviewQuestionProgress> implements IInterviewQuestionProgressService {
|
||||
|
||||
@Override
|
||||
public Page<InterviewQuestionProgress> pageList(QuestionProgressPageParams params) {
|
||||
return page(
|
||||
Page.of(params.getCurrent(), params.getSize()),
|
||||
new LambdaQueryWrapper<InterviewQuestionProgress>()
|
||||
.like(StringUtils.isNotBlank(params.getQuestionName()), InterviewQuestionProgress::getQuestionContent, params.getQuestionName())
|
||||
.in(InterviewQuestionProgress::getStatus,
|
||||
Arrays.asList(
|
||||
InterviewQuestionProgress.Status.ACTIVE.name(),
|
||||
InterviewQuestionProgress.Status.COMPLETED.name()
|
||||
)
|
||||
)
|
||||
.orderByAsc(InterviewQuestionProgress::getStatus)
|
||||
.orderByDesc(InterviewQuestionProgress::getUpdatedTime)
|
||||
.orderByDesc(InterviewQuestionProgress::getCreatedTime)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.qingqiu.interview.service.llm;
|
||||
|
||||
public interface LlmService {
|
||||
|
||||
|
||||
/**
|
||||
* 与模型进行单轮对话
|
||||
* @param prompt 提示词
|
||||
* @return ai回复
|
||||
*/
|
||||
String chat(String prompt);
|
||||
|
||||
/**
|
||||
* 与模型进行多轮对话
|
||||
* @param prompt 提示词
|
||||
* @param token 会话token
|
||||
* @return ai回复
|
||||
*/
|
||||
String chat(String prompt, String token);
|
||||
|
||||
Integer getPromptTokens(String prompt);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
package com.qingqiu.interview.service.llm.qwen;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import com.alibaba.dashscope.aigc.generation.Generation;
|
||||
import com.alibaba.dashscope.common.Message;
|
||||
import com.alibaba.dashscope.common.Role;
|
||||
import com.alibaba.dashscope.tokenizers.Tokenizer;
|
||||
import com.alibaba.dashscope.tokenizers.TokenizerFactory;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.qingqiu.interview.ai.factory.AIClientManager;
|
||||
import com.qingqiu.interview.common.constants.AIStrategyConstant;
|
||||
import com.qingqiu.interview.entity.AiSessionLog;
|
||||
import com.qingqiu.interview.mapper.AiSessionLogMapper;
|
||||
import com.qingqiu.interview.service.llm.LlmService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.qingqiu.interview.common.utils.AIUtils.createMessage;
|
||||
|
||||
@Slf4j
|
||||
@Service("qwenService")
|
||||
@RequiredArgsConstructor
|
||||
public class QwenService implements LlmService {
|
||||
|
||||
private final Generation generation;
|
||||
|
||||
private final AiSessionLogMapper aiSessionLogMapper;
|
||||
|
||||
@Value("${dashscope.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
private final AIClientManager aiClientManager;
|
||||
|
||||
|
||||
@Override
|
||||
public String chat(String prompt) {
|
||||
// log.info("开始调用API....");
|
||||
// long l = System.currentTimeMillis();
|
||||
return aiClientManager.getClient(AIStrategyConstant.DEEPSEEK).chatCompletion(prompt);
|
||||
// GenerationParam param = GenerationParam.builder()
|
||||
// .model(DEEPSEEK_3) // 可根据需要更换模型
|
||||
// .messages(Collections.singletonList(createMessage(Role.USER.getValue(), prompt)))
|
||||
// .resultFormat(GenerationParam.ResultFormat.MESSAGE)
|
||||
// .apiKey(apiKey)
|
||||
// .build();
|
||||
//
|
||||
// GenerationResult result = null;
|
||||
// try {
|
||||
// result = generation.call(param);
|
||||
// log.info("调用成功,耗时: {} ms", System.currentTimeMillis() - l);
|
||||
// log.debug("响应结果: {}", result.getOutput().getChoices().get(0).getMessage().getContent());
|
||||
// return result.getOutput().getChoices().get(0).getMessage().getContent();
|
||||
// } catch (ApiException | InputRequiredException e) {
|
||||
// throw new RuntimeException("调用AI服务失败", e);
|
||||
// } catch (NoApiKeyException e) {
|
||||
// throw new RuntimeException("请检查API密钥是否正确", e);
|
||||
// }
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(String prompt, String token) {
|
||||
// 根据token查询会话记录
|
||||
List<AiSessionLog> aiSessionLogs = aiSessionLogMapper.selectList(
|
||||
new LambdaQueryWrapper<AiSessionLog>()
|
||||
.eq(AiSessionLog::getToken, token)
|
||||
.orderByDesc(AiSessionLog::getCreatedTime)
|
||||
);
|
||||
// 构造发给ai的消息
|
||||
List<Message> messages = new ArrayList<>();
|
||||
if (CollectionUtil.isNotEmpty(aiSessionLogs)) {
|
||||
// 预估tokens
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (AiSessionLog aiSessionLog : aiSessionLogs) {
|
||||
sb.append(aiSessionLog.getContent());
|
||||
}
|
||||
// 加上本次对话内容
|
||||
sb.append(prompt);
|
||||
Integer promptTokens = getPromptTokens(sb.toString());
|
||||
// 如果token大于了模型上限,则执行丢弃操作
|
||||
int size = aiSessionLogs.size();
|
||||
log.info("当前会话id: {}, tokens: {}", token, promptTokens);
|
||||
|
||||
// 假设模型上限为30000个token(根据实际模型调整)
|
||||
int maxTokens = 100000;
|
||||
if (promptTokens > maxTokens) {
|
||||
// 需要丢弃30%的会话记录(按时间倒序,丢弃最旧的)
|
||||
int discardCount = (int) (size * 0.3);
|
||||
// 从当前会话记录列表中移除旧的会话记录,而不是删除数据库中的记录
|
||||
for (int i = 0; i < discardCount; i++) {
|
||||
aiSessionLogs.remove(aiSessionLogs.size() - 1);
|
||||
}
|
||||
}
|
||||
// 移除旧记录后再按时间正序排序(最旧的在前面,最新的在后面)
|
||||
aiSessionLogs = aiSessionLogs.stream()
|
||||
.sorted((log1, log2) -> log1.getCreatedTime().compareTo(log2.getCreatedTime()))
|
||||
.collect(Collectors.toList());
|
||||
for (AiSessionLog aiSessionLog : aiSessionLogs) {
|
||||
messages.add(
|
||||
createMessage(aiSessionLog.getRole(), aiSessionLog.getContent())
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
messages.add(
|
||||
createMessage(Role.USER.getValue(), prompt)
|
||||
);
|
||||
|
||||
String aiResponse = aiClientManager.getClient(AIStrategyConstant.DEEPSEEK).chatCompletion(messages);
|
||||
// 存储用户提问
|
||||
AiSessionLog userLog = new AiSessionLog();
|
||||
userLog.setToken(token);
|
||||
userLog.setRole(Role.USER.getValue());
|
||||
userLog.setContent(prompt);
|
||||
aiSessionLogMapper.insert(userLog);
|
||||
|
||||
// 存储AI回复
|
||||
AiSessionLog aiLog = new AiSessionLog();
|
||||
aiLog.setToken(token);
|
||||
aiLog.setRole(Role.ASSISTANT.getValue());
|
||||
aiLog.setContent(aiResponse);
|
||||
aiSessionLogMapper.insert(aiLog);
|
||||
return aiResponse;
|
||||
|
||||
// // 调用AI模型
|
||||
// try {
|
||||
// log.info("开始调用API....");
|
||||
// long l = System.currentTimeMillis();
|
||||
// GenerationParam param = GenerationParam.builder()
|
||||
// .model(DEEPSEEK_3_1) // 可根据需要更换模型
|
||||
// .messages(messages)
|
||||
// .resultFormat(GenerationParam.ResultFormat.MESSAGE)
|
||||
// .apiKey(apiKey)
|
||||
// .build();
|
||||
//
|
||||
// GenerationResult result = generation.call(param);
|
||||
// log.info("调用成功,耗时: {} ms", System.currentTimeMillis() - l);
|
||||
// String aiResponse = result.getOutput().getChoices().get(0).getMessage().getContent();
|
||||
// log.debug("响应结果: {}", aiResponse);
|
||||
// // 存储用户提问
|
||||
// AiSessionLog userLog = new AiSessionLog();
|
||||
// userLog.setToken(token);
|
||||
// userLog.setRole(Role.USER.getValue());
|
||||
// userLog.setContent(prompt);
|
||||
// aiSessionLogMapper.insert(userLog);
|
||||
//
|
||||
// // 存储AI回复
|
||||
// AiSessionLog aiLog = new AiSessionLog();
|
||||
// aiLog.setToken(token);
|
||||
// aiLog.setRole(Role.ASSISTANT.getValue());
|
||||
// aiLog.setContent(aiResponse);
|
||||
// aiSessionLogMapper.insert(aiLog);
|
||||
//
|
||||
// return aiResponse;
|
||||
// } catch (ApiException | NoApiKeyException | InputRequiredException e) {
|
||||
// throw new RuntimeException("调用AI服务失败", e);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取prompt的token数
|
||||
*
|
||||
* @param prompt 输入
|
||||
* @return tokens
|
||||
*/
|
||||
@Override
|
||||
public Integer getPromptTokens(String prompt) {
|
||||
Tokenizer tokenizer = TokenizerFactory.qwen();
|
||||
List<Integer> integers = tokenizer.encodeOrdinary(prompt);
|
||||
return integers.size();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.qingqiu.interview.service.parser;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
public interface DocumentParser {
|
||||
/**
|
||||
* 解析文档内容
|
||||
* @param inputStream 文档输入流
|
||||
* @return 文档的文本内容
|
||||
*/
|
||||
String parse(InputStream inputStream);
|
||||
|
||||
/**
|
||||
* 获取支持的文件类型
|
||||
* @return "pdf", "md", etc.
|
||||
*/
|
||||
String getSupportedType();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.qingqiu.interview.service.parser;
|
||||
|
||||
import org.commonmark.node.Node;
|
||||
import org.commonmark.parser.Parser;
|
||||
import org.commonmark.renderer.text.TextContentRenderer;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
@Service("mdParser")
|
||||
public class MarkdownParserService implements DocumentParser {
|
||||
|
||||
private final Parser parser = Parser.builder().build();
|
||||
private final TextContentRenderer renderer = TextContentRenderer.builder().build();
|
||||
|
||||
@Override
|
||||
public String parse(InputStream inputStream) {
|
||||
try (InputStreamReader reader = new InputStreamReader(inputStream)) {
|
||||
Node document = parser.parseReader(reader);
|
||||
return renderer.render(document);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to parse Markdown document", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSupportedType() {
|
||||
return "md";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.qingqiu.interview.service.parser;
|
||||
|
||||
|
||||
import org.apache.pdfbox.Loader;
|
||||
import org.apache.pdfbox.cos.COSDocument;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.pdmodel.fdf.FDFDocument;
|
||||
import org.apache.pdfbox.text.PDFTextStripper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service("pdfParser")
|
||||
public class PdfParserService implements DocumentParser {
|
||||
|
||||
/**
|
||||
* 解析 PDF 文档内容
|
||||
* @param inputStream PDF 文件输入流
|
||||
* @return 提取的文本内容
|
||||
*/
|
||||
@Override
|
||||
public String parse(InputStream inputStream) {
|
||||
// 检查输入流是否为 null,避免空指针异常
|
||||
Objects.requireNonNull(inputStream, "PDF文件输入流不能为空");
|
||||
|
||||
// 使用 try-with-resources 确保 PDDocument 资源自动关闭
|
||||
|
||||
try (PDDocument document = Loader.loadPDF(inputStream.readAllBytes())) {
|
||||
|
||||
// 创建 PDF 文本提取器
|
||||
PDFTextStripper pdfStripper = new PDFTextStripper();
|
||||
|
||||
// 配置提取参数
|
||||
pdfStripper.setSortByPosition(true); // 按位置排序,保持原始布局
|
||||
pdfStripper.setAddMoreFormatting(true); // 保留更多格式信息
|
||||
pdfStripper.setSuppressDuplicateOverlappingText(true); // 去除重复文本
|
||||
|
||||
// 执行文本提取并返回结果
|
||||
return pdfStripper.getText(document);
|
||||
|
||||
} catch (Exception e) {
|
||||
// 处理其他未知异常
|
||||
throw new RuntimeException("解析PDF时发生未知错误", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取该解析器支持的文档类型
|
||||
* @return 支持的文档类型标识(此处为"pdf")
|
||||
*/
|
||||
@Override
|
||||
public String getSupportedType() {
|
||||
return "pdf"; // 返回支持的文档类型
|
||||
}
|
||||
}
|
||||
|
||||
27
src/main/resources/application.yml
Normal file
27
src/main/resources/application.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
dashscope:
|
||||
api-key: sk-58d6fed688c54e8db02e6a7ffbfc7a5f
|
||||
deepseek:
|
||||
api-url: https://api.deepseek.com/chat/completions
|
||||
api-key: sk-faaa2a1b485442ccbf115ff1271a3480
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://gz-cynosdbmysql-grp-5ai5zw7r.sql.tencentcdb.com:24944/ai_interview?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
|
||||
username: qingqiu
|
||||
password: 020979hP
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
# ai:
|
||||
# openai:
|
||||
# api-key: sk-faaa2a1b485442ccbf115ff1271a3480
|
||||
# base-url: https://api.deepseek.com
|
||||
# chat:
|
||||
# options:
|
||||
# model: deepseek-chat
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: deleted # 全局逻辑删除字段名
|
||||
logic-delete-value: 1 # 逻辑已删除值。可选,默认值为 1
|
||||
logic-not-delete-value: 0 # 逻辑未删除值。可选,默认值为 0
|
||||
30
src/main/resources/mapper/AIClientService.xml
Normal file
30
src/main/resources/mapper/AIClientService.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingqiu.interview.mapper.InterviewSessionMapper">
|
||||
|
||||
<select id="selectBySessionId" resultType="com.qingqiu.interview.entity.InterviewSession">
|
||||
SELECT * FROM interview_session
|
||||
WHERE session_id = #{sessionId} AND deleted = 0
|
||||
</select>
|
||||
|
||||
<select id="selectActiveSessionsByModel" resultType="com.qingqiu.interview.entity.InterviewSession">
|
||||
SELECT * FROM interview_session
|
||||
WHERE ai_model = #{aiModel} AND status = 'ACTIVE' AND deleted = 0
|
||||
ORDER BY created_time DESC
|
||||
</select>
|
||||
|
||||
<update id="updateSessionStatus">
|
||||
UPDATE interview_session
|
||||
SET status = #{status}, updated_time = NOW()
|
||||
WHERE session_id = #{sessionId}
|
||||
</update>
|
||||
|
||||
<select id="countRecentInterviews" resultType="com.qingqiu.interview.dto.DashboardStatsResponse$DailyStat">
|
||||
SELECT DATE(created_time) as date, COUNT(*) as count
|
||||
FROM interview_session
|
||||
WHERE created_time >= DATE_SUB(NOW(), INTERVAL #{days} DAY)
|
||||
GROUP BY DATE(created_time)
|
||||
ORDER BY date ASC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
5
src/main/resources/mapper/AiSessionLogMapper.xml
Normal file
5
src/main/resources/mapper/AiSessionLogMapper.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingqiu.interview.mapper.AiSessionLogMapper">
|
||||
|
||||
</mapper>
|
||||
17
src/main/resources/mapper/InterviewEvaluationMapper.xml
Normal file
17
src/main/resources/mapper/InterviewEvaluationMapper.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingqiu.interview.mapper.InterviewEvaluationMapper">
|
||||
|
||||
<select id="selectBySessionId" resultType="com.qingqiu.interview.entity.InterviewEvaluation">
|
||||
SELECT * FROM interview_evaluation
|
||||
WHERE session_id = #{sessionId}
|
||||
ORDER BY created_time ASC
|
||||
</select>
|
||||
|
||||
<select id="selectBySessionIdAndQuestionId" resultType="com.qingqiu.interview.entity.InterviewEvaluation">
|
||||
SELECT * FROM interview_evaluation
|
||||
WHERE session_id = #{sessionId} AND question_id = #{questionId}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
23
src/main/resources/mapper/InterviewMessageMapper.xml
Normal file
23
src/main/resources/mapper/InterviewMessageMapper.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingqiu.interview.mapper.InterviewMessageMapper">
|
||||
|
||||
<select id="selectBySessionIdOrderByOrder" resultType="com.qingqiu.interview.entity.InterviewMessage">
|
||||
SELECT * FROM interview_message
|
||||
WHERE session_id = #{sessionId}
|
||||
ORDER BY message_order ASC
|
||||
</select>
|
||||
|
||||
<select id="selectLatestBySessionId" resultType="com.qingqiu.interview.entity.InterviewMessage">
|
||||
SELECT * FROM interview_message
|
||||
WHERE session_id = #{sessionId}
|
||||
ORDER BY message_order DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectMaxOrderBySessionId" resultType="int">
|
||||
SELECT COALESCE(MAX(message_order), 0) FROM interview_message
|
||||
WHERE session_id = #{sessionId}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingqiu.interview.mapper.InterviewQuestionProgressMapper">
|
||||
|
||||
</mapper>
|
||||
50
src/main/resources/mapper/QuestionMapper.xml
Normal file
50
src/main/resources/mapper/QuestionMapper.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingqiu.interview.mapper.QuestionMapper">
|
||||
|
||||
<select id="selectByCategory" resultType="com.qingqiu.interview.entity.Question">
|
||||
SELECT *
|
||||
FROM question
|
||||
WHERE category = #{category} AND deleted = 0
|
||||
ORDER BY created_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectByCategories" resultType="com.qingqiu.interview.entity.Question">
|
||||
SELECT *
|
||||
FROM question
|
||||
WHERE category IN
|
||||
<foreach collection="categories" item="category" open="(" separator="," close=")">
|
||||
#{category}
|
||||
</foreach>
|
||||
AND deleted = 0
|
||||
ORDER BY created_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectRandomByCategories" resultType="com.qingqiu.interview.entity.Question">
|
||||
SELECT *
|
||||
FROM question
|
||||
WHERE category IN
|
||||
<foreach collection="categories" item="category" open="(" separator="," close=")">
|
||||
#{category}
|
||||
</foreach>
|
||||
AND deleted = 0
|
||||
ORDER BY RAND()
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<select id="selectByContent" resultType="com.qingqiu.interview.entity.Question">
|
||||
SELECT *
|
||||
FROM question
|
||||
WHERE content = #{content} AND deleted = 0
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="countByCategory" resultType="com.qingqiu.interview.dto.DashboardStatsResponse$CategoryStat">
|
||||
SELECT category as name, COUNT(*) as value
|
||||
FROM question
|
||||
WHERE deleted = 0
|
||||
GROUP BY category
|
||||
ORDER BY value DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
57
src/main/resources/sql/schema.sql
Normal file
57
src/main/resources/sql/schema.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
-- 题库表
|
||||
CREATE TABLE IF NOT EXISTS question (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
content TEXT NOT NULL COMMENT '题目内容',
|
||||
category VARCHAR(100) NOT NULL COMMENT '题目分类',
|
||||
difficulty VARCHAR(20) NOT NULL COMMENT '难度等级',
|
||||
tags VARCHAR(500) COMMENT '标签,逗号分隔',
|
||||
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除标记'
|
||||
);
|
||||
|
||||
-- 面试会话表
|
||||
CREATE TABLE IF NOT EXISTS interview_session (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
session_id VARCHAR(64) UNIQUE NOT NULL COMMENT '会话唯一标识',
|
||||
candidate_name VARCHAR(100) COMMENT '候选人姓名',
|
||||
resume_content TEXT COMMENT '简历内容',
|
||||
extracted_skills TEXT COMMENT '提取的技能,JSON格式',
|
||||
ai_model VARCHAR(50) NOT NULL COMMENT '使用的AI模型',
|
||||
status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '会话状态:ACTIVE, COMPLETED, TERMINATED',
|
||||
total_questions INT DEFAULT 0 COMMENT '总问题数',
|
||||
current_question_index INT DEFAULT 0 COMMENT '当前问题索引',
|
||||
score DECIMAL(5,2) COMMENT '面试评分',
|
||||
selected_question_ids TEXT COMMENT 'AI选择的题目ID列表,JSON格式',
|
||||
final_report TEXT COMMENT 'AI生成的最终面试报告,JSON格式',
|
||||
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted TINYINT DEFAULT 0
|
||||
);
|
||||
|
||||
-- 面试消息记录表
|
||||
CREATE TABLE IF NOT EXISTS interview_message (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
session_id VARCHAR(64) NOT NULL COMMENT '会话ID',
|
||||
message_type VARCHAR(20) NOT NULL COMMENT '消息类型:QUESTION, ANSWER, SYSTEM',
|
||||
sender VARCHAR(20) NOT NULL COMMENT '发送者:AI, USER, SYSTEM',
|
||||
content TEXT NOT NULL COMMENT '消息内容',
|
||||
question_id BIGINT COMMENT '关联的题目ID',
|
||||
message_order INT NOT NULL COMMENT '消息顺序',
|
||||
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_session_id (session_id),
|
||||
INDEX idx_session_order (session_id, message_order)
|
||||
);
|
||||
|
||||
-- 面试评估表
|
||||
CREATE TABLE IF NOT EXISTS interview_evaluation (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
session_id VARCHAR(64) NOT NULL COMMENT '会话ID',
|
||||
question_id BIGINT COMMENT '题目ID',
|
||||
user_answer TEXT COMMENT '用户回答',
|
||||
ai_feedback TEXT COMMENT 'AI反馈',
|
||||
score DECIMAL(3,1) COMMENT '单题得分',
|
||||
evaluation_criteria TEXT COMMENT '评估标准,JSON格式',
|
||||
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_session_id (session_id)
|
||||
);
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.qingqiu.interview;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class AiInterviewApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
27
target/classes/application.yml
Normal file
27
target/classes/application.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
dashscope:
|
||||
api-key: sk-58d6fed688c54e8db02e6a7ffbfc7a5f
|
||||
deepseek:
|
||||
api-url: https://api.deepseek.com/chat/completions
|
||||
api-key: sk-faaa2a1b485442ccbf115ff1271a3480
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://gz-cynosdbmysql-grp-5ai5zw7r.sql.tencentcdb.com:24944/ai_interview?createDatabaseIfNotExist=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8
|
||||
username: qingqiu
|
||||
password: 020979hP
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
# ai:
|
||||
# openai:
|
||||
# api-key: sk-faaa2a1b485442ccbf115ff1271a3480
|
||||
# base-url: https://api.deepseek.com
|
||||
# chat:
|
||||
# options:
|
||||
# model: deepseek-chat
|
||||
mybatis-plus:
|
||||
configuration:
|
||||
map-underscore-to-camel-case: true
|
||||
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
|
||||
global-config:
|
||||
db-config:
|
||||
logic-delete-field: deleted # 全局逻辑删除字段名
|
||||
logic-delete-value: 1 # 逻辑已删除值。可选,默认值为 1
|
||||
logic-not-delete-value: 0 # 逻辑未删除值。可选,默认值为 0
|
||||
30
target/classes/mapper/AIClientService.xml
Normal file
30
target/classes/mapper/AIClientService.xml
Normal file
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingqiu.interview.mapper.InterviewSessionMapper">
|
||||
|
||||
<select id="selectBySessionId" resultType="com.qingqiu.interview.entity.InterviewSession">
|
||||
SELECT * FROM interview_session
|
||||
WHERE session_id = #{sessionId} AND deleted = 0
|
||||
</select>
|
||||
|
||||
<select id="selectActiveSessionsByModel" resultType="com.qingqiu.interview.entity.InterviewSession">
|
||||
SELECT * FROM interview_session
|
||||
WHERE ai_model = #{aiModel} AND status = 'ACTIVE' AND deleted = 0
|
||||
ORDER BY created_time DESC
|
||||
</select>
|
||||
|
||||
<update id="updateSessionStatus">
|
||||
UPDATE interview_session
|
||||
SET status = #{status}, updated_time = NOW()
|
||||
WHERE session_id = #{sessionId}
|
||||
</update>
|
||||
|
||||
<select id="countRecentInterviews" resultType="com.qingqiu.interview.dto.DashboardStatsResponse$DailyStat">
|
||||
SELECT DATE(created_time) as date, COUNT(*) as count
|
||||
FROM interview_session
|
||||
WHERE created_time >= DATE_SUB(NOW(), INTERVAL #{days} DAY)
|
||||
GROUP BY DATE(created_time)
|
||||
ORDER BY date ASC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
5
target/classes/mapper/AiSessionLogMapper.xml
Normal file
5
target/classes/mapper/AiSessionLogMapper.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingqiu.interview.mapper.AiSessionLogMapper">
|
||||
|
||||
</mapper>
|
||||
17
target/classes/mapper/InterviewEvaluationMapper.xml
Normal file
17
target/classes/mapper/InterviewEvaluationMapper.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingqiu.interview.mapper.InterviewEvaluationMapper">
|
||||
|
||||
<select id="selectBySessionId" resultType="com.qingqiu.interview.entity.InterviewEvaluation">
|
||||
SELECT * FROM interview_evaluation
|
||||
WHERE session_id = #{sessionId}
|
||||
ORDER BY created_time ASC
|
||||
</select>
|
||||
|
||||
<select id="selectBySessionIdAndQuestionId" resultType="com.qingqiu.interview.entity.InterviewEvaluation">
|
||||
SELECT * FROM interview_evaluation
|
||||
WHERE session_id = #{sessionId} AND question_id = #{questionId}
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
23
target/classes/mapper/InterviewMessageMapper.xml
Normal file
23
target/classes/mapper/InterviewMessageMapper.xml
Normal file
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingqiu.interview.mapper.InterviewMessageMapper">
|
||||
|
||||
<select id="selectBySessionIdOrderByOrder" resultType="com.qingqiu.interview.entity.InterviewMessage">
|
||||
SELECT * FROM interview_message
|
||||
WHERE session_id = #{sessionId}
|
||||
ORDER BY message_order ASC
|
||||
</select>
|
||||
|
||||
<select id="selectLatestBySessionId" resultType="com.qingqiu.interview.entity.InterviewMessage">
|
||||
SELECT * FROM interview_message
|
||||
WHERE session_id = #{sessionId}
|
||||
ORDER BY message_order DESC
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="selectMaxOrderBySessionId" resultType="int">
|
||||
SELECT COALESCE(MAX(message_order), 0) FROM interview_message
|
||||
WHERE session_id = #{sessionId}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingqiu.interview.mapper.InterviewQuestionProgressMapper">
|
||||
|
||||
</mapper>
|
||||
50
target/classes/mapper/QuestionMapper.xml
Normal file
50
target/classes/mapper/QuestionMapper.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.qingqiu.interview.mapper.QuestionMapper">
|
||||
|
||||
<select id="selectByCategory" resultType="com.qingqiu.interview.entity.Question">
|
||||
SELECT *
|
||||
FROM question
|
||||
WHERE category = #{category} AND deleted = 0
|
||||
ORDER BY created_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectByCategories" resultType="com.qingqiu.interview.entity.Question">
|
||||
SELECT *
|
||||
FROM question
|
||||
WHERE category IN
|
||||
<foreach collection="categories" item="category" open="(" separator="," close=")">
|
||||
#{category}
|
||||
</foreach>
|
||||
AND deleted = 0
|
||||
ORDER BY created_time DESC
|
||||
</select>
|
||||
|
||||
<select id="selectRandomByCategories" resultType="com.qingqiu.interview.entity.Question">
|
||||
SELECT *
|
||||
FROM question
|
||||
WHERE category IN
|
||||
<foreach collection="categories" item="category" open="(" separator="," close=")">
|
||||
#{category}
|
||||
</foreach>
|
||||
AND deleted = 0
|
||||
ORDER BY RAND()
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<select id="selectByContent" resultType="com.qingqiu.interview.entity.Question">
|
||||
SELECT *
|
||||
FROM question
|
||||
WHERE content = #{content} AND deleted = 0
|
||||
LIMIT 1
|
||||
</select>
|
||||
|
||||
<select id="countByCategory" resultType="com.qingqiu.interview.dto.DashboardStatsResponse$CategoryStat">
|
||||
SELECT category as name, COUNT(*) as value
|
||||
FROM question
|
||||
WHERE deleted = 0
|
||||
GROUP BY category
|
||||
ORDER BY value DESC
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
57
target/classes/sql/schema.sql
Normal file
57
target/classes/sql/schema.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
-- 题库表
|
||||
CREATE TABLE IF NOT EXISTS question (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
content TEXT NOT NULL COMMENT '题目内容',
|
||||
category VARCHAR(100) NOT NULL COMMENT '题目分类',
|
||||
difficulty VARCHAR(20) NOT NULL COMMENT '难度等级',
|
||||
tags VARCHAR(500) COMMENT '标签,逗号分隔',
|
||||
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted TINYINT DEFAULT 0 COMMENT '逻辑删除标记'
|
||||
);
|
||||
|
||||
-- 面试会话表
|
||||
CREATE TABLE IF NOT EXISTS interview_session (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
session_id VARCHAR(64) UNIQUE NOT NULL COMMENT '会话唯一标识',
|
||||
candidate_name VARCHAR(100) COMMENT '候选人姓名',
|
||||
resume_content TEXT COMMENT '简历内容',
|
||||
extracted_skills TEXT COMMENT '提取的技能,JSON格式',
|
||||
ai_model VARCHAR(50) NOT NULL COMMENT '使用的AI模型',
|
||||
status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '会话状态:ACTIVE, COMPLETED, TERMINATED',
|
||||
total_questions INT DEFAULT 0 COMMENT '总问题数',
|
||||
current_question_index INT DEFAULT 0 COMMENT '当前问题索引',
|
||||
score DECIMAL(5,2) COMMENT '面试评分',
|
||||
selected_question_ids TEXT COMMENT 'AI选择的题目ID列表,JSON格式',
|
||||
final_report TEXT COMMENT 'AI生成的最终面试报告,JSON格式',
|
||||
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
deleted TINYINT DEFAULT 0
|
||||
);
|
||||
|
||||
-- 面试消息记录表
|
||||
CREATE TABLE IF NOT EXISTS interview_message (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
session_id VARCHAR(64) NOT NULL COMMENT '会话ID',
|
||||
message_type VARCHAR(20) NOT NULL COMMENT '消息类型:QUESTION, ANSWER, SYSTEM',
|
||||
sender VARCHAR(20) NOT NULL COMMENT '发送者:AI, USER, SYSTEM',
|
||||
content TEXT NOT NULL COMMENT '消息内容',
|
||||
question_id BIGINT COMMENT '关联的题目ID',
|
||||
message_order INT NOT NULL COMMENT '消息顺序',
|
||||
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_session_id (session_id),
|
||||
INDEX idx_session_order (session_id, message_order)
|
||||
);
|
||||
|
||||
-- 面试评估表
|
||||
CREATE TABLE IF NOT EXISTS interview_evaluation (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
session_id VARCHAR(64) NOT NULL COMMENT '会话ID',
|
||||
question_id BIGINT COMMENT '题目ID',
|
||||
user_answer TEXT COMMENT '用户回答',
|
||||
ai_feedback TEXT COMMENT 'AI反馈',
|
||||
score DECIMAL(3,1) COMMENT '单题得分',
|
||||
evaluation_criteria TEXT COMMENT '评估标准,JSON格式',
|
||||
created_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_session_id (session_id)
|
||||
);
|
||||
3
target/maven-archiver/pom.properties
Normal file
3
target/maven-archiver/pom.properties
Normal file
@@ -0,0 +1,3 @@
|
||||
artifactId=AI-Interview
|
||||
groupId=com.qingqiu
|
||||
version=0.0.1-SNAPSHOT
|
||||
@@ -0,0 +1,55 @@
|
||||
com/qingqiu/interview/controller/InterviewQuestionProgressController.class
|
||||
com/qingqiu/interview/dto/ChatRequest.class
|
||||
com/qingqiu/interview/service/QuestionClassificationService.class
|
||||
com/qingqiu/interview/db/entity/InterviewSession.class
|
||||
com/qingqiu/interview/dto/DashboardStatsResponse$CategoryStat.class
|
||||
com/qingqiu/interview/db/mapper/InterviewEvaluationMapper.class
|
||||
com/qingqiu/interview/dto/ApiResponse.class
|
||||
com/qingqiu/interview/controller/QuestionController.class
|
||||
com/qingqiu/interview/db/entity/InterviewMessage.class
|
||||
com/qingqiu/interview/controller/InterviewController.class
|
||||
com/qingqiu/interview/controller/AiSessionLogController.class
|
||||
com/qingqiu/interview/db/entity/AiSessionLog.class
|
||||
com/qingqiu/interview/dto/DashboardStatsResponse$CategoryScoreStat.class
|
||||
com/qingqiu/interview/dto/InterviewReportResponse$QuestionDetail.class
|
||||
com/qingqiu/interview/db/mapper/InterviewMessageMapper.class
|
||||
com/qingqiu/interview/config/JacksonConfig.class
|
||||
com/qingqiu/interview/db/entity/InterviewQuestionProgress.class
|
||||
com/qingqiu/interview/service/llm/LlmService.class
|
||||
com/qingqiu/interview/service/InterviewService$1.class
|
||||
com/qingqiu/interview/service/IInterviewQuestionProgressService.class
|
||||
com/qingqiu/interview/db/entity/InterviewMessage$Sender.class
|
||||
com/qingqiu/interview/config/MyBatisPlusConfig.class
|
||||
com/qingqiu/interview/db/mapper/InterviewSessionMapper.class
|
||||
com/qingqiu/interview/service/parser/MarkdownParserService.class
|
||||
com/qingqiu/interview/db/mapper/QuestionMapper.class
|
||||
com/qingqiu/interview/service/impl/InterviewQuestionProgressServiceImpl.class
|
||||
com/qingqiu/interview/db/entity/Question.class
|
||||
com/qingqiu/interview/dto/InterviewStartRequest.class
|
||||
com/qingqiu/interview/service/parser/PdfParserService.class
|
||||
com/qingqiu/interview/service/parser/DocumentParser.class
|
||||
com/qingqiu/interview/dto/DashboardStatsResponse$WeakestQuestionStat.class
|
||||
com/qingqiu/interview/service/impl/AiSessionLogServiceImpl.class
|
||||
com/qingqiu/interview/db/entity/InterviewQuestionProgress$Status.class
|
||||
com/qingqiu/interview/service/QuestionService.class
|
||||
com/qingqiu/interview/constants/QwenModelConstant.class
|
||||
com/qingqiu/interview/controller/DashboardController.class
|
||||
com/qingqiu/interview/dto/DashboardStatsResponse$DailyStat.class
|
||||
com/qingqiu/interview/service/InterviewService.class
|
||||
com/qingqiu/interview/exception/GlobalExceptionHandler.class
|
||||
com/qingqiu/interview/dto/SessionRequest.class
|
||||
com/qingqiu/interview/service/DashboardService.class
|
||||
com/qingqiu/interview/db/entity/InterviewSession$Status.class
|
||||
com/qingqiu/interview/service/IAiSessionLogService.class
|
||||
com/qingqiu/interview/config/DashScopeConfig.class
|
||||
com/qingqiu/interview/service/llm/qwen/QwenService.class
|
||||
com/qingqiu/interview/dto/SessionHistoryResponse$MessageDto.class
|
||||
com/qingqiu/interview/dto/InterviewResponse.class
|
||||
com/qingqiu/interview/db/entity/InterviewMessage$MessageType.class
|
||||
com/qingqiu/interview/db/mapper/InterviewQuestionProgressMapper.class
|
||||
com/qingqiu/interview/db/mapper/AiSessionLogMapper.class
|
||||
com/qingqiu/interview/db/entity/InterviewEvaluation.class
|
||||
com/qingqiu/interview/AiInterviewApplication.class
|
||||
com/qingqiu/interview/dto/InterviewReportResponse.class
|
||||
com/qingqiu/interview/dto/DashboardStatsResponse.class
|
||||
com/qingqiu/interview/dto/SessionHistoryResponse.class
|
||||
@@ -0,0 +1,44 @@
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/AiInterviewApplication.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/config/DashScopeConfig.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/config/JacksonConfig.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/config/MyBatisPlusConfig.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/constants/QwenModelConstant.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/controller/AiSessionLogController.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/controller/DashboardController.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/controller/InterviewController.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/controller/InterviewQuestionProgressController.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/controller/QuestionController.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/db/entity/AiSessionLog.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/db/entity/InterviewEvaluation.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/db/entity/InterviewMessage.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/db/entity/InterviewQuestionProgress.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/db/entity/InterviewSession.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/db/entity/Question.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/db/mapper/AiSessionLogMapper.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/db/mapper/InterviewEvaluationMapper.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/db/mapper/InterviewMessageMapper.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/db/mapper/InterviewQuestionProgressMapper.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/db/mapper/InterviewSessionMapper.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/db/mapper/QuestionMapper.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/dto/ApiResponse.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/dto/ChatRequest.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/dto/DashboardStatsResponse.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/dto/InterviewReportResponse.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/dto/InterviewResponse.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/dto/InterviewStartRequest.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/dto/SessionHistoryResponse.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/dto/SessionRequest.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/exception/GlobalExceptionHandler.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/service/DashboardService.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/service/IAiSessionLogService.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/service/IInterviewQuestionProgressService.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/service/InterviewService.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/service/QuestionClassificationService.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/service/QuestionService.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/service/impl/AiSessionLogServiceImpl.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/service/impl/InterviewQuestionProgressServiceImpl.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/service/llm/LlmService.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/service/llm/qwen/QwenService.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/service/parser/DocumentParser.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/service/parser/MarkdownParserService.java
|
||||
/data/study/real-end/Java/AI-Interview/src/main/java/com/qingqiu/interview/service/parser/PdfParserService.java
|
||||
@@ -0,0 +1 @@
|
||||
com/qingqiu/interview/AiInterviewApplicationTests.class
|
||||
@@ -0,0 +1 @@
|
||||
/data/study/real-end/Java/AI-Interview/src/test/java/com/qingqiu/interview/AiInterviewApplicationTests.java
|
||||
Reference in New Issue
Block a user