12 Commits

Author SHA1 Message Date
b08a3988f7 添加需求文档 2026-01-22 19:30:42 +08:00
80dcb23bbc 使用spring-ai-alibaba重构项目 2025-11-24 13:37:37 +08:00
5fb4ed754c 使用spring-ai-alibaba重构项目 2025-10-22 19:53:26 +08:00
fac1346104 使用spring-ai-alibaba重构项目 2025-10-10 18:03:16 +08:00
5711d611f2 修改打包配置 2025-09-26 12:42:04 +08:00
d3b5ca0033 优化代码 2025-09-21 21:19:44 +08:00
df5aa0b9c6 优化代码 2025-09-20 22:24:07 +08:00
08b4b8b206 优化代码 2025-09-20 21:43:36 +08:00
8b357fbb93 优化代码 2025-09-19 15:19:41 +08:00
a384bbfd16 修改AI面试相关内容 2025-09-17 21:36:09 +08:00
7f24d65d76 添加分类的controller 2025-09-14 22:21:03 +08:00
d14b46d007 修改代码 2025-09-11 22:33:53 +08:00
134 changed files with 5111 additions and 1892 deletions

0
.gitignore vendored Normal file → Executable file
View File

27
HELP.md
View File

@@ -1,27 +0,0 @@
# 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.

0
LICENSE Normal file → Executable file
View File

0
README.md Normal file → Executable file
View File

View File

@@ -1,135 +0,0 @@
<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>

332
docs/B端需求文档.md Normal file
View File

@@ -0,0 +1,332 @@
------
# 📘《AI 智能模拟面试系统》B 端管理后台PRD —— 完整版
------
# 0. 文档信息
| 项目 | 内容 |
| ------------ | ----------------------------------- |
| 产品名称 | AI 智能模拟面试系统B 端管理后台) |
| 文档版本 | v1.0 |
| 平台 | PC Web |
| 用户角色 | 超级管理员 / 内容运营 / 数据分析师 |
| 作者 | ChatGPT |
| 最后更新时间 | 2025.11 |
------
# 1. 产品定位
B 端用于支持 C 端内容与运维,包括:
- 题库管理
- 岗位管理
- 用户管理
- AI 模型管理
- 面试记录查看
- 内容配置banner、推荐
- 系统设置
- 数据分析与看板
------
# 2. B 端用户角色与权限
| 角色 | 权限 |
| ----------- | -------------------- |
| Super Admin | 全权限,含系统配置 |
| 内容管理员 | 岗位、题库管理 |
| 数据分析员 | 查看数据与记录 |
| 运营 | Banner、推荐内容配置 |
采用 **RBAC基于角色的权限控制**,权限点在后台可配置。
------
# 3. 功能需求(按模块)
------
# **3.1 管理员系统(权限体系)**
## 3.1.1 管理员账号管理
功能点:
- 新增管理员
- 编辑管理员
- 禁用账号
- 重置密码
- 设置角色
字段:
- 用户名
- 邮箱
- 角色
- 状态
- 创建时间
------
## 3.1.2 角色管理
- 创建角色
- 编辑角色
- 配置权限点checkbox
- 删除角色
------
## 3.1.3 权限点管理
权限点模块化:
| 模块 | 示例权限点 |
| -------- | ------------------ |
| 岗位管理 | 新增、编辑、删除 |
| 题库管理 | 新增题目、禁用题目 |
| 用户管理 | 查看用户、禁用用户 |
| 面试管理 | 查看所有面试记录 |
| 系统设置 | 配置 OSS、模型参数 |
------
# **3.2 岗位管理模块**
## 3.2.1 岗位列表
展示字段:
- 岗位名
- 技能标签
- 难度
- 使用次数
- 状态
## 3.2.2 岗位维护
可以:
- 新增岗位
- 编辑岗位
- 删除 / 禁用岗位
- 配置技能标签(多个)
- 配置难度15
------
# **3.3 题库管理模块**
## 3.3.1 题目列表
展示:
- 题目内容(前 30 字)
- 所属岗位
- 难度
- 类型(开放题、多选题等)
- 使用次数
- 创建人
- 状态(有效/无效)
## 3.3.2 新增题目
字段:
- 问题内容(文本)
- 示例答案
- 难度
- 标签(可多选)
- 所属岗位
- question_typeopen / case / multiple
支持「AI 自动生成题目」。
## 3.3.3 批量导入
Excel 模板字段:
- 问题内容
- 答案
- 岗位
- 标签
- 难度
------
# **3.4 面试记录管理模块**
## 3.4.1 面试记录列表
字段:
- 面试 ID
- 用户
- 岗位
- 得分
- 面试模式
- 创建时间
支持筛选:
- 按日期
- 按岗位
- 按用户
- 分数段
## 3.4.2 面试详情
可查看:
- 完整问答(题目、用户回答)
- 音频回放
- AI 评分
- AI 点评
- 报告内容
------
# **3.5 用户管理模块**
## 3.5.1 用户列表
字段:
- 用户 ID
- 手机号/微信身份
- 注册时间
- 最近活跃时间
- 状态(正常/封禁)
## 3.5.2 用户详情
包含:
- 基本信息
- 简历解析内容
- 面试记录
- 行为数据(使用次数、平均分)
可执行操作:
- 封禁用户
- 清空数据(仅开发可见开关)
------
# **3.6 内容配置模块**
## 3.6.1 Banner 配置
- 上传图片
- 配置跳转链接
- 配置排序
- 设置上线/下线
## 3.6.2 推荐内容
- 推荐岗位排列
- 推荐题目列表
------
# **3.7 系统设置模块**
## 3.7.1 小程序配置
- AppID
- Secret
## 3.7.2 OSS/存储配置
- AccessKey
- Secret
- Bucket 名称
## 3.7.3 鉴权与 Token
- Token 有效期配置
- 登录限制
## 3.7.4 模型配置(关键)
- 当前使用模型(例如 GPT、Qwen
- 模型版本号
- 评分模型权重(逻辑 / 表达 / 深度 / 专业)
- 灰度开关
------
# **3.8 数据分析模块Dashboard**
## 3.8.1 总览数据
- 今日新增用户
- 今日面试次数
- 总注册数
- 总面试数
## 3.8.2 趋势图
- 最近 30 天用户增长
- 最近 30 天面试次数
- 平均分趋势
## 3.8.3 岗位热度
- 岗位使用次数
- 平均得分
## 3.8.4 模型数据
- 模型调用成功率
- 评分稳定性分析
------
# 4. 页面的 IA信息架构
```
登录页
首页Dashboard
├─ 数据总览
├─ 趋势图
岗位管理
├─ 岗位列表
├─ 新建岗位
题库管理
├─ 题目列表
├─ 新建题目
├─ 批量导入
用户管理
├─ 用户列表
├─ 用户详情
面试管理
├─ 面试记录
├─ 面试详情
内容配置
├─ Banner
├─ 推荐岗位
系统设置
├─ 小程序设置
├─ 文件存储设置
├─ 模型设置
权限管理
├─ 管理员
├─ 角色
├─ 权限点
```
------
# 5. 非功能性要求B 端)
- 管理后台响应 < 300ms
- 并发支持 1000+
- 日志必须记录所有管理员操作
- 敏感操作需二次确认
- 文件需防盗链

266
docs/C端需求文档.md Normal file
View File

@@ -0,0 +1,266 @@
------
# 📘《AI 智能模拟面试系统》C 端用户端PRD —— 完整版
------
# 0. 文档信息
| 项目 | 内容 |
| ------------ | ------------------------------ |
| 产品名称 | AI 智能模拟面试系统C 端) |
| 文档版本 | v1.0 |
| 支持平台 | PC 网页、Mobile H5、微信小程序 |
| 使用人群 | 求职者,学生,工作者 |
| 作者 | ChatGPT产品经理角色 |
| 最后更新时间 | 2025.11 |
------
# 1. 产品概述
用户通过 C 端可进行:
- AI 模拟面试
- 模拟面试评分与复盘
- 岗位选择与题库练习
- 个人资料与简历管理
- 学习提升
C 端目标是帮助用户低成本、高效率提升面试能力。
------
# 2. 用户场景与核心价值
## 场景
1. 求职前需要练习面试,但没有面试官
2. 希望通过真实演练了解自身弱点
3. 想知道针对某岗位常见问题
4. 想收到专业点评和改进建议
5. 想通过复盘持续提升表达能力与专业度
## 核心价值
- 用 AI 快速模拟真实面试
- 获得专业化、结构化的评分评估
- 提供收敛的复盘内容与提升建议
- 实现个性化岗位匹配与题目推荐
------
# 3. 角色 & 权限
| 角色 | 权限 |
| -------- | ---------------------------------------- |
| 普通用户 | 注册、登录、创建面试、查看报告、编辑简历 |
| 游客 | 浏览岗位、部分题库、引导注册 |
------
# 4. 功能需求(按模块)
------
## 4.1 账号系统
### 4.1.1 登录注册
| 功能 | 说明 |
| ------------------ | -------------------------- |
| 手机号验证码登录 | 支持短信验证 |
| 微信登录(小程序) | 自动拉取头像昵称 |
| 邮箱/密码登录 | 可选开关 |
| Token 机制 | AccessToken + RefreshToken |
**错误提示:**
- 验证码不正确
- 手机格式错误
- 微信登录失败
------
## 4.2 用户中心
### 4.2.1 基本资料
用户可编辑:
| 字段 | 限制 |
| -------- | ------ |
| 头像 | ≤5MB |
| 昵称 | ≤20 字 |
| 技能标签 | ≤10 |
| 工作年限 | 020+ |
| 当前职位 | ≤50 字 |
### 4.2.2 简历管理
| 功能 | 描述 |
| ------------ | ---------------------- |
| 上传简历 | PDF/DOC ≤10MB |
| 自动解析 | 解析技能 → 项目 → 亮点 |
| 编辑解析结果 | 可调整 |
| 删除简历 | 支持 |
------
## 4.3 岗位模块
### 4.3.1 岗位列表
功能点:
- 按关键词搜索
- 筛选(技能标签、难度)
- 岗位推荐(基于用户简历)
- 按热度排序
### 4.3.2 岗位详情
包含:
- 岗位描述
- 技能要求
- 常见题型展示
- 推荐学习路径
- 「开始模拟面试」按钮
------
## 4.4 模拟面试(主流程)
### 4.4.1 创建面试
用户选择:
- 岗位
- 模式(文本 / 语音)
- 题量5/8/10
进入面试准备界面(提示麦克风权限)。
------
### 4.4.2 面试进行中
| 功能 | 描述 |
| -------- | ------------------------------ |
| AI 提问 | 每题 1 个主问题,可 12 个追问 |
| 文本回答 | 输入框 |
| 语音回答 | 录音上传ASR 转写 |
| 进度条 | x / N 题 |
| 跳过功能 | 可跳过 1 次 |
| 中断 | 用户主动退出或异常断开 |
### 界面区块
- 左侧:问题内容
- 中间:答题区
- 下方:进度 & 操作按钮
------
## 4.5 面试结果与复盘
### 4.5.1 面试列表
字段:
- 总分
- 岗位
- 时间
- 面试模式
- 标签(优秀、一般、需提升)
### 4.5.2 面试详情
展示:
- 每题内容与用户原回答
- 文本/音频回放
- 评分维度(逻辑、表达、深度、专业)
- AI 点评
------
### 4.5.3 复盘报告(核心)
报告包含以下结构:
| 模块 | 内容 |
| ------------ | ---------- |
| 综合评分 | 010 |
| 综合评语 | 200300 字 |
| 优势总结 | 35 条 |
| 待改进点 | 35 条 |
| 行为建议 | 37 条 |
| 推荐学习方向 | 动态生成 |
支持下载 / 分享卡片(图像形式)。
------
## 4.6 学习中心
内容:
- 岗位相关题库
- 常见行为面试题STAR 法则)
- 面试技巧教程
- 视频/图文内容(可选)
------
## 4.7 通用功能
### 4.7.1 文件上传
- 支持音频上传WAV/MP3
- 支持头像、简历、图片上传
### 4.7.2 消息中心
- 面试报告生成通知
- 系统公告
### 4.7.3 设置
- 修改手机号
- 注销账号
------
# 5. 页面结构(信息架构 IA
```
首页
├─ 推荐岗位
├─ 开始面试
用户中心
├─ 基本资料
├─ 简历管理
岗位
├─ 岗位列表
├─ 岗位详情
模拟面试
├─ 创建面试
├─ 面试问答页
面试记录
├─ 面试列表
├─ 面试详情
└─ 复盘报告
学习中心
├─ 题库练习
├─ 面试技巧
```
------
# 6. 非功能性要求C 端)
- 面试响应 ≤ 200ms
- 报告生成 ≤ 10 秒
- 音频上传失败重试机制

