Compare commits
12 Commits
704ea4ab7b
...
alibaba-re
| Author | SHA1 | Date | |
|---|---|---|---|
| b08a3988f7 | |||
| 80dcb23bbc | |||
| 5fb4ed754c | |||
| fac1346104 | |||
| 5711d611f2 | |||
| d3b5ca0033 | |||
| df5aa0b9c6 | |||
| 08b4b8b206 | |||
| 8b357fbb93 | |||
| a384bbfd16 | |||
| 7f24d65d76 | |||
| d14b46d007 |
0
.gitignore
vendored
Normal file → Executable file
0
.gitignore
vendored
Normal file → Executable file
27
HELP.md
27
HELP.md
@@ -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.
|
||||
|
||||
@@ -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
332
docs/B端需求文档.md
Normal 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 岗位维护
|
||||
|
||||
可以:
|
||||
|
||||
- 新增岗位
|
||||
- 编辑岗位
|
||||
- 删除 / 禁用岗位
|
||||
- 配置技能标签(多个)
|
||||
- 配置难度(1–5)
|
||||
|
||||
------
|
||||
|
||||
# **3.3 题库管理模块**
|
||||
|
||||
## 3.3.1 题目列表
|
||||
|
||||
展示:
|
||||
|
||||
- 题目内容(前 30 字)
|
||||
- 所属岗位
|
||||
- 难度
|
||||
- 类型(开放题、多选题等)
|
||||
- 使用次数
|
||||
- 创建人
|
||||
- 状态(有效/无效)
|
||||
|
||||
## 3.3.2 新增题目
|
||||
|
||||
字段:
|
||||
|
||||
- 问题内容(文本)
|
||||
- 示例答案
|
||||
- 难度
|
||||
- 标签(可多选)
|
||||
- 所属岗位
|
||||
- question_type:open / 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
266
docs/C端需求文档.md
Normal 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 |
|
||||
| 工作年限 | 0–20+ |
|
||||
| 当前职位 | ≤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 个主问题,可 1–2 个追问 |
|
||||
| 文本回答 | 输入框 |
|
||||
| 语音回答 | 录音上传,ASR 转写 |
|
||||
| 进度条 | x / N 题 |
|
||||
| 跳过功能 | 可跳过 1 次 |
|
||||
| 中断 | 用户主动退出或异常断开 |
|
||||
|
||||
### 界面区块
|
||||
|
||||
- 左侧:问题内容
|
||||
- 中间:答题区
|
||||
- 下方:进度 & 操作按钮
|
||||
|
||||
------
|
||||
|
||||
## 4.5 面试结果与复盘
|
||||
|
||||
### 4.5.1 面试列表
|
||||
|
||||
字段:
|
||||
|
||||
- 总分
|
||||
- 岗位
|
||||
- 时间
|
||||
- 面试模式
|
||||
- 标签(优秀、一般、需提升)
|
||||
|
||||
### 4.5.2 面试详情
|
||||
|
||||
展示:
|
||||
|
||||
- 每题内容与用户原回答
|
||||
- 文本/音频回放
|
||||
- 评分维度(逻辑、表达、深度、专业)
|
||||
- AI 点评
|
||||
|
||||
------
|
||||
|
||||
### 4.5.3 复盘报告(核心)
|
||||
|
||||
报告包含以下结构:
|
||||
|
||||
| 模块 | 内容 |
|
||||
| ------------ | ---------- |
|
||||
| 综合评分 | 0–10 |
|
||||
| 综合评语 | 200–300 字 |
|
||||
| 优势总结 | 3–5 条 |
|
||||
| 待改进点 | 3–5 条 |
|
||||
| 行为建议 | 3–7 条 |
|
||||
| 推荐学习方向 | 动态生成 |
|
||||
|
||||
支持下载 / 分享卡片(图像形式)。
|
||||
|
||||
------
|
||||
|
||||
## 4.6 学习中心
|
||||
|
||||
内容:
|
||||
|
||||
- 岗位相关题库
|
||||
- 常见行为面试题(STAR 法则)
|
||||
- 面试技巧教程
|
||||
- 视频/图文内容(可选)
|
||||
|
||||
------
|
||||
|
||||
## 4.7 通用功能
|
||||
|
||||
### 4.7.1 文件上传
|
||||
|
||||
- 支持音频上传(WAV/MP3)
|
||||
- 支持头像、简历、图片上传
|
||||
|
||||
### 4.7.2 消息中心
|
||||
|
||||
- 面试报告生成通知
|
||||
- 系统公告
|
||||
|
||||
### 4.7.3 设置
|
||||
|
||||
- 修改手机号
|
||||
- 注销账号
|
||||
|
||||
------
|
||||
|
||||
# 5. 页面结构(信息架构 IA)
|
||||
|
||||
```
|
||||
首页
|
||||
├─ 推荐岗位
|
||||
├─ 开始面试
|
||||
用户中心
|
||||
├─ 基本资料
|
||||
├─ 简历管理
|
||||
岗位
|
||||
├─ 岗位列表
|
||||
├─ 岗位详情
|
||||
模拟面试
|
||||
├─ 创建面试
|
||||
├─ 面试问答页
|
||||
面试记录
|
||||
├─ 面试列表
|
||||
├─ 面试详情
|
||||
└─ 复盘报告
|
||||
学习中心
|
||||
├─ 题库练习
|
||||
├─ 面试技巧
|
||||
```
|
||||
|
||||
------
|
||||
|
||||
# 6. 非功能性要求(C 端)
|
||||
|
||||
- 面试响应 ≤ 200ms
|
||||
- 报告生成 ≤ 10 秒
|
||||
- 音频上传失败重试机制
|
||||
139
pom.xml
Normal file → Executable file
139
pom.xml
Normal file → Executable 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>
|
||||
|
||||
<!-- <!– Spring AI Dependencies –>-->
|
||||
<!-- 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> <!– 或最新版本 –>-->
|
||||
<!-- <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
0
sql/.idea/.gitignore
generated
vendored
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/AiInterviewApplication.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/AiInterviewApplication.java
Normal file → Executable 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;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.qingqiu.interview.ai.factory;
|
||||
|
||||
import com.qingqiu.interview.ai.service.AIClientService;
|
||||
|
||||
public interface AIClientFactory {
|
||||
AIClientService createAIClient();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
0
src/main/java/com/qingqiu/interview/common/constants/AIStrategyConstant.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/constants/AIStrategyConstant.java
Normal file → Executable 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;
|
||||
}
|
||||
17
src/main/java/com/qingqiu/interview/common/constants/CommonConstant.java
Executable file
17
src/main/java/com/qingqiu/interview/common/constants/CommonConstant.java
Executable 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");
|
||||
}
|
||||
0
src/main/java/com/qingqiu/interview/common/constants/QwenModelConstant.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/constants/QwenModelConstant.java
Normal file → Executable file
2
src/main/java/com/qingqiu/interview/dto/PageBaseParams.java → src/main/java/com/qingqiu/interview/common/dto/PageBaseParams.java
Normal file → Executable file
2
src/main/java/com/qingqiu/interview/dto/PageBaseParams.java → src/main/java/com/qingqiu/interview/common/dto/PageBaseParams.java
Normal file → Executable file
@@ -1,4 +1,4 @@
|
||||
package com.qingqiu.interview.dto;
|
||||
package com.qingqiu.interview.common.dto;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
63
src/main/java/com/qingqiu/interview/common/enums/CommonStateEnum.java
Executable file
63
src/main/java/com/qingqiu/interview/common/enums/CommonStateEnum.java
Executable 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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
0
src/main/java/com/qingqiu/interview/common/ex/ApiException.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/ex/ApiException.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/ex/GlobalErrorHandler.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/ex/GlobalErrorHandler.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/res/IErrorCode.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/res/IErrorCode.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/res/R.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/res/R.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/res/ResultCode.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/res/ResultCode.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/service/HttpService.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/service/HttpService.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/service/impl/HttpServiceImpl.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/common/service/impl/HttpServiceImpl.java
Normal file → Executable 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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.qingqiu.interview.common.utils;
|
||||
|
||||
public class UUIDUtils {
|
||||
|
||||
public static String getUUID() {
|
||||
return java.util.UUID.randomUUID().toString().replace("-", "");
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
22
src/main/java/com/qingqiu/interview/config/CorsConfig.java
Normal file
22
src/main/java/com/qingqiu/interview/config/CorsConfig.java
Normal 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秒内,不需要再发送预检验请求,可以缓存该结果
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
114
src/main/java/com/qingqiu/interview/config/DashScopeConfig.java
Normal file → Executable file
114
src/main/java/com/qingqiu/interview/config/DashScopeConfig.java
Normal file → Executable 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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
0
src/main/java/com/qingqiu/interview/config/JacksonConfig.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/config/JacksonConfig.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/config/MyBatisPlusConfig.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/config/MyBatisPlusConfig.java
Normal file → Executable file
100
src/main/java/com/qingqiu/interview/config/OpenAiChatConfig.java
Normal file
100
src/main/java/com/qingqiu/interview/config/OpenAiChatConfig.java
Normal 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();
|
||||
}
|
||||
}
|
||||
32
src/main/java/com/qingqiu/interview/config/RedisConfig.java
Normal file
32
src/main/java/com/qingqiu/interview/config/RedisConfig.java
Normal 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;
|
||||
}
|
||||
}
|
||||
0
src/main/java/com/qingqiu/interview/config/WebClientConfig.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/config/WebClientConfig.java
Normal file → Executable file
29
src/main/java/com/qingqiu/interview/controller/AiSessionLogController.java
Normal file → Executable file
29
src/main/java/com/qingqiu/interview/controller/AiSessionLogController.java
Normal file → Executable 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())
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
2
src/main/java/com/qingqiu/interview/controller/DashboardController.java
Normal file → Executable file
2
src/main/java/com/qingqiu/interview/controller/DashboardController.java
Normal file → Executable file
@@ -12,7 +12,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
* 仪表盘数据统计接口
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/dashboard")
|
||||
@RequestMapping("/dashboard")
|
||||
@RequiredArgsConstructor
|
||||
public class DashboardController {
|
||||
|
||||
|
||||
@@ -1,58 +1,133 @@
|
||||
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()));
|
||||
}
|
||||
}
|
||||
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 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("/interview")
|
||||
@RequiredArgsConstructor(onConstructor_ = {@Autowired, @Lazy})
|
||||
public class InterviewController {
|
||||
|
||||
private final InterviewService interviewService;
|
||||
|
||||
|
||||
/**
|
||||
* 开始面试
|
||||
*
|
||||
* @return 包含会话ID的会话信息
|
||||
*/
|
||||
@PostMapping("/start")
|
||||
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 下一个问题
|
||||
*/
|
||||
@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 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/{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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
21
src/main/java/com/qingqiu/interview/controller/InterviewQuestionProgressController.java
Normal file → Executable file
21
src/main/java/com/qingqiu/interview/controller/InterviewQuestionProgressController.java
Normal file → Executable 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));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
162
src/main/java/com/qingqiu/interview/controller/QuestionCategoryController.java
Executable file
162
src/main/java/com/qingqiu/interview/controller/QuestionCategoryController.java
Executable 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("搜索分类失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
10
src/main/java/com/qingqiu/interview/controller/QuestionController.java
Normal file → Executable file
10
src/main/java/com/qingqiu/interview/controller/QuestionController.java
Normal file → Executable 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));
|
||||
}
|
||||
}
|
||||
|
||||
0
src/main/java/com/qingqiu/interview/dto/ApiResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/ApiResponse.java
Normal file → Executable file
28
src/main/java/com/qingqiu/interview/dto/ChatDTO.java
Normal file
28
src/main/java/com/qingqiu/interview/dto/ChatDTO.java
Normal 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;
|
||||
}
|
||||
0
src/main/java/com/qingqiu/interview/dto/ChatRequest.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/ChatRequest.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/DashboardStatsResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/DashboardStatsResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/InterviewReportResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/InterviewReportResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/InterviewResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/InterviewResponse.java
Normal file → Executable file
23
src/main/java/com/qingqiu/interview/dto/InterviewStartRequest.java
Normal file → Executable file
23
src/main/java/com/qingqiu/interview/dto/InterviewStartRequest.java
Normal file → Executable file
@@ -1,15 +1,34 @@
|
||||
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 {
|
||||
|
||||
@NotBlank(message = "候选人姓名不能为空")
|
||||
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单独传递
|
||||
}
|
||||
|
||||
38
src/main/java/com/qingqiu/interview/dto/QuestionCategoryDTO.java
Executable file
38
src/main/java/com/qingqiu/interview/dto/QuestionCategoryDTO.java
Executable 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;
|
||||
}
|
||||
48
src/main/java/com/qingqiu/interview/dto/QuestionCategoryPageParams.java
Executable file
48
src/main/java/com/qingqiu/interview/dto/QuestionCategoryPageParams.java
Executable 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
5
src/main/java/com/qingqiu/interview/dto/QuestionPageParams.java
Normal file → Executable file
5
src/main/java/com/qingqiu/interview/dto/QuestionPageParams.java
Normal file → Executable 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;
|
||||
}
|
||||
|
||||
3
src/main/java/com/qingqiu/interview/dto/QuestionProgressPageParams.java
Normal file → Executable file
3
src/main/java/com/qingqiu/interview/dto/QuestionProgressPageParams.java
Normal file → Executable 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;
|
||||
}
|
||||
|
||||
0
src/main/java/com/qingqiu/interview/dto/SessionHistoryResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/SessionHistoryResponse.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/SessionRequest.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/dto/SessionRequest.java
Normal file → Executable 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;
|
||||
}
|
||||
33
src/main/java/com/qingqiu/interview/dto/SubmitAnswerDTO.java
Normal file
33
src/main/java/com/qingqiu/interview/dto/SubmitAnswerDTO.java
Normal 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;
|
||||
}
|
||||
10
src/main/java/com/qingqiu/interview/entity/AiSessionLog.java
Normal file → Executable file
10
src/main/java/com/qingqiu/interview/entity/AiSessionLog.java
Normal file → Executable 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;
|
||||
|
||||
|
||||
}
|
||||
|
||||
0
src/main/java/com/qingqiu/interview/entity/InterviewEvaluation.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/entity/InterviewEvaluation.java
Normal file → Executable file
4
src/main/java/com/qingqiu/interview/entity/InterviewMessage.java
Normal file → Executable file
4
src/main/java/com/qingqiu/interview/entity/InterviewMessage.java
Normal file → Executable 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;
|
||||
|
||||
9
src/main/java/com/qingqiu/interview/entity/InterviewQuestionProgress.java
Normal file → Executable file
9
src/main/java/com/qingqiu/interview/entity/InterviewQuestionProgress.java
Normal file → Executable file
@@ -33,6 +33,15 @@ public class InterviewQuestionProgress {
|
||||
@TableField("question_content")
|
||||
private String questionContent;
|
||||
|
||||
/** 问题序号 */
|
||||
private Integer questionIndex;
|
||||
|
||||
/** 答题耗时(秒) */
|
||||
private Long timeTaken;
|
||||
|
||||
/** 详细评估信息 */
|
||||
private String evaluationDetails;
|
||||
|
||||
/**
|
||||
* 面试会话ID
|
||||
*/
|
||||
|
||||
13
src/main/java/com/qingqiu/interview/entity/InterviewSession.java
Normal file → Executable file
13
src/main/java/com/qingqiu/interview/entity/InterviewSession.java
Normal file → Executable 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;
|
||||
|
||||
7
src/main/java/com/qingqiu/interview/entity/Question.java
Normal file → Executable file
7
src/main/java/com/qingqiu/interview/entity/Question.java
Normal file → Executable 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;
|
||||
|
||||
58
src/main/java/com/qingqiu/interview/entity/QuestionCategory.java
Normal file → Executable file
58
src/main/java/com/qingqiu/interview/entity/QuestionCategory.java
Normal file → Executable 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;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
0
src/main/java/com/qingqiu/interview/mapper/AiSessionLogMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/AiSessionLogMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewEvaluationMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewEvaluationMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewMessageMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewMessageMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewQuestionProgressMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewQuestionProgressMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewSessionMapper.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/mapper/InterviewSessionMapper.java
Normal file → Executable file
5
src/main/java/com/qingqiu/interview/mapper/QuestionCategoryMapper.java
Normal file → Executable file
5
src/main/java/com/qingqiu/interview/mapper/QuestionCategoryMapper.java
Normal file → Executable 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);
|
||||
}
|
||||
|
||||
8
src/main/java/com/qingqiu/interview/mapper/QuestionMapper.java
Normal file → Executable file
8
src/main/java/com/qingqiu/interview/mapper/QuestionMapper.java
Normal file → Executable 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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@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());
|
||||
}
|
||||
}
|
||||
package com.qingqiu.interview.service;
|
||||
|
||||
|
||||
import com.qingqiu.interview.dto.DashboardStatsResponse;
|
||||
|
||||
/**
|
||||
* @program: ai-interview
|
||||
* @description: 工作台接口
|
||||
* @author: huangpeng
|
||||
* @create: 2025-11-07 14:54
|
||||
**/
|
||||
public interface DashboardService {
|
||||
|
||||
DashboardStatsResponse getDashboardStats();
|
||||
}
|
||||
|
||||
0
src/main/java/com/qingqiu/interview/service/IAiSessionLogService.java
Normal file → Executable file
0
src/main/java/com/qingqiu/interview/service/IAiSessionLogService.java
Normal file → Executable file
2
src/main/java/com/qingqiu/interview/service/IInterviewQuestionProgressService.java
Normal file → Executable file
2
src/main/java/com/qingqiu/interview/service/IInterviewQuestionProgressService.java
Normal file → Executable 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);
|
||||
}
|
||||
|
||||
61
src/main/java/com/qingqiu/interview/service/IQuestionCategoryService.java
Normal file → Executable file
61
src/main/java/com/qingqiu/interview/service/IQuestionCategoryService.java
Normal file → Executable 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);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user