0
mvnw vendored Normal file → Executable file
View File

0
mvnw.cmd vendored Normal file → Executable file
View File

139
pom.xml Normal file → Executable file
View File

@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.10-SNAPSHOT</version>
<version>3.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.qingqiu</groupId>
@@ -13,6 +13,7 @@
<version>0.0.1-SNAPSHOT</version>
<name>AI-Interview</name>
<description>AI-Interview</description>
<!-- TODO: 考虑删除空的元数据元素 -->
<url/>
<licenses>
<license/>
@@ -28,6 +29,9 @@
</scm>
<properties>
<java.version>17</java.version>
<spring-ai.version>1.0.0</spring-ai.version>
<spring-ai-alibaba.version>1.0.0.2</spring-ai-alibaba.version>
<spring-boot.version>3.4.5</spring-boot.version>
</properties>
<dependencies>
<!-- MyBatis-Plus -->
@@ -43,31 +47,68 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- aop和aspect -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId> <!-- 用于 WebClient -->
</dependency>
<!-- &lt;!&ndash; Spring AI Dependencies &ndash;&gt;-->
<!-- TODO: 考虑删除已注释的依赖 -->
<!-- <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>-->
<!-- <version>1.0.0-SNAPSHOT</version>-->
<!-- </dependency>-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dashscope-sdk-java</artifactId>
<version>2.21.5</version>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<!-- TODO: 检查内存相关依赖是否都需要 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-memory</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-memory-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-memory-redis</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>5.2.0</version>
</dependency>
<!-- 集成redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- TODO: 考虑删除已注释的依赖 -->
<!-- <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>
@@ -124,16 +165,28 @@
</dependency>
</dependencies>
<dependencyManagement>
<!-- <dependencies>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.ai</groupId>-->
<!-- <artifactId>spring-ai-bom</artifactId>-->
<!-- <version>1.0.0-M5</version> &lt;!&ndash; 或最新版本 &ndash;&gt;-->
<!-- <type>pom</type>-->
<!-- <scope>import</scope>-->
<!-- </dependency>-->
<!-- </dependencies>-->
<dependencies>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>${spring-ai-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-bom</artifactId>
@@ -142,10 +195,19 @@
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<finalName>ai-interview</finalName>
<plugins>
<!-- TODO: 检查Spring Boot插件版本是否与父POM一致 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.5.0</version>
</plugin>
<!-- TODO: 考虑在正式环境中启用测试 -->
<!-- maven 打包时跳过测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
@@ -156,33 +218,6 @@
</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>

0
sql/.idea/.gitignore generated vendored Normal file → Executable file
View File

View File

View File

@@ -1,22 +0,0 @@
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;
}

View File

@@ -1,7 +0,0 @@
package com.qingqiu.interview.ai.factory;
import com.qingqiu.interview.ai.service.AIClientService;
public interface AIClientFactory {
AIClientService createAIClient();
}

View File

@@ -1,25 +0,0 @@
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();
}
}

View File

@@ -1,14 +0,0 @@
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);
}
}

View File

@@ -1,14 +0,0 @@
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);
}
}

View File

@@ -1,13 +0,0 @@
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;
}
}

View File

@@ -1,65 +0,0 @@
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;
}
}

View File

@@ -1,60 +0,0 @@
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);
}
}
}

View File

@@ -0,0 +1,44 @@
package com.qingqiu.interview.common.constants;
/**
* 聊天相关常量类
* 定义了聊天功能中使用的各种常量值
*/
public class ChatConstant {
/**
* MySQL聊天记录存储的Bean名称
*/
public static final String MYSQL_CHAT_MEMORY_BEAN_NAME = "mysql-chat-memory";
/**
* Redis聊天记录存储的Bean名称
*/
public static final String REDIS_CHAT_MEMORY_BEAN_NAME = "redis-chat-memory";
/**
* 聊天记录最大保存消息数
*/
public static final Integer MAX_MESSAGES = 100;
/**
* DashScope聊天模型的Bean名称
*/
public static final String DASH_SCOPE_CHAT_MODEL_BEAN_NAME = "dash-scope-chat-model";
public static final String OPEN_AI_CHAT_MODEL_BEAN_NAME = "open-ai-chat-model";
/**
* DashScope聊天客户端的Bean名称
*/
public static final String DASH_SCOPE_CHAT_CLIENT_BEAN_NAME = "dash-scope-chat-client";
/**
* OpenAI聊天客户端的Bean名称
*/
public static final String OPEN_AI_CHAT_CLIENT_BEAN_NAME = "open-ai-chat-client";
/**
* 最大补全token数量
*/
public static final Integer MAX_COMPLETION_TOKENS = 8192;
}

View File

@@ -0,0 +1,17 @@
package com.qingqiu.interview.common.constants;
import java.math.BigDecimal;
/**
* <h1>公共常量</h1>
* @author huangpeng
* @date 2025/9/11 09:30
*/
public class CommonConstant {
public static final Integer ZERO = 0;
public static final Integer ONE = 1;
public static final Long ROOT_PARENT_ID = 0L;
public static final Integer MAX_TOKEN = 64000;
public static final BigDecimal DEFAULT_TRUNCATE_RATIO = new BigDecimal("0.1");
}

View File

@@ -1,4 +1,4 @@
package com.qingqiu.interview.dto;
package com.qingqiu.interview.common.dto;
import lombok.Data;
import lombok.EqualsAndHashCode;

View File

@@ -0,0 +1,63 @@
package com.qingqiu.interview.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* <h1></h1>
*
* @author huangpeng
* @date 2025/9/11 09:49
*/
@Getter
@AllArgsConstructor
public enum CommonStateEnum {
/**
* 禁用状态
*/
DISABLED(0, "禁用"),
/**
* 启用状态
*/
ENABLED(1, "启用"),
;
/**
* 状态码
*/
private final Integer code;
private final String value;
/**
* 根据状态码获取枚举
*/
public static CommonStateEnum getByCode(Integer code) {
if (code == null) {
return null;
}
for (CommonStateEnum state : values()) {
if (state.getCode().equals(code)) {
return state;
}
}
return null;
}
/**
* 根据标识获取枚举
*/
public static CommonStateEnum getByValue(String value) {
if (value == null || value.isEmpty()) {
return null;
}
for (CommonStateEnum state : values()) {
if (state.getValue().equalsIgnoreCase(value)) {
return state;
}
}
return null;
}
}

View File

@@ -0,0 +1,33 @@
package com.qingqiu.interview.common.enums;
import lombok.Getter;
/**
* <h1></h1>
*
* @author qingqiu
* @date 2025/9/18 16:43
*/
@Getter
public enum DocumentParserProvider {
PDF("pdf"),
MARKDOWN("md"),
;
private final String code;
DocumentParserProvider(String code) {
this.code = code;
}
public static DocumentParserProvider fromCode(String code) {
for (DocumentParserProvider provider : values()) {
if (provider.getCode().equals(code)) {
return provider;
}
}
throw new IllegalArgumentException("Unknown provider: " + code);
}
}

View File

@@ -0,0 +1,30 @@
package com.qingqiu.interview.common.enums;
import lombok.Getter;
@Getter
public enum LLMProvider {
OPEN_AI("openai"),
CLAUDE("claude"),
GEMINI("gemini"),
DEEPSEEK("deepSeek"),
OLLAMA("ollama"),
QWEN("qwen"),
;
private final String code;
LLMProvider(String code) {
this.code = code;
}
public static LLMProvider fromCode(String code) {
for (LLMProvider provider : values()) {
if (provider.getCode().equals(code)) {
return provider;
}
}
throw new IllegalArgumentException("Unknown provider: " + code);
}
}

View File

View File

View File

0
src/main/java/com/qingqiu/interview/common/res/R.java Normal file → Executable file
View File

View File

View File

View File

@@ -1,26 +0,0 @@
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);
}
}

View File

@@ -0,0 +1,430 @@
package com.qingqiu.interview.common.utils;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 提示模板工具类
*/
public class PromptTemplateUtils {
/**
* 获取提取技能的提示
*
* @param resumeContent 简历内容
* @return 提示
*/
public static Prompt getExtractSkillsPrompt(String resumeContent) {
SystemMessage systemMessage = new SystemMessage("""
你是一位资深的技术面试官,以严格和深入著称。
你需要从提供的简历内容中,提取出所有与职位相关的技能。
请按照以下JSON格式返回
{"skills": ["技能1", "技能2", "..."]}
""");
UserMessage userMessage = new UserMessage(resumeContent);
return new Prompt(List.of(userMessage, systemMessage));
}
/**
* 获取AI面试官的提示
*
* @param params 参数
* @return 提示
*/
public static Prompt getAiInterviewerPrompt(Map<String, Object> params) {
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate("""
你是一位经验丰富的软件开发技术面试官,具备以下特质:
## 专业能力
- 拥有10年以上软件开发和团队管理经验
- 熟悉各种主流技术栈和架构模式
- 擅长通过技术面试评估候选人的真实能力
- 能够设计多层次、递进式的面试问题
## 面试原则
1. **精准匹配**: 问题必须紧密结合岗位要求和候选人背景
2. **层次递进**: 从基础概念到深度应用,从理论到实践
3. **场景化考察**: 基于真实项目场景设计问题
4. **全面评估**: 涵盖技术深度、问题解决能力、系统思维
## 本岗位招聘要求
[{jobRequirements}]
## 输出要求
- 严格按照指定的JSON格式输出
- 不得包含任何JSON格式之外的内容
- 不得添加代码块标记(```json)或其他解释性文字
- 确保生成的问题数量与要求完全一致
""");
PromptTemplate userPromptTemplate = new PromptTemplate("""
请根据岗位招聘要求设计 {count} 道技术面试题。
## 候选人信息
### 技术栈
{skills}
### 简历内容
{resume}
## 面试题设计要求
### 覆盖维度
1. **基础理论** (20%): 核心概念、原理机制
2. **项目实践** (40%): 具体项目中的技术难点和解决方案
3. **系统设计** (20%): 架构思维、技术选型、性能优化
4. **问题解决** (20%): 调试能力、故障排查、代码优化
### 难度分布
- 基础题 (30%): 验证核心技能掌握情况
- 进阶题 (50%): 考察深度理解和实际应用
- 高阶题 (20%): 评估架构能力和创新思维
### 问题类型
- **概念阐述**: "请解释...""什么是..."
- **场景分析**: "在你的XX项目中...""如果遇到XX问题..."
- **方案设计**: "如何设计...""请给出..."
- **比较选择**: "XX和YY的区别...""为什么选择..."
## 输出格式
必须严格按照以下JSON格式输出不得有任何偏差
{jsonRes}
请立即开始生成面试题:
""");
String s = """
{
"questions": [
{
"id": "ai-gen-1",
"content": "问题内容..."
}
]
}
""";
params.put("jsonRes", s);
return new Prompt(List.of(userPromptTemplate.createMessage(params), systemPromptTemplate.createMessage(params)));
}
public static Prompt getLocalInterviewPrompt(Map<String, Object> params) {
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate("""
你是一位资深的技术面试专家。
## 你的专业背景
- 10年+ 软件开发和技术管理经验
- 熟悉前端、后端、全栈、嵌入式、移动端、DevOps、大数据、AI等各技术领域
- 精通各种技术栈的深度和广度评估
- 擅长根据岗位特点和候选人背景进行精准匹配
## 通用岗位分类及评估重点
### 后端开发岗位
- 重点框架熟练度、数据库设计、API设计、性能优化、并发处理
- 核心技能:编程语言基础、框架应用、系统设计、问题排查
### 前端开发岗位
- 重点UI/UX实现、性能优化、跨浏览器兼容、现代框架使用
- 核心技能HTML/CSS/JS、框架应用、工程化、用户体验
### 全栈开发岗位
- 重点前后端技术栈、系统架构、DevOps流程
- 核心技能:多技术栈掌握、系统整合、项目管理
### 架构师岗位
- 重点:系统设计、技术选型、性能调优、团队技术规划
- 核心技能:架构设计、技术决策、团队领导、业务理解
## 筛选策略框架
### 匹配维度权重
1. **技术栈匹配度** (35%): 候选人技能与岗位要求的重合度
2. **项目经验相关性** (30%): 过往项目与岗位场景的匹配度
3. **技能深度评估** (20%): 根据经验年限判断技能掌握深度
4. **发展潜力考量** (15%): 学习能力和技术视野的考察
## 本岗位招聘要求
[{jobRequirements}]
## 输出规范
- 必须输出标准JSON格式{jsonRes}
- 不得包含任何解释、注释或代码块标记
- 确保所选题目ID真实存在于题库中
- 保证题目数量与要求完全一致
""");
PromptTemplate userPromptTemplate = new PromptTemplate("""
请根据岗位招聘要求,从题库中筛选筛选 {count} 道技术面试题。
## 候选人档案
### 技术栈
{skills}
### 简历内容
{resume}
## 筛选要求
请根据以上信息,结合你的专业框架,从题库中筛选出最能评估该候选人是否适合此岗位的面试题。
### 重点考虑因素
1. 岗位核心技术要求与候选人技能的匹配度
2. 候选人项目经验与岗位应用场景的关联性
3. 题目难度与候选人经验水平的适配性
4. 题目类型的多样性(理论+实践+设计)
"""
);
params.put("jsonRes",
"""
{
"questions": [
{
"id": "1",
"content": "问题内容..."
},
{
"id": "3",
"content": "问题内容..."
}
...
]
}
"""
);
return new Prompt(List.of(
userPromptTemplate.createMessage(params),
systemPromptTemplate.createMessage(params)
));
}
public static Prompt getEvaluatePrompt(Map<String, Object> params) {
// SystemMessage systemMessage = new SystemMessage("""
// 你是一位经验丰富的高级技术面试官,以公正、严谨、深入的评估风格著称。
// ## 你的专业特质
// - 拥有15年+ 技术开发和面试经验
// - 善于通过追问挖掘候选人的真实技术水平
// - 能够准确识别标准答案、实际经验和深度理解的区别
// - 注重考察解决问题的思路和实际应用能力
//
// """);
PromptTemplate promptTemplate = new PromptTemplate("""
请对候选人的回答进行专业评估。
## 评估维度及权重
### 技术准确性 (30%)
- 概念理解的正确性
- 技术细节的准确程度
- 是否存在明显错误或误解
### 深度与广度 (25%)
- 知识的深入程度
- 相关技术的关联理解
- 是否能举一反三
### 实践经验 (25%)
- 是否有真实项目经验支撑
- 能否结合具体场景说明
- 对技术选型和权衡的理解
### 表达能力 (20%)
- 逻辑清晰度
- 表达的完整性
- 专业术语使用的准确性
## 评分标准
### 优秀 (85-100分)
- 回答准确、深入、有见解
- 能结合实际项目经验
- 表达清晰、逻辑严密
- 展现出深度思考和实践能力
### 良好 (70-84分)
- 回答基本正确,有一定深度
- 有实际应用经验
- 表达较为清晰
- 个别地方可能需要补充
### 及格 (60-69分)
- 回答基本正确但较浅显
- 缺乏深入理解或实践经验
- 表达尚可但不够完整
- 需要进一步考察
### 不及格 (0-59分)
- 回答错误或严重不完整
- 基础概念理解有误
- 表达混乱或逻辑不清
- 明显缺乏相关经验
## 追问策略
### 何时追问
1. **概念模糊**: 候选人给出的概念定义不够准确或完整
2. **缺少细节**: 回答过于宽泛,缺乏具体的技术细节
3. **经验质疑**: 怀疑候选人是否有真实的项目经验
4. **深度探索**: 基础回答正确,想考察更深层次的理解
### 追问类型
1. **澄清追问**: "你刚才提到XX能具体解释一下吗"
2. **场景追问**: "在实际项目中你是如何处理XX问题的"
3. **对比追问**: "XX和YY有什么区别你会如何选择"
4. **深度追问**: "如果遇到XX情况你会如何优化"
### 追问限制
- 单个问题最多追问3次
- 追问应该递进深入,不重复
- 追问后必须给出综合评估
- 避免过度纠缠细节,影响整体面试节奏
## 当前问题
{question}
## 候选人回答
{candidateAnswer}
## 评估任务
### 请分析以下方面
1. **技术准确性**: 回答是否正确,有无技术错误
2. **完整性**: 是否涵盖了问题的关键要点
3. **深度**: 是否展现了深入的理解和思考
4. **实践性**: 是否结合了实际项目经验
5. **表达质量**: 逻辑是否清晰,表达是否准确
### 追问决策逻辑
- 如果回答存在明显不足,制定一个精准的追问来深入考察
- 如果已经追问过2次本次必须结束追问continueAsking: false
- 追问应该针对性强,避免过于宽泛
- 优先考察核心技术能力,避免偏离主题
### AI标准答案要求
请提供一个简洁、准确的技术答案作为参考,突出关键要点。
## 输出格式要求
严格按照以下JSON格式输出确保字段完整且格式正确
{jsonRes}
开始评估:
""");
params.put(
"jsonRes",
"""
{
"feedback": "对回答的具体评价,指出优点和不足",
"suggestions": "具体的改进建议和学习方向",
"aiAnswer": "简洁准确的标准答案要点",
"score": 75.5,
"continueAsking": true,
"followUpQuestion": "具体的追问问题(如果不追问则为空字符串)"
}
"""
);
return new Prompt(List.of(
promptTemplate.createMessage(params)
));
}
/**
* 获取最终报告
*
* @param params 参数
* @return 提示
*/
public static Prompt getFinalReportPrompt(Map<String, Object> params) {
// SystemMessage systemMessage = new SystemMessage("""
// 你是一位资深的技术面试官和HR专家具备丰富的候选人评估经验。你的职责是
//
// ## 核心职能:
// - 基于面试数据进行客观、全面的技术能力评估
// - 提供标准化的面试结果分析和建议
// - 支持招聘决策制定
//
// ## 评估原则:
// 1. **客观性**:基于实际表现数据,避免主观推测
// 2. **全面性**:覆盖技术能力、沟通表达、问题解决等维度
// 3. **标准化**:使用统一的评分体系和输出格式
// 4. **实用性**:提供可执行的改进建议和明确的录用建议
//
// ## 输出要求:
// 严格按照JSON格式输出不得包含任何markdown标记或额外解释
//
// {
// "overallScore": <1-100整数>,
// "overallFeedback": "<综合评价客观描述候选人整体表现150-200字>",
// "technicalAssessment": {
// "Java基础": "掌握良好,对集合框架理解深入。",
// "Spring框架": "熟悉基本使用,但对底层原理理解不足。",
// "数据库": "能够编写常规SQL但在索引优化方面知识欠缺。",
// "<技术领域1>": "<该领域的具体评估>",
// ...
// },
// "strengthsAndWeaknesses": {
// "strengths": ["<具体优势1>", "<具体优势2>", "<具体优势3>"],
// "weaknesses": ["<具体不足1>", "<具体不足2>", "<具体不足3>"]
// },
// "suggestions": [
// "<具体可执行的改进建议1>",
// "<具体可执行的改进建议2>",
// "<具体可执行的改进建议3>",
// "<具体可执行的改进建议4>",
// "<具体可执行的改进建议5>"
// ],
// "hiringRecommendation": "<强烈推荐|推荐|待考虑|不推荐>",
// "hiringReason": "<录用建议的具体理由50-80字>"
// }
//
// ## 评分标准:
// - 90-100分技术优秀表达清晰思维敏捷超出岗位要求
// - 80-89分技术良好基本满足岗位要求有培养潜力
// - 70-79分技术一般需要指导和培养
// - 60-69分技术较弱存在明显知识盲区
// - 60分以下技术不足不符合岗位要求
// """);
PromptTemplate promptTemplate = new PromptTemplate("""
请根据以下候选人信息和面试记录,生成标准化的面试评估报告:
## 核心职能:
- 基于面试数据进行客观、全面的技术能力评估
- 提供标准化的面试结果分析和建议
- 支持招聘决策制定
## 评估原则:
1. **客观性**:基于实际表现数据,避免主观推测
2. **全面性**:覆盖技术能力、沟通表达、问题解决等维度
3. **标准化**:使用统一的评分体系和输出格式
4. **实用性**:提供可执行的改进建议和明确的录用建议
## 输出要求:
严格按照JSON格式输出不得包含任何markdown标记或额外解释
{jsonRes}
## 评分标准:
- 90-100分技术优秀表达清晰思维敏捷超出岗位要求
- 80-89分技术良好基本满足岗位要求有培养潜力
- 70-79分技术一般需要指导和培养
- 60-69分技术较弱存在明显知识盲区
- 60分以下技术不足不符合岗位要求
## 候选人简历信息:
{resume}
## 完整面试记录:
{history}
请开始评估并输出JSON格式的报告。
""");
params.put("jsonRes", """
{
"overallScore": <1-100整数>,
"overallFeedback": "<综合评价客观描述候选人整体表现150-200字>",
"technicalAssessment": {
"Java基础": "掌握良好,对集合框架理解深入。",
"Spring框架": "熟悉基本使用,但对底层原理理解不足。",
"数据库": "能够编写常规SQL但在索引优化方面知识欠缺。",
"<技术领域1>": "<该领域的具体评估>",
...
},
"strengthsAndWeaknesses": {
"strengths": ["<具体优势1>", "<具体优势2>", "<具体优势3>"],
"weaknesses": ["<具体不足1>", "<具体不足2>", "<具体不足3>"]
},
"suggestions": [
"<具体可执行的改进建议1>",
"<具体可执行的改进建议2>",
"<具体可执行的改进建议3>",
"<具体可执行的改进建议4>",
"<具体可执行的改进建议5>"
],
"hiringRecommendation": "<强烈推荐|推荐|待考虑|不推荐>",
"hiringReason": "<录用建议的具体理由50-80字>"
}
""");
return new Prompt(List.of(
promptTemplate.createMessage(params)
));
}
}

View File

@@ -6,12 +6,12 @@ import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
@Component
public class SpringApplicationContextUtil implements ApplicationContextAware {
public class SpringApplicationContextUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringApplicationContextUtil.applicationContext = applicationContext;
SpringApplicationContextUtils.applicationContext = applicationContext;
}
public static <T> T getBean(Class<T> beanClass) {

View File

@@ -0,0 +1,63 @@
package com.qingqiu.interview.common.utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
public class TreeUtils {
/**
* 通用树形结构构建方法
*/
public static <T, ID> List<T> buildTree(List<T> list,
Function<T, ID> idGetter,
Function<T, ID> parentIdGetter,
Function<T, List<T>> childrenSetter,
ID rootParentId) {
if (list == null || list.isEmpty()) {
return Collections.emptyList();
}
// 按父ID分组
Map<ID, List<T>> parentMap = list.stream()
.collect(Collectors.groupingBy(parentIdGetter));
// 设置子节点
list.forEach(item -> {
List<T> children = parentMap.get(idGetter.apply(item));
if (children != null && !children.isEmpty()) {
childrenSetter.apply(item).addAll(children);
}
});
// 返回根节点
return parentMap.get(rootParentId);
}
/**
* 扁平化树形结构
*/
public static <T> List<T> flattenTree(List<T> tree, Function<T, List<T>> childrenGetter) {
List<T> result = new ArrayList<>();
flattenTreeRecursive(tree, childrenGetter, result);
return result;
}
private static <T> void flattenTreeRecursive(List<T> nodes,
Function<T, List<T>> childrenGetter,
List<T> result) {
if (nodes == null) return;
for (T node : nodes) {
result.add(node);
List<T> children = childrenGetter.apply(node);
if (children != null && !children.isEmpty()) {
flattenTreeRecursive(children, childrenGetter, result);
}
}
}
}

View File

@@ -0,0 +1,8 @@
package com.qingqiu.interview.common.utils;
public class UUIDUtils {
public static String getUUID() {
return java.util.UUID.randomUUID().toString().replace("-", "");
}
}

View File

@@ -0,0 +1,46 @@
package com.qingqiu.interview.common.utils;
import io.netty.channel.ChannelOption;
import org.springframework.http.client.ReactorClientHttpRequestFactory;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.client.RestClient;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import java.time.Duration;
/**
* @program: ai-interview
* @description:
* @author: huangpeng
* @create: 2025-11-06 20:04
**/
public class WebClientUtils {
/**
* 创建全局默认的WebClient Bean实例
* 配置了连接超时和响应超时时间
*
* @return WebClient实例
*/
public static WebClient.Builder getWebClientBuilder() {
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 30000) // 连接超时
.responseTimeout(Duration.ofMillis(30000)); // 读取超时
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
;
}
public static RestClient.Builder getRestClientBuilder() {
return RestClient.builder()
.requestFactory(
new ReactorClientHttpRequestFactory(
HttpClient.create()
.responseTimeout(Duration.ofMinutes(30))
)
);
}
}

View File

@@ -0,0 +1,73 @@
package com.qingqiu.interview.config;
import com.alibaba.cloud.ai.memory.jdbc.MysqlChatMemoryRepository;
import com.alibaba.cloud.ai.memory.redis.RedisChatMemoryRepository;
import com.qingqiu.interview.common.constants.ChatConstant;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import static com.qingqiu.interview.common.constants.ChatConstant.MAX_MESSAGES;
/**
* 聊天记忆相关配置
*/
@Configuration
public class ChatMemoryConfig {
@Value("${spring.ai.memory.redis.host}")
private String redisHost;
@Value("${spring.ai.memory.redis.port}")
private int redisPort;
@Value("${spring.ai.memory.redis.password}")
private String redisPassword;
@Value("${spring.ai.memory.redis.timeout}")
private int redisTimeout;
@Resource
private DataSource dataSource;
@Bean
public MysqlChatMemoryRepository mysqlChatMemoryRepository() {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
return MysqlChatMemoryRepository.mysqlBuilder()
.jdbcTemplate(jdbcTemplate)
.build();
}
@Primary
@Bean(name = ChatConstant.MYSQL_CHAT_MEMORY_BEAN_NAME)
public MessageWindowChatMemory mysqlChatMemory(MysqlChatMemoryRepository mysqlChatMemoryRepository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(mysqlChatMemoryRepository)
.maxMessages(MAX_MESSAGES)
.build();
}
@Bean
public RedisChatMemoryRepository redisChatMemoryRepository() {
return RedisChatMemoryRepository.builder()
.host(redisHost)
.port(redisPort)
// 若没有设置密码则注释该项
.password(redisPassword)
.timeout(redisTimeout)
.build();
}
@Bean(name = ChatConstant.REDIS_CHAT_MEMORY_BEAN_NAME)
public MessageWindowChatMemory redisChatMemory(RedisChatMemoryRepository redisChatMemoryRepository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(redisChatMemoryRepository)
.maxMessages(MAX_MESSAGES)
.build();
}
}

View File

@@ -0,0 +1,22 @@
package com.qingqiu.interview.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").
allowedOriginPatterns("*") //允许跨域的域名,可以用*表示允许任何域名使用
// allowedOrigins("*"). //在Springboot2.4对应Spring5.3后在设置allowCredentials(true)的基础上不能直接使用通配符设置allowedOrigins而是需要指定特定的URL。如果需要设置通配符需要通过allowedOriginPatterns指定
.allowedMethods("GET", "POST", "DELETE", "PUT") //允许任何方法post、get等
.allowedHeaders("*") //允许任何请求头
.allowCredentials(true) //带上cookie信息
.exposedHeaders(HttpHeaders.SET_COOKIE).maxAge(3600L); //maxAge(3600)表明在3600秒内不需要再发送预检验请求可以缓存该结果
}
}

View File

@@ -1,14 +1,120 @@
package com.qingqiu.interview.config;
import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.api.DashScopeResponseFormat;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import com.qingqiu.interview.common.constants.ChatConstant;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import static com.qingqiu.interview.common.utils.WebClientUtils.getRestClientBuilder;
import static com.qingqiu.interview.common.utils.WebClientUtils.getWebClientBuilder;
/**
* DashScope相关配置类
* 主要配置阿里云百炼平台(DashScope)和OpenAI的大模型服务
* 包括API客户端、聊天模型和聊天客户端的配置
*
* @author qingqiu
*/
@Configuration
public class DashScopeConfig {
// 从配置文件中读取DashScope API密钥
@Value("${spring.ai.dashscope.api-key}")
private String dashScopeApiKey;
@Value("${spring.ai.dashscope.chat.options.model}")
private String dashScopeChatModelName;
@Resource(name = ChatConstant.MYSQL_CHAT_MEMORY_BEAN_NAME)
private MessageWindowChatMemory mysqlChatMemory;
/**
* 创建DashScopeApi Bean实例
* 配置了HTTP客户端连接参数包括连接超时和响应超时时间
*
* @return DashScopeApi实例
*/
@Bean
public Generation generation() {
return new Generation();
public DashScopeApi dashScopeApi() {
return DashScopeApi.builder()
.apiKey(dashScopeApiKey)
.restClientBuilder(getRestClientBuilder())
.webClientBuilder(getWebClientBuilder())
.build();
}
/**
* 创建DashScope聊天模型Bean实例
* 配置了最大token数、使用的模型以及返回格式为JSON对象
*
* @return ChatModel实例
*/
@Bean(name = ChatConstant.DASH_SCOPE_CHAT_MODEL_BEAN_NAME)
public ChatModel dashScopeChatModel() {
return DashScopeChatModel.builder()
.dashScopeApi(dashScopeApi())
.defaultOptions(
DashScopeChatOptions.builder()
.withMaxToken(ChatConstant.MAX_COMPLETION_TOKENS)
.withModel(dashScopeChatModelName)
.withResponseFormat(
DashScopeResponseFormat.builder()
.type(DashScopeResponseFormat.Type.JSON_OBJECT)
.build()
)
.build()
)
.build();
}
/**
* 创建默认的聊天客户端Bean实例使用DashScope模型
* 添加了日志记录功能
*
* @return ChatClient实例
*/
@Bean
@Primary
public ChatClient chatClient() {
return ChatClient
.builder(dashScopeChatModel())
.defaultAdvisors(
new SimpleLoggerAdvisor()
)
.build();
}
/**
* 创建基于MySQL存储聊天历史的DashScope聊天客户端Bean实例
* 配置了消息内存管理和日志记录功能
*
* @return ChatClient实例
*/
@Bean(name = ChatConstant.DASH_SCOPE_CHAT_CLIENT_BEAN_NAME)
public ChatClient dashScopeChatClient() {
return ChatClient
.builder(dashScopeChatModel())
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(mysqlChatMemory).build(),
new SimpleLoggerAdvisor()
)
.build();
}
}

View File

@@ -0,0 +1,57 @@
package com.qingqiu.interview.config;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
public class Fastjson2RedisSerializer<T> implements RedisSerializer<T> {
// 默认编码
public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
// 泛型类型,用于反序列化
private final Class<T> clazz;
public Fastjson2RedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
/**
* 序列化:将对象转换为字节数组
*/
@Override
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return new byte[0];
}
try {
// 使用 Fastjson2 序列化对象,并写入字节数组
// 配置写入特性WriteClassName 确保反序列化时能识别类型,如果不需要,可以移除。
return JSON.toJSONBytes(t, JSONWriter.Feature.WriteClassName);
} catch (Exception ex) {
throw new SerializationException("Could not serialize object with Fastjson2", ex);
}
}
/**
* 反序列化:将字节数组转换为对象
*/
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length <= 0) {
return null;
}
try {
// 使用 Fastjson2 反序列化字节数组
// 配置读取特性SupportAutoType确保可以正确读取带有类名信息的JSON
return JSON.parseObject(bytes, clazz, JSONReader.Feature.SupportAutoType);
} catch (Exception ex) {
throw new SerializationException("Could not deserialize object with Fastjson2", ex);
}
}
}

View File

View File

View File

@@ -0,0 +1,100 @@
package com.qingqiu.interview.config;
import com.qingqiu.interview.common.constants.ChatConstant;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.ResponseFormat;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import static com.qingqiu.interview.common.utils.WebClientUtils.getRestClientBuilder;
import static com.qingqiu.interview.common.utils.WebClientUtils.getWebClientBuilder;
/**
* @program: ai-interview
* @description: openai聊天配置
* @author: huangpeng
* @create: 2025-11-06 20:03
**/
@Configuration
public class OpenAiChatConfig {
@Value("${spring.ai.openai.chat.options.model}")
private String openAiChatModelName;
@Value("${spring.ai.openai.base-url}")
private String openAiBaseUrl;
@Value("${spring.ai.openai.api-key}")
private String openAiApiKey;
@Resource(name = ChatConstant.MYSQL_CHAT_MEMORY_BEAN_NAME)
private MessageWindowChatMemory mysqlChatMemory;
/**
* 创建OpenAI聊天模型Bean实例主模型
* 配置了最大完成token数和响应格式为JSON Schema
*
* @return ChatModel实例
*/
@Bean(name = ChatConstant.OPEN_AI_CHAT_MODEL_BEAN_NAME)
@Primary
public ChatModel openAiChatModel() {
return OpenAiChatModel.builder()
.openAiApi(
OpenAiApi.builder()
.apiKey(openAiApiKey)
.baseUrl(openAiBaseUrl)
.webClientBuilder(getWebClientBuilder())
.restClientBuilder(getRestClientBuilder())
.build()
)
.defaultOptions(
OpenAiChatOptions.builder()
.model(openAiChatModelName)
.maxCompletionTokens(ChatConstant.MAX_COMPLETION_TOKENS)
.responseFormat(
ResponseFormat.builder()
.type(ResponseFormat.Type.JSON_SCHEMA)
.jsonSchema(
ResponseFormat.JsonSchema
.builder()
.build()
).build()
)
.build()
)
.build();
}
/**
* 创建基于MySQL存储聊天历史的OpenAI聊天客户端Bean实例
* 配置了消息内存管理和日志记录功能
*
* @return ChatClient实例
*/
@Bean(name = ChatConstant.OPEN_AI_CHAT_CLIENT_BEAN_NAME)
public ChatClient openAiChatClient() {
return ChatClient
.builder(openAiChatModel())
.defaultAdvisors(
MessageChatMemoryAdvisor.builder(mysqlChatMemory).build(),
new SimpleLoggerAdvisor()
)
.build();
}
}

View File

@@ -0,0 +1,32 @@
package com.qingqiu.interview.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
template.setKeySerializer(stringRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
// 使用Fastjson2RedisSerializer来序列化和反序列化redis的value值
Fastjson2RedisSerializer<Object> fastjson2RedisSerializer = new Fastjson2RedisSerializer<>(Object.class);
template.setValueSerializer(fastjson2RedisSerializer);
template.setHashValueSerializer(fastjson2RedisSerializer);
template.afterPropertiesSet();
return template;
}
}

View File

View File

@@ -1,10 +1,23 @@
package com.qingqiu.interview.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qingqiu.interview.common.res.R;
import com.qingqiu.interview.entity.AiSessionLog;
import com.qingqiu.interview.service.IAiSessionLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* ai会话记录 前端控制器
@@ -13,8 +26,22 @@ import org.springframework.web.bind.annotation.RestController;
* @author huangpeng
* @since 2025-08-30
*/
@Slf4j
@RestController
@RequestMapping("/ai-session-log")
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
public class AiSessionLogController {
private final IAiSessionLogService service;
@GetMapping("/list-by-session-id/{sessionId}")
public R<List<AiSessionLog>> list(@PathVariable String sessionId) {
return R.success(service.list(
new LambdaQueryWrapper<AiSessionLog>()
.eq(AiSessionLog::getToken, sessionId)
.ne(AiSessionLog::getRole, MessageType.SYSTEM.getValue())
));
}
}

View File

@@ -0,0 +1,36 @@
package com.qingqiu.interview.controller;
import com.qingqiu.interview.common.res.R;
import com.qingqiu.interview.dto.ChatDTO;
import com.qingqiu.interview.dto.InterviewStartRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
/**
* <h1>AI聊天控制器</h1>
*
* @author qingqiu
* @date 2025/9/18 12:11
*/
@RestController
@RequestMapping("/chat")
@RequiredArgsConstructor
public class ChatController {
/**
* 创建聊天
* @return
*/
@PostMapping("/send")
public R<?> createChat(@RequestBody ChatDTO dto) {
return R.success();
}
@PostMapping("/interview/create")
public R<?> createInterview(@RequestParam("resume") MultipartFile resume,
@Validated @ModelAttribute InterviewStartRequest request) {
return R.success();
}
}

View File

@@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.RestController;
* 仪表盘数据统计接口
*/
@RestController
@RequestMapping("/api/v1/dashboard")
@RequestMapping("/dashboard")
@RequiredArgsConstructor
public class DashboardController {

View File

@@ -1,58 +1,133 @@
package com.qingqiu.interview.controller;
import com.qingqiu.interview.common.res.R;
import com.qingqiu.interview.dto.*;
import com.qingqiu.interview.entity.InterviewMessage;
import com.qingqiu.interview.entity.InterviewQuestionProgress;
import com.qingqiu.interview.entity.InterviewSession;
import com.qingqiu.interview.service.InterviewService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
/**
* 面试流程相关接口
* <h1></h1>
*
* @author qingqiu
* @date 2025/9/19 16:13
*/
@Slf4j
@RestController
@RequestMapping("/api/v1/interview")
@RequiredArgsConstructor
@RequestMapping("/interview")
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
public class InterviewController {
private final InterviewService interviewService;
/**
* 开始新的面试会话
* 开始面试
*
* @return 包含会话ID的会话信息
*/
@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);
public R<InterviewSession> start(@RequestPart("resume") MultipartFile resume,
@RequestPart("interviewStartDto") InterviewStartRequest request) {
// log.info("接受的数据: {}", JSONObject.toJSONString(request));
// return R.success();
try {
InterviewSession session = interviewService.startInterview(resume, request);
return R.success(session);
} catch (Exception e) {
log.error("开始面试失败", e);
return R.error("开始面试失败:" + e.getMessage());
}
}
/**
* 继续面试会话(用户回答)
* 获取下一个问题
*
* @param sessionId 会话ID
* @return 下一个问题
*/
@PostMapping("/chat")
public ApiResponse<InterviewResponse> continueInterview(@Validated @RequestBody ChatRequest request) {
InterviewResponse response = interviewService.continueInterview(request);
return ApiResponse.success(response);
@GetMapping("/next-question/{sessionId}/{progressId}")
public R<InterviewMessage> getNextQuestion(@PathVariable String sessionId,
@PathVariable Long progressId) {
try {
InterviewMessage nextQuestion = interviewService.getNextQuestion(sessionId, progressId);
if (nextQuestion == null) {
return R.success(null, "所有问题已回答完毕!");
}
return R.success(nextQuestion);
} catch (Exception e) {
// log.error("获取下一题失败", e);
return R.error("获取下一题失败:" + e.getMessage());
}
}
/**
* 获取所有面试会话列表
* 提交答案
*
* @param submitDto 包含进度ID和答案
* @return 对当前问题的评估
*/
@PostMapping("/submit-answer")
public R<InterviewQuestionProgress> submitAnswer(@RequestBody SubmitAnswerDTO submitDto) {
try {
InterviewQuestionProgress result = interviewService.submitAnswer(submitDto);
return R.success(result);
} catch (Exception e) {
// log.error("提交答案失败", e);
return R.error("提交答案失败:" + e.getMessage());
}
}
/**
* 结束面试并获取最终报告
*
* @param sessionId 会话ID
* @return 包含最终报告的会话信息
*/
@PostMapping("/{sessionId}/end")
public R<InterviewSession> endInterview(@PathVariable String sessionId) {
try {
InterviewSession finalSession = interviewService.endInterview(sessionId);
return R.success(finalSession);
} catch (Exception e) {
// log.error("结束面试失败", e);
return R.error("结束面试失败:" + e.getMessage());
}
}
@PostMapping("/get-history-list")
public ApiResponse<java.util.List<InterviewSession>> getInterviewHistoryList() {
return ApiResponse.success(interviewService.getInterviewSessions());
public R<List<InterviewSession>> getHistoryList() {
try {
List<InterviewSession> historyList = interviewService.list();
return R.success(historyList);
} catch (Exception e) {
// log.error("获取面试历史列表失败", e);
return R.error("获取面试历史列表失败:" + e.getMessage());
}
}
/**
* 获取单次面试的详细复盘报告
*/
@PostMapping("/get-report-detail")
public ApiResponse<InterviewReportResponse> getInterviewReportDetail(@RequestBody SessionRequest request) {
return ApiResponse.success(interviewService.getInterviewReport(request.getSessionId()));
@PostMapping("/get-report-detail/{sessionId}")
public R<InterviewReportResponse> getInterviewReportDetail(@PathVariable String sessionId) {
return R.success(interviewService.getInterviewReport(sessionId));
}
@PostMapping("/read-pdf")
public R<?> readPdf(@RequestParam MultipartFile file) throws IOException {
String readPdfFile = interviewService.readPdfFile(file);
log.info("resume content: {}", readPdfFile);
return R.success(readPdfFile);
}
}

View File

@@ -0,0 +1,42 @@
package com.qingqiu.interview.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qingqiu.interview.common.res.R;
import com.qingqiu.interview.entity.InterviewMessage;
import com.qingqiu.interview.service.InterviewMessageService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <h1></h1>
*
* @author qingqiu
* @date 2025/9/21 11:59
*/
@Slf4j
@RestController
@RequestMapping("/interview-message")
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
public class InterviewMessageController {
public final InterviewMessageService service;
@GetMapping("/list-by-session-id/{sessionId}")
public R<List<InterviewMessage>> listBySessionId(@PathVariable String sessionId) {
return R.success(
service.list(
new LambdaQueryWrapper<InterviewMessage>()
.eq(InterviewMessage::getSessionId, sessionId)
.orderByAsc(InterviewMessage::getCreatedTime)
)
);
}
}

View File

@@ -3,12 +3,10 @@ package com.qingqiu.interview.controller;
import com.qingqiu.interview.common.res.R;
import com.qingqiu.interview.dto.QuestionProgressPageParams;
import com.qingqiu.interview.entity.InterviewQuestionProgress;
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;
import org.springframework.web.bind.annotation.*;
/**
* <p>
@@ -19,7 +17,7 @@ import org.springframework.web.bind.annotation.RestController;
* @since 2025-08-30
*/
@RestController
@RequestMapping("/api/v1/interview-question-progress")
@RequestMapping("/interview-question-progress")
@RequiredArgsConstructor
public class InterviewQuestionProgressController {
@@ -27,6 +25,7 @@ public class InterviewQuestionProgressController {
/**
* 面试问题进度列表
*
* @param params 查询参数
* @return data
*/
@@ -35,4 +34,16 @@ public class InterviewQuestionProgressController {
return R.success(service.pageList(params));
}
/**
* 获取面试问题进度详情
*
* @param id id
* @return data
*/
@GetMapping("/{id}")
public R<InterviewQuestionProgress> getDetail(@PathVariable Long id) {
return R.success(service.getById(id));
}
}

View File

@@ -0,0 +1,162 @@
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.QuestionCategoryDTO;
import com.qingqiu.interview.dto.QuestionCategoryPageParams;
import com.qingqiu.interview.entity.QuestionCategory;
import com.qingqiu.interview.service.IQuestionCategoryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/question-category")
@RequiredArgsConstructor
public class QuestionCategoryController {
@Lazy
private final IQuestionCategoryService questionCategoryService;
/**
* 获取分类树列表
*/
@GetMapping("/tree-list")
public R<List<QuestionCategory>> getTreeList() {
List<QuestionCategory> list = questionCategoryService.getTreeList();
return R.success(list);
}
@GetMapping("/question-tree-list")
public R<List<QuestionCategory>> getQuestionTreeList() {
// List<QuestionCategory> list = questionCategoryService.getQuestionTreeList();
return R.success();
}
/**
* 获取分类选项
*/
@GetMapping("/options")
public R<List<QuestionCategory>> getOptions() {
try {
List<QuestionCategory> options = questionCategoryService.getOptions();
return R.success(options);
} catch (Exception e) {
log.error("获取分类选项失败", e);
return R.error("获取分类选项失败");
}
}
/**
* 获取分类详情
*/
@GetMapping("/{id}")
public R<QuestionCategory> getDetail(@PathVariable Long id) {
try {
QuestionCategory category = questionCategoryService.getCategoryDetail(id);
return R.success(category);
} catch (Exception e) {
log.error("获取分类详情失败", e);
return R.error("获取分类详情失败");
}
}
/**
* 分页查询分类
*/
@GetMapping("/page")
public R<Page<QuestionCategory>> getPage(QuestionCategoryPageParams query) {
try {
Page<QuestionCategory> pageR = questionCategoryService.getCategoryPage(query);
return R.success(pageR);
} catch (Exception e) {
log.error("分页查询分类失败", e);
return R.error("分页查询分类失败");
}
}
/**
* 创建分类
*/
@PostMapping
public R<Long> create(@Validated @RequestBody QuestionCategoryDTO dto) {
try {
Long id = questionCategoryService.createCategory(dto);
return R.success(id);
} catch (RuntimeException e) {
log.error("创建分类失败", e);
return R.error(e.getMessage());
} catch (Exception e) {
log.error("创建分类失败", e);
return R.error("创建分类失败");
}
}
/**
* 更新分类
*/
@PostMapping("/update")
public R<Void> update(@RequestBody QuestionCategoryDTO dto) {
try {
questionCategoryService.updateCategory(dto);
return R.success();
} catch (RuntimeException e) {
log.error("更新分类失败", e);
return R.error(e.getMessage());
} catch (Exception e) {
log.error("更新分类失败", e);
return R.error("更新分类失败");
}
}
/**
* 删除分类
*/
@DeleteMapping("/{id}")
public R<Void> delete(@PathVariable Long id) {
try {
questionCategoryService.deleteCategory(id);
return R.success();
} catch (RuntimeException e) {
log.error("删除分类失败", e);
return R.error(e.getMessage());
} catch (Exception e) {
log.error("删除分类失败", e);
return R.error("删除分类失败");
}
}
/**
* 更新分类状态
*/
@PatchMapping("/{id}/state")
public R<Void> updateState(@PathVariable Long id, @RequestParam Integer state) {
try {
questionCategoryService.updateState(id, state);
return R.success();
} catch (Exception e) {
log.error("更新分类状态失败", e);
return R.error("更新分类状态失败");
}
}
/**
* 搜索分类
*/
@GetMapping("/search")
public R<List<QuestionCategory>> search(@RequestParam String name) {
try {
List<QuestionCategory> res = questionCategoryService.searchByName(name);
return R.success(res);
} catch (Exception e) {
log.error("搜索分类失败", e);
return R.error("搜索分类失败");
}
}
}

View File

@@ -3,21 +3,24 @@ 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.QuestionOptionsDTO;
import com.qingqiu.interview.dto.QuestionPageParams;
import com.qingqiu.interview.entity.Question;
import com.qingqiu.interview.service.QuestionService;
import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO;
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;
import java.util.List;
/**
* 题库管理相关接口
*/
@RestController
@RequestMapping("/api/v1/question")
@RequestMapping("/question")
@RequiredArgsConstructor
public class QuestionController {
@@ -76,4 +79,9 @@ public class QuestionController {
questionService.useAiCheckQuestionData();
return R.success();
}
@PostMapping("/tree-list-category")
public R<List<QuestionAndCategoryTreeListVO>> getTreeListCategory(@RequestBody QuestionOptionsDTO dto) {
return R.success(questionService.getTreeListCategory(dto));
}
}

View File

View File

@@ -0,0 +1,28 @@
package com.qingqiu.interview.dto;
import com.qingqiu.interview.common.enums.LLMProvider;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* <h1></h1>
*
* @author qingqiu
* @date 2025/9/18 12:54
*/
@Data
@Accessors(chain = true)
public class ChatDTO {
/** 会话id */
private String sessionId;
/** 调用模型 */
private String aiModel = LLMProvider.DEEPSEEK.getCode();
/** 输入内容 */
private String content;
/** 0 普通会话 1 面试会话 */
private Integer dataType;
/** 角色类型user/assistant/system */
private String role;
}

View File

View File

View File

View File

View File

@@ -1,8 +1,12 @@
package com.qingqiu.interview.dto;
import com.qingqiu.interview.common.enums.LLMProvider;
import com.qingqiu.interview.vo.QuestionAndCategoryTreeListVO;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.util.List;
@Data
public class InterviewStartRequest {
@@ -10,6 +14,21 @@ public class InterviewStartRequest {
private String candidateName;
private List<QuestionAndCategoryTreeListVO> selectedNodes;
@NotBlank(message = "面试类型不能为空")
private String model;
/** 选择的AI模型 */
private String aiModel = LLMProvider.QWEN.getCode();
/** 生成的面试题目数量 */
private Integer totalQuestions = 10;
/**
* 岗位要求
*/
private String jobRequirements;
// 简历文件通过MultipartFile单独传递
}

View File

@@ -0,0 +1,38 @@
package com.qingqiu.interview.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* <h1></h1>
*
* @author huangpeng
* @date 2025/9/11 09:39
*/
@Data
public class QuestionCategoryDTO {
private Long id;
@NotBlank(message = "分类名称不能为空")
private String name;
@NotNull(message = "父级分类ID不能为空")
private Long parentId;
@NotNull(message = "排序不能为空")
private Integer sort;
@NotNull(message = "状态不能为空")
private Integer state;
private String ancestor;
private Integer level;
/**
* 父分类名称(用于前端显示)
*/
private String parentName;
}

View File

@@ -0,0 +1,48 @@
package com.qingqiu.interview.dto;
import com.qingqiu.interview.common.dto.PageBaseParams;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* <h1></h1>
*
* @author huangpeng
* @date 2025/9/11 09:40
*/
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class QuestionCategoryPageParams extends PageBaseParams {
/**
* 分类名称(模糊查询)
*/
private String name;
/**
* 状态0禁用1启用
*/
private Integer state;
/**
* 父级分类ID
*/
private Long parentId;
/**
* 层级
*/
private Integer level;
/**
* 是否包含子分类
*/
private Boolean includeChildren = false;
/**
* 是否只返回启用状态的分类
*/
private Boolean onlyEnabled = false;
}

View File

@@ -0,0 +1,17 @@
package com.qingqiu.interview.dto;
import lombok.Data;
import java.util.List;
@Data
public class QuestionOptionsDTO {
/** 分类id */
private List<Long> categoryIds;
/** 难度 */
private String difficulty;
}

View File

@@ -1,5 +1,6 @@
package com.qingqiu.interview.dto;
import com.qingqiu.interview.common.dto.PageBaseParams;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@@ -8,7 +9,9 @@ import lombok.experimental.Accessors;
@Data
@EqualsAndHashCode(callSuper = true)
@Accessors(chain = true)
public class QuestionPageParams extends PageBaseParams{
public class QuestionPageParams extends PageBaseParams {
private String content;
private Long categoryId;
}

View File

@@ -1,5 +1,6 @@
package com.qingqiu.interview.dto;
import com.qingqiu.interview.common.dto.PageBaseParams;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@@ -7,6 +8,6 @@ import lombok.experimental.Accessors;
@EqualsAndHashCode(callSuper = true)
@Data
@Accessors(chain = true)
public class QuestionProgressPageParams extends PageBaseParams{
public class QuestionProgressPageParams extends PageBaseParams {
private String questionName;
}

View File

View File

View File

@@ -0,0 +1,43 @@
package com.qingqiu.interview.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serial;
import java.io.Serializable;
/**
* <h1>
* 开始面试请求的数据传输对象
* </h1>
*
* @author qingqiu
* @date 2025/9/19 16:03
*/
@Data
@Accessors(chain = true)
public class StartInterviewDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 候选人姓名
*/
private String candidateName;
/**
* 简历完整内容或简历文件URL
*/
private String resumeContent;
/**
* 指定使用的AI模型
*/
private String aiModel;
/**
* 计划提问总数
*/
private Integer totalQuestions;
}

View File

@@ -0,0 +1,33 @@
package com.qingqiu.interview.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serial;
import java.io.Serializable;
/**
* <h1></h1>
*
* @author qingqiu
* @date 2025/9/19 16:04
*/
@Data
@Accessors(chain = true)
public class SubmitAnswerDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String sessionId;
/**
* 当前问题的进度ID (interview_question_progress.id)
*/
private Long progressId;
/**
* 用户的回答内容
*/
private String answer;
}

View File

@@ -5,6 +5,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
@@ -22,6 +23,7 @@ import java.time.LocalDateTime;
@TableName("ai_session_log")
public class AiSessionLog implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
@@ -32,6 +34,11 @@ public class AiSessionLog implements Serializable {
*/
private String role;
/**
* 数据类型 0 普通会话 1 面试会话
*/
private Integer dataType;
/**
* 输入内容
*/
@@ -54,5 +61,8 @@ public class AiSessionLog implements Serializable {
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
@TableLogic
private Integer deleted;
}

View File

View File

@@ -28,8 +28,8 @@ public class InterviewMessage {
@TableField("content")
private String content;
@TableField("question_id")
private Long questionId;
@TableField("question_progress_id")
private Long questionProgressId;
@TableField("message_order")
private Integer messageOrder;

View File

@@ -33,6 +33,15 @@ public class InterviewQuestionProgress {
@TableField("question_content")
private String questionContent;
/** 问题序号 */
private Integer questionIndex;
/** 答题耗时(秒) */
private Long timeTaken;
/** 详细评估信息 */
private String evaluationDetails;
/**
* 面试会话ID
*/

View File

@@ -31,11 +31,24 @@ public class InterviewSession implements Serializable {
@TableField("resume_content")
private String resumeContent;
@TableField("job_requirements")
private String jobRequirements;
@TableField("extracted_skills")
private String extractedSkills;
@TableField("interview_type")
private String interviewType;
@TableField("estimated_duration")
private Integer estimatedDuration;
@TableField("current_question_id")
private Long currentQuestionId;
@TableField("ai_model")
private String aiModel;
@TableField("model")
private String model;
@TableField("status")
private String status;

View File

@@ -19,8 +19,11 @@ public class Question {
@TableField("content")
private String content;
@TableField("category")
private String category;
@TableField("category_id")
private Long categoryId;
@TableField("category_name")
private String categoryName;
@TableField("difficulty")
private String difficulty;

View File

@@ -1,14 +1,14 @@
package com.qingqiu.interview.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.time.LocalDateTime;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* <p>
* 题型分类
@@ -25,7 +25,7 @@ public class QuestionCategory implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
@TableId(type = IdType.AUTO)
private Long id;
/**
@@ -33,16 +33,60 @@ public class QuestionCategory implements Serializable {
*/
private String name;
/**
* 上级id
*/
private Long parentId;
/**
* 层级
*/
private Integer level;
/**
* 上级序列
*/
private String ancestor;
/**
* 排序
*/
private Integer sort;
/**
* 状态 0 禁用 1 启用
*/
private Integer state;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updatedTime;
@TableLogic
private Integer deleted;
/**
* 子分类列表(非数据库字段)
*/
@TableField(exist = false)
private List<QuestionCategory> children;
/**
* 子分类数量(非数据库字段)
*/
@TableField(exist = false)
private Integer childrenCount;
/**
* 父分类名称(非数据库字段,用于显示)
*/
@TableField(exist = false)
private String parentName;
}

View File

@@ -0,0 +1,11 @@
package com.qingqiu.interview.entity.ai;
import com.fasterxml.jackson.annotation.JsonProperty;
public record EvaluateAiRes(@JsonProperty("feedback") String feedback,
@JsonProperty("suggestions") String suggestions,
@JsonProperty("aiAnswer") String aiAnswer,
@JsonProperty("score") double score,
@JsonProperty("continueAsking") boolean continueAsking,
@JsonProperty("followUpQuestion") String followUpQuestion) {
}

View File

@@ -0,0 +1,10 @@
package com.qingqiu.interview.entity.ai;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public record ExtractSkillAiRes(
@JsonProperty("skills") List<String> skills
) {
}

View File

@@ -0,0 +1,27 @@
package com.qingqiu.interview.entity.ai;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
public record InterviewReportAiRes(
@JsonProperty("overallScore") String overallScore,
@JsonProperty("overallFeedback") String overallFeedback,
@JsonProperty("technicalAssessment") Map<String, String> technicalAssessment,
@JsonProperty("strengthsAndWeaknesses") StrengthsAndWeaknesses strengthsAndWeaknesses,
@JsonProperty("suggestions") List<String> suggestions,
@JsonProperty("hiringRecommendation") String hiringRecommendation,
@JsonProperty("hiringReason") String hiringReason
) {
/**
* 内部 Record对应 "strengthsAndWeaknesses" 对象。
*/
public record StrengthsAndWeaknesses(
@JsonProperty("strengths") List<String> strengths,
@JsonProperty("weaknesses") List<String> weaknesses
) {}
}

View File

@@ -0,0 +1,26 @@
package com.qingqiu.interview.entity.ai;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class QuestionAiRes {
/**
* 内部实体类:对应 JSON 数组中的每个元素。
* { "id": "ai-gen-1", "content": "问题内容..." }
*/
public record Question(
@JsonProperty("id") String id,
@JsonProperty("content") String content
) {}
/**
* 外部实体类:对应整个 JSON 响应的顶层结构。
* { "questions": [...] }
*/
public record Wrapper(
@JsonProperty("questions") List<Question> questions
) {}
}

View File

@@ -0,0 +1,22 @@
package com.qingqiu.interview.entity.ai;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/**
* @program: ai-interview
* @description: 题型分类ai返回
* @author: huangpeng
* @create: 2025-11-06 20:08
**/
public class QuestionClassificationAiRes {
public record Item(@JsonProperty("content") String content,
@JsonProperty("category") String category,
@JsonProperty("difficulty") String difficulty,
@JsonProperty("tags") String tags) {}
public record Wrapper(@JsonProperty("questions")List<Item> questions) {}
}

View File

@@ -0,0 +1,18 @@
package com.qingqiu.interview.entity.ai;
import com.fasterxml.jackson.annotation.JsonProperty;
import redis.clients.jedis.graph.Statistics;
import java.util.List;
/**
* @program: ai-interview
* @description: 题目去重AI返回结果
* @author: huangpeng
* @create: 2025-11-07 10:37
**/
public record QuestionDeduplicationAiRes(
@JsonProperty("questionIds") String questionIds
) {
}

View File

View File

View File

View File

@@ -2,6 +2,9 @@ package com.qingqiu.interview.mapper;
import com.qingqiu.interview.entity.QuestionCategory;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* <p>
@@ -12,5 +15,5 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
* @since 2025-09-08
*/
public interface QuestionCategoryMapper extends BaseMapper<QuestionCategory> {
List<QuestionCategory> batchFindByAncestorIdsUnion(@Param("searchIds") List<Long> searchIds);
}

View File

@@ -1,6 +1,9 @@
package com.qingqiu.interview.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qingqiu.interview.dto.DashboardStatsResponse;
import com.qingqiu.interview.dto.QuestionPageParams;
import com.qingqiu.interview.entity.Question;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -18,6 +21,9 @@ public interface QuestionMapper extends BaseMapper<Question> {
Question selectByContent(@Param("content") String content);
List<com.qingqiu.interview.dto.DashboardStatsResponse.CategoryStat> countByCategory();
List<DashboardStatsResponse.CategoryStat> countByCategory();
Page<Question> queryPage(@Param("page") Page<Question> page, @Param("params") QuestionPageParams params);
}

View File

@@ -1,59 +1,15 @@
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;
/**
* @program: ai-interview
* @description: 工作台接口
* @author: huangpeng
* @create: 2025-11-07 14:54
**/
public interface DashboardService {
@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());
}
DashboardStatsResponse getDashboardStats();
}

View File

View File

@@ -15,4 +15,6 @@ import com.qingqiu.interview.entity.InterviewQuestionProgress;
*/
public interface IInterviewQuestionProgressService extends IService<InterviewQuestionProgress> {
Page<InterviewQuestionProgress> pageList(QuestionProgressPageParams params);
InterviewQuestionProgress getNextQuestion(String sessionId);
}

View File

@@ -1,7 +1,12 @@
package com.qingqiu.interview.service;
import com.qingqiu.interview.entity.QuestionCategory;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.qingqiu.interview.dto.QuestionCategoryDTO;
import com.qingqiu.interview.dto.QuestionCategoryPageParams;
import com.qingqiu.interview.entity.QuestionCategory;
import java.util.List;
/**
* <p>
@@ -12,5 +17,59 @@ import com.baomidou.mybatisplus.extension.service.IService;
* @since 2025-09-08
*/
public interface IQuestionCategoryService extends IService<QuestionCategory> {
/**
* 获取分类树列表
*/
List<QuestionCategory> getTreeList();
/**
* 获取分类选项(用于下拉选择)
*/
List<QuestionCategory> getOptions();
/**
* 创建分类
*/
Long createCategory(QuestionCategoryDTO dto);
/**
* 更新分类
*/
void updateCategory(QuestionCategoryDTO dto);
/**
* 删除分类
*/
void deleteCategory(Long id);
/**
* 更新分类状态
*/
void updateState(Long id, Integer state);
/**
* 获取分类详情
*/
QuestionCategory getCategoryDetail(Long id);
/**
* 分页查询分类
*/
Page<QuestionCategory> getCategoryPage(QuestionCategoryPageParams query);
/**
* 根据名称搜索分类
*/
List<QuestionCategory> searchByName(String name);
/**
* 检查分类名称是否重复
*/
boolean checkNameExists(String name, Long parentId, Long excludeId);
List<QuestionCategory> batchFindByAncestorIdsUnion(List<Long> searchIds);
}

View File

@@ -0,0 +1,63 @@
package com.qingqiu.interview.service;
import com.qingqiu.interview.entity.InterviewQuestionProgress;
import com.qingqiu.interview.entity.InterviewSession;
import com.qingqiu.interview.entity.Question;
import com.qingqiu.interview.entity.ai.EvaluateAiRes;
import com.qingqiu.interview.entity.ai.InterviewReportAiRes;
import com.qingqiu.interview.entity.ai.QuestionAiRes;
import java.util.List;
/**
* <h1>
* 面试接入AI的接口
* </h1>
*
* @author qingqiu
* @date 2025/9/19 16:48
*/
public interface InterviewAiService {
/**
* 从简历内容中提取技能列表
*
* @param resumeContent 简历文本
* @return 包含技能列表的JSON对象
*/
List<String> extractSkillsFromResume(String resumeContent);
/**
* 根据技能动态生成面试题目
*
* @param skills 技能列表
* @param resumeContent 简历内容
* @param count 需要生成的题目数量
* @return 包含问题列表的JSON对象
*/
List<QuestionAiRes.Question> generateQuestionsOfAi(String sessionId, List<String> skills, String jobRequirements, String resumeContent, int count);
List<QuestionAiRes.Question> generateQuestionOfLocal(String sessionId, List<Question> questions, List<String> skills, String jobRequirements, String resumeContent, int count);
/**
* 评估用户的回答
*
* @param question 问题内容
* @param userAnswer 用户的回答
* @param context 可选的上下文(之前的问答历史)
* @return 包含评估结果的JSON对象
*/
EvaluateAiRes evaluateAnswer(String sessionId, String question, String userAnswer, List<InterviewQuestionProgress> context);
/**
* 生成最终的面试评估报告
*
* @param session 面试会话信息
* @param progressList 整个面试的问答记录
* @return 包含最终报告的JSON对象
*/
InterviewReportAiRes generateFinalReport(InterviewSession session, List<InterviewQuestionProgress> progressList);
String generateFirstQuestion(String sessionId, String candidateName, String questionContent);
}

View File

@@ -0,0 +1,17 @@
package com.qingqiu.interview.service;
import com.qingqiu.interview.dto.InterviewStartRequest;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
/**
* <h1></h1>
*
* @author qingqiu
* @date 2025/9/18 16:37
*/
public interface InterviewChatService {
void startInterview(MultipartFile resume, InterviewStartRequest request) throws IOException;
}

Some files were not shown because too many files have changed in this diff Show More