fix: [临时提交]

This commit is contained in:
ovo 2025-04-29 00:18:39 +08:00
parent 37a7af487b
commit bebac77236
39 changed files with 1344 additions and 49 deletions

View File

@ -0,0 +1,61 @@
import request from '@/utils/request'
import type { Exam } from '@/types/api'
// Define interface for query parameters
export interface ExamQueryParams {
page?: number
size?: number
title?: string
timeLimit?: number
}
// Define interface for the paginated response
export interface ExamListResponse {
list: Exam[]
total: number
pageNum?: number
pageSize?: number
}
// Get exam list (paginated)
export function getExamList(params: ExamQueryParams) {
return request<ExamListResponse>({
url: '/admin/exams',
method: 'get',
params
})
}
// Get single exam details
export function getExam(id: string) {
return request<Exam>({
url: `/admin/exams/${id}`,
method: 'get'
})
}
// Create a new exam
export function createExam(data: Partial<Exam>) {
return request({
url: '/admin/exams',
method: 'post',
data
})
}
// Update an existing exam
export function updateExam(id: string, data: Partial<Exam>) {
return request({
url: `/admin/exams/${id}`,
method: 'put',
data
})
}
// Delete an exam
export function deleteExam(id: string) {
return request({
url: `/admin/exams/${id}`,
method: 'delete'
})
}

View File

@ -4,6 +4,7 @@ import AdminLayout from '@/layouts/AdminLayout.vue'
import DashboardView from '@/views/DashboardView.vue'
import CategoryListView from '@/views/category/CategoryListView.vue'
import CourseListView from '@/views/course/CourseListView.vue'
import ExamListView from '@/views/exam/index.vue'
// Restore the routes array definition
const routes: Array<RouteRecordRaw> = [
@ -29,6 +30,12 @@ const routes: Array<RouteRecordRaw> = [
name: 'CourseList',
component: CourseListView,
meta: { title: '课程管理', icon: 'Reading' }
},
{
path: 'exams',
name: 'ExamList',
component: ExamListView,
meta: { title: '考试管理', icon: 'Document' }
}
]
},

View File

@ -1 +1,11 @@
// Exam entity type definition
export interface Exam {
id: string
title: string
timeLimit: number
startTime: string
endTime: string
totalTime: number
totalScore: number
qualifyScore: number
}

View File

@ -3,7 +3,7 @@ import { ElMessage } from 'element-plus'
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // API 基础路径,稍后在 .env 文件中配置
baseURL: 'http://localhost:8084' || '/api', // API 基础路径,稍后在 .env 文件中配置
timeout: 10000, // 请求超时时间
})

View File

@ -0,0 +1,306 @@
<template>
<div class="exam-container">
<div class="filter-container">
<el-form :inline="true" :model="queryParams" class="demo-form-inline">
<el-form-item label="考试名称">
<el-input v-model="queryParams.title" placeholder="请输入考试名称" clearable />
</el-form-item>
<el-form-item label="是否限时">
<el-select v-model="queryParams.timeLimit" placeholder="请选择" clearable>
<el-option :value="1" label="是" />
<el-option :value="0" label="否" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="action-container">
<el-button type="primary" @click="handleAdd">新增考试</el-button>
</div>
<el-table v-loading="loading" :data="examList" border style="width: 100%">
<el-table-column prop="id" label="ID" width="150" />
<el-table-column prop="title" label="考试名称" min-width="120" />
<el-table-column label="是否限时" width="100">
<template #default="scope">
{{ scope.row.timeLimit === 1 ? '是' : '否' }}
</template>
</el-table-column>
<el-table-column label="考试时间" min-width="280">
<template #default="scope">
<span v-if="scope.row.timeLimit === 1">
{{ formatDateTime(scope.row.startTime) }} {{ formatDateTime(scope.row.endTime) }}
</span>
<span v-else>
{{ scope.row.totalTime }} 分钟
</span>
</template>
</el-table-column>
<el-table-column prop="totalScore" label="考试总分" width="100" />
<el-table-column prop="qualifyScore" label="及格分数" width="100" />
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">编辑</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-container">
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.size"
:page-sizes="[10, 20, 30, 50]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
<!-- 添加/编辑对话框 -->
<el-dialog
:title="dialogTitle"
v-model="dialogVisible"
width="600px"
append-to-body
>
<el-form
ref="examFormRef"
:model="formData"
:rules="rules"
label-width="120px"
>
<el-form-item label="考试名称" prop="title">
<el-input v-model="formData.title" placeholder="请输入考试名称" />
</el-form-item>
<el-form-item label="是否限时" prop="timeLimit">
<el-radio-group v-model="formData.timeLimit">
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
<template v-if="formData.timeLimit === 1">
<el-form-item label="开始时间" prop="startTime">
<el-date-picker
v-model="formData.startTime"
type="datetime"
placeholder="选择开始时间"
/>
</el-form-item>
<el-form-item label="结束时间" prop="endTime">
<el-date-picker
v-model="formData.endTime"
type="datetime"
placeholder="选择结束时间"
/>
</el-form-item>
</template>
<template v-else>
<el-form-item label="考试时长(分钟)" prop="totalTime">
<el-input-number v-model="formData.totalTime" :min="1" />
</el-form-item>
</template>
<el-form-item label="考试总分" prop="totalScore">
<el-input-number v-model="formData.totalScore" :min="1" />
</el-form-item>
<el-form-item label="及格分数" prop="qualifyScore">
<el-input-number
v-model="formData.qualifyScore"
:min="1"
:max="formData.totalScore || 100"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox, FormInstance } from 'element-plus'
import { getExamList, getExam, createExam, updateExam, deleteExam } from '@/api/exam'
import type { Exam, ExamQueryParams } from '@/api/exam'
//
const loading = ref(false)
const examList = ref<Exam[]>([])
const total = ref(0)
const dialogVisible = ref(false)
const dialogType = ref<'add' | 'edit'>('add')
const examFormRef = ref<FormInstance>()
//
const queryParams = reactive<ExamQueryParams>({
page: 1,
size: 10,
title: '',
timeLimit: undefined
})
//
const defaultFormData = {
id: '',
title: '',
timeLimit: 1,
startTime: '',
endTime: '',
totalTime: 90,
totalScore: 100,
qualifyScore: 60
}
const formData = reactive<Partial<Exam>>({ ...defaultFormData })
//
const rules = reactive({
title: [{ required: true, message: '请输入考试名称', trigger: 'blur' }],
timeLimit: [{ required: true, message: '请选择是否限时', trigger: 'change' }],
startTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
endTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
totalTime: [{ required: true, message: '请输入考试时长', trigger: 'blur' }],
totalScore: [{ required: true, message: '请输入考试总分', trigger: 'blur' }],
qualifyScore: [{ required: true, message: '请输入及格分数', trigger: 'blur' }]
})
//
const dialogTitle = computed(() => {
return dialogType.value === 'add' ? '添加考试' : '编辑考试'
})
//
onMounted(() => {
fetchExamList()
})
//
const fetchExamList = async () => {
loading.value = true
try {
const res = await getExamList(queryParams)
examList.value = res.list
total.value = res.total
} catch (error) {
console.error('获取考试列表失败', error)
} finally {
loading.value = false
}
}
const handleQuery = () => {
queryParams.page = 1
fetchExamList()
}
const resetQuery = () => {
queryParams.title = ''
queryParams.timeLimit = undefined
queryParams.page = 1
fetchExamList()
}
const handleSizeChange = (size: number) => {
queryParams.size = size
fetchExamList()
}
const handleCurrentChange = (page: number) => {
queryParams.page = page
fetchExamList()
}
const handleAdd = () => {
dialogType.value = 'add'
Object.assign(formData, defaultFormData)
dialogVisible.value = true
}
const handleEdit = async (row: Exam) => {
dialogType.value = 'edit'
dialogVisible.value = true
try {
loading.value = true
const res = await getExam(row.id)
Object.assign(formData, res)
} catch (error) {
console.error('获取考试详情失败', error)
} finally {
loading.value = false
}
}
const handleDelete = (row: Exam) => {
ElMessageBox.confirm(
`确认删除考试 "${row.title}" 吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(async () => {
try {
await deleteExam(row.id)
ElMessage.success('删除成功')
fetchExamList()
} catch (error) {
console.error('删除失败', error)
}
})
.catch(() => {})
}
const submitForm = async () => {
if (!examFormRef.value) return
await examFormRef.value.validate(async (valid, fields) => {
if (valid) {
try {
if (dialogType.value === 'add') {
await createExam(formData)
ElMessage.success('添加成功')
} else {
await updateExam(formData.id as string, formData)
ElMessage.success('更新成功')
}
dialogVisible.value = false
fetchExamList()
} catch (error) {
console.error('保存失败', error)
}
}
})
}
const formatDateTime = (time: string) => {
if (!time) return ''
const date = new Date(time)
return date.toLocaleString()
}
</script>
<style scoped>
.exam-container {
padding: 20px;
}
.filter-container {
margin-bottom: 20px;
}
.action-container {
margin-bottom: 20px;
}
.pagination-container {
margin-top: 20px;
text-align: right;
}
</style>

11
pom.xml
View File

@ -484,6 +484,12 @@
</exclusions>-->
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>1.0.0-beta2</version>
</dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
@ -507,6 +513,11 @@
<version>4.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>

View File

@ -1,14 +1,25 @@
package com.guwan.backend.Handler;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.toolkit.ReflectionKit;
import com.guwan.backend.annotation.RecoverIfDeleted;
import com.guwan.backend.util.ReflectUtil;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.SqlSession;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
System.out.println("metaObject = " + metaObject);
this.strictInsertFill(metaObject, "createdTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
}
@ -18,4 +29,6 @@ public class MyMetaObjectHandler implements MetaObjectHandler {
this.strictUpdateFill(metaObject, "lastLoginTime", LocalDateTime.class, LocalDateTime.now());
this.strictUpdateFill(metaObject, "updatedTime", LocalDateTime.class, LocalDateTime.now());
}
}

View File

@ -11,18 +11,42 @@ import java.io.InputStream;
import java.net.URL;
public class VideoDuration {
// public static void main(String[] args) {
// /* String preSignedUrl = "http://localhost:9000/videos/ffffad37-9804-4765-ae18-3f8dcda9bea8.mp4"; // Minio 预签名 URL
//
// try (InputStream stream = new URL(preSignedUrl).openStream()) {
// Metadata metadata = new Metadata();
// Parser parser = new AutoDetectParser();
// parser.parse(stream, new BodyContentHandler(), metadata, new ParseContext());
//
// String duration = metadata.get("duration");
// System.out.println("视频时长(毫秒): " + duration);
// } catch (Exception e) {
// e.printStackTrace();
// }*/
// int i = 0;
// while (i< 5){
// if (i ==3){
// i++;
// continue;
// }
// System.out.println(i);
// i++;
// }
//
// }
static boolean foo(char x) {
System.out.print(x);
return true;
}
public static void main(String[] args) {
String preSignedUrl = "http://localhost:9000/videos/ffffad37-9804-4765-ae18-3f8dcda9bea8.mp4"; // Minio 预签名 URL
try (InputStream stream = new URL(preSignedUrl).openStream()) {
Metadata metadata = new Metadata();
Parser parser = new AutoDetectParser();
parser.parse(stream, new BodyContentHandler(), metadata, new ParseContext());
String duration = metadata.get("duration");
System.out.println("视频时长(毫秒): " + duration);
} catch (Exception e) {
e.printStackTrace();
int i = 0;
for (foo('A'); foo('B') && (i < 2); foo('C')) {
i++;
foo('D');
}
}
}

View File

@ -0,0 +1,12 @@
package com.guwan.backend.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD) // 这个注解应用于字段
@Retention(RetentionPolicy.RUNTIME) // 注解在运行时有效
public @interface CheckExistence {
String message() default "Field value already exists"; // 错误提示消息
}

View File

@ -0,0 +1,63 @@
package com.guwan.backend.annotation;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.guwan.backend.annotation.CheckExistence;
import java.lang.reflect.Field;
public class CheckExistenceProcessor {
// 检查该字段值是否在数据库中存在
public static <T> boolean checkFieldExistence(T entity, IService<T> service) throws Exception {
// 获取实体类的所有字段
Field[] fields = entity.getClass().getDeclaredFields();
for (Field field : fields) {
// 如果字段上有 @CheckExistence 注解
if (field.isAnnotationPresent(CheckExistence.class)) {
field.setAccessible(true); // 设置字段可访问
Object value = field.get(entity); // 获取字段值
// 如果字段值为空不进行检查
if (value == null) {
continue;
}
// 获取字段的名称
String fieldName = field.getName();
// 使用 MyBatis-Plus 查询是否已存在该值
LambdaQueryWrapper<T> queryWrapper = new LambdaQueryWrapper<>();
// 将字段通过 SFunction 引入
SFunction<T, ?> fieldExpression = getFieldExpression(entity, fieldName);
if (fieldExpression != null) {
queryWrapper.eq(fieldExpression, value);
}
// 执行查询
long count = service.count(queryWrapper); // 使用 count 而不是 list避免不必要的数据加载
if (count > 0) {
// 如果数据库中已经存在该值返回 true
System.out.println("Error: " + fieldName + " value already exists.");
return true;
}
}
}
return false; // 没有找到重复值
}
// 通过反射获取字段的 SFunction 表达式
private static <T> SFunction<T, ?> getFieldExpression(T entity, String fieldName) {
try {
// 根据字段名生成 SFunction 表达式
return (SFunction<T, ?>) entity.getClass().getDeclaredField(fieldName).get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
return null;
}
}
}

View File

@ -0,0 +1,13 @@
package com.guwan.backend.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RecoverIfDeleted {
String value() default "deleted"; // 逻辑删除字段
String businessKey() default "uniqueKey"; // 业务唯一键
}

View File

@ -115,13 +115,13 @@ public class OperationLogAspect {
*/
private String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个代理的情况第一个IP为客户端真实IP

View File

@ -20,7 +20,6 @@ public interface SimpleTTSClient {
@PostMapping(value = "/v1/audio/speech")
byte[] saveAudio(String jsonInputString);
}

View File

@ -0,0 +1,48 @@
package com.guwan.backend.config;
import com.guwan.backend.store.PersistentChatMemoryStore;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.UserMessage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Aiconfig {
public interface Assistant {
String chat(@MemoryId String memoryId, @UserMessage String message);
// 流式响应
TokenStream stream(@MemoryId String memoryId, @UserMessage String message);
}
@Bean
public Assistant assistant(ChatLanguageModel qwenchatModel,
StreamingChatLanguageModel qwenstreamingchatModel) {
PersistentChatMemoryStore store = new PersistentChatMemoryStore();
ChatMemoryProvider chatMemoryProvider = memoryId -> MessageWindowChatMemory.builder()
.id(memoryId)
.maxMessages(10)
.chatMemoryStore(store)
.build();
return AiServices.builder(Assistant.class)
.chatLanguageModel(qwenchatModel)
.streamingChatLanguageModel(qwenstreamingchatModel)
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.builder().maxMessages(10)
.id(memoryId).build()
)
.chatMemoryProvider(chatMemoryProvider)
.build();
}
}

View File

@ -11,8 +11,6 @@ public class CorsConfig {
//get请求变成了options 后端预检过不了怎么改
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();

View File

@ -23,7 +23,7 @@ public class MybatisPlusConfig {
return new MyMetaObjectHandler();
}
//mybatisplus分页流程为全量查询 =本地分页
//mybatisplus分页流程为全量查询 => 本地分页
// 而如果不对查询结果进行拦截mybatis将不能执行分页操作自然也拿不到页数和总数的数据了
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {

View File

@ -1,15 +0,0 @@
package com.guwan.backend.controller;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Ac{
List<String> ids;
String str;
}

View File

@ -0,0 +1,89 @@
package com.guwan.backend.controller;
import com.guwan.backend.annotation.OperationLog;
import com.guwan.backend.common.Result;
import com.guwan.backend.pojo.entity.BaseExam;
import com.guwan.backend.service.BaseExamService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
/**
* (BaseExam)表控制层
*
* @author Guwan
* @since 2025-04-19 22:54:02
*/
@RestController
@RequestMapping("baseExam")
public class BaseExamController {
/**
* 服务对象
*/
@Autowired
private BaseExamService baseExamService;
@OperationLog
@GetMapping
public Result queryByPage(@RequestParam(name = "page") Integer page,
@RequestParam(name = "page") Integer pageSize,
@RequestParam(name = "query") String query) {
return Result.success(baseExamService.queryByPage(page, pageSize, query));
// return Result.success();
}
/**
* 通过主键查询单条数据
*
* @param id 主键
* @return 单条数据
*/
@GetMapping("{id}")
public Result<BaseExam> queryById(@PathVariable("id") String id) {
return Result.success(this.baseExamService.getById(id));
}
/**
* 新增数据
*
* @param baseExam 实体
* @return 新增结果
*/
@PostMapping
public Result<BaseExam> add(BaseExam baseExam) {
this.baseExamService.save(baseExam);
return Result.success();
}
/**
* 编辑数据
*
* @param baseExam 实体
* @return 编辑结果
*/
@PutMapping
public Result<BaseExam> edit(BaseExam baseExam) {
//return Result.success(this.baseExamService.update(baseExam));
return Result.success();
}
/**
* 删除数据
*
* @param id 主键
* @return 删除是否成功
*/
@DeleteMapping
public Result<Boolean> deleteById(String id) {
return Result.success(this.baseExamService.removeById(id));
}
}

View File

@ -5,10 +5,7 @@ import com.guwan.backend.annotation.OperationLog;
import com.guwan.backend.client.SimpleTTSClient;
import com.guwan.backend.client.VoiceServiceClient;
import com.guwan.backend.common.Result;
import com.guwan.backend.mongodb.EveryReadDetailOfMongodb;
import com.guwan.backend.mongodb.EveryReadDetailOfMongodbService;
import com.guwan.backend.mongodb.MongodbUserService;
import com.guwan.backend.mongodb.User;
import com.guwan.backend.mongodb.*;
import com.guwan.backend.pojo.entity.BookContent;
import com.guwan.backend.util.MinioUtil;
import dev.langchain4j.community.model.dashscope.QwenChatModel;
@ -41,8 +38,13 @@ import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -70,6 +72,9 @@ public class CommonController {
private final QwenChatModel qwenChatModel;
private final QwenStreamingChatModel qwenStreamingChatModel;
private final TestDateRepository testDateRepository;
private final TestDateDao testDateDao;
@PostMapping("/uploadFile")
public Result<String> uploadFile(String bucketName, MultipartFile file){
return Result.success(minioUtil.getUrl(minioUtil.getFileUrl
@ -498,10 +503,7 @@ public class CommonController {
System.out.println("str = " + str);
}
@PostMapping("/testPostUseBody")
public void testPostUseBody(@RequestBody Ac ac){
System.out.println("ac = " + ac);
}
@GetMapping("/testGetParam")
public void testGetParam(@RequestParam("ids") List<String> ids,
@ -511,6 +513,48 @@ public class CommonController {
}
@GetMapping("/testDate")
public void testDate(){
TestDate testDate = new TestDate();
testDate.setId(UUID.randomUUID().toString());
Instant now = Instant.now();
//Instant now = Instant.now();
// 获取当前系统默认时区
ZoneId zoneId = ZoneId.systemDefault();
// 获取今天的 00:00:00
Instant todayStart = LocalDate.now(zoneId)
.atStartOfDay(zoneId)
.toInstant();
// 假设 testDate 有一个 setDate(Instant instant) 方法
testDate.setDate(Date.from(todayStart));
// testDate.setDate(new Date());
testDateRepository.insert(testDate);
}
@GetMapping("/testDateFind")
public void testDateFind(){
TestDate testDate = new TestDate();
testDate.setId(UUID.randomUUID().toString());
Instant now = Instant.now();
//Instant now = Instant.now();
// 获取当前系统默认时区
ZoneId zoneId = ZoneId.systemDefault();
// 获取今天的 00:00:00
Instant todayStart = LocalDate.now(zoneId)
.atStartOfDay(zoneId)
.toInstant();
Date from = Date.from(todayStart);
List<TestDate> byDate = testDateDao.findByDate(from);
System.out.println("byDate = " + byDate);
}

View File

@ -68,8 +68,6 @@ public class CourseController {
courseService.getCourseDetail(courseId);
return Result.success();
}

View File

@ -1,4 +1,77 @@
package com.guwan.backend.controller;
import com.guwan.backend.common.Result;
import com.guwan.backend.config.Aiconfig;
import com.guwan.backend.security.CustomUserDetails;
import dev.langchain4j.community.model.dashscope.QwenChatModel;
import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
import dev.langchain4j.model.chat.response.ChatResponse;
import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import reactor.core.publisher.Flux;
/**
* ai 聊天控制层
*
* 1.聊天
* 2.会话管理
*
*/
@RequiredArgsConstructor
public class GPTController {
private final QwenChatModel qwenChatModel;
private final QwenStreamingChatModel qwenStreamingChatModel;
private final Aiconfig.Assistant assistant;
@GetMapping("/testQwen")
public Result chatWithQwen(@RequestParam(value = "message", required = false) String message) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//Java 16+ 模式匹配写法
if (authentication != null && authentication.getPrincipal() instanceof CustomUserDetails userDetails) {
// sysLog.setUserId(userDetails.getUserId());
// sysLog.setUsername(userDetails.getUsername());
}
var response = assistant.chat("userId+chatId", message);
System.out.println("response = " + response);
return Result.success(response);
}
@GetMapping(value = "/testQwenStreaming", produces = "text/steam;charset=UTF-8")
public Flux<String> testQwenStreaming(@RequestParam(value = "message", required = false) String message) {
Flux<String> flux = Flux.create(fluxSink -> {
qwenStreamingChatModel.chat(message, new StreamingChatResponseHandler() {
//每一次流式响应的文本
@Override
public void onPartialResponse(String partialResponse) {
fluxSink.next(partialResponse);
}
//响应结束的文本
@Override
public void onCompleteResponse(ChatResponse chatResponse) {
fluxSink.complete();
}
@Override
public void onError(Throwable throwable) {
fluxSink.error(throwable);
}
});
});
return flux;
}
}

View File

@ -0,0 +1,18 @@
package com.guwan.backend.mapper;
import com.guwan.backend.pojo.entity.BaseExam;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
* @author 12455
* @description 针对表base_exam的数据库操作Mapper
* @createDate 2025-04-19 22:52:21
* @Entity com.guwan.backend.pojo.entity.BaseExam
*/
public interface BaseExamMapper extends BaseMapper<BaseExam> {
}

View File

@ -2,6 +2,7 @@ package com.guwan.backend.model.exam.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.guwan.backend.common.Result;
import com.guwan.backend.core.api.ApiRest;
import com.guwan.backend.core.api.controller.BaseController;
import com.guwan.backend.core.api.dto.BaseIdReqDTO;
@ -9,6 +10,8 @@ import com.guwan.backend.core.api.dto.BaseIdsReqDTO;
import com.guwan.backend.core.api.dto.BaseStateReqDTO;
import com.guwan.backend.core.api.dto.PagingReqDTO;
import com.guwan.backend.model.exam.dto.ExamDTO;
import com.guwan.backend.model.exam.dto.ExamResponseDTO;
import com.guwan.backend.model.exam.dto.QuestionDTO;
import com.guwan.backend.model.exam.dto.request.ExamSaveReqDTO;
import com.guwan.backend.model.exam.dto.response.ExamOnlineRespDTO;
import com.guwan.backend.model.exam.dto.response.ExamReviewRespDTO;
@ -22,7 +25,9 @@ import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* <p>
@ -64,4 +69,54 @@ public class ExamController extends BaseController {
}
@GetMapping("/exam")
public Result getExam() {
// 模拟数据可以从数据库中查询
ExamResponseDTO response = new ExamResponseDTO();
response.setId("1");
response.setTitle("模拟考试试卷 (含新题型)");
response.setTotalScore(100);
response.setTotalTime(120);
response.setLeftSeconds(7180);
response.setExamStartTime("2025-04-17 00:10:24");
response.setExamDuration(60 * 60 * 2);
// 填充题数据
List<QuestionDTO> fillList = new ArrayList<>();
fillList.add(new QuestionDTO("5001", "501", 0, false, "填空题1"));
fillList.add(new QuestionDTO("5002", "502", 1, false, "填空题2"));
response.setFillList(fillList);
// 判断题数据
List<QuestionDTO> judgeList = new ArrayList<>();
judgeList.add(new QuestionDTO("3001", "301", 2, false, "判断题1"));
response.setJudgeList(judgeList);
// 单选题数据
List<QuestionDTO> radioList = new ArrayList<>();
radioList.add(new QuestionDTO("1001", "101", 3, false, "单选题1"));
radioList.add(new QuestionDTO("1002", "102", 4, false, "单选题2"));
response.setRadioList(radioList);
// 多选题数据
List<QuestionDTO> multiList = new ArrayList<>();
multiList.add(new QuestionDTO("2001", "201", 5, false, "多选题1"));
response.setMultiList(multiList);
// 简答题数据
List<QuestionDTO> saqList = new ArrayList<>();
saqList.add(new QuestionDTO("6001", "601", 6, false, "简答题1"));
response.setSaqList(saqList);
// 编程题数据
List<QuestionDTO> programmingList = new ArrayList<>();
programmingList.add(new QuestionDTO("4001", "401", 7, false, "两数之和"));
programmingList.add(new QuestionDTO("4002", "402", 8, false, "反转链表"));
response.setProgrammingList(programmingList);
return Result.success(response);
}
}

View File

@ -0,0 +1,27 @@
package com.guwan.backend.model.exam.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExamResponseDTO {
private String id;
private String title;
private int totalScore;
private int totalTime;
private int leftSeconds;
private String examStartTime; // 这里用字符串格式而不是标准时间
private int examDuration;
private List<QuestionDTO> fillList;
private List<QuestionDTO> judgeList;
private List<QuestionDTO> radioList;
private List<QuestionDTO> multiList;
private List<QuestionDTO> saqList;
private List<QuestionDTO> programmingList;
}

View File

@ -0,0 +1,16 @@
package com.guwan.backend.model.exam.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class QuestionDTO {
private String id;
private String quId;
private int sort;
private boolean answered;
private String title;
}

View File

@ -38,8 +38,6 @@ public class FileStorageService {
Query query = new Query(Criteria.where("_id").is(objectId));
// 根据文件 ID 获取文件
GridFSFile gridFSFile = gridFsTemplate.findOne(query);

View File

@ -0,0 +1,28 @@
package com.guwan.backend.mongodb;
import lombok.RequiredArgsConstructor;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.util.Date;
import java.util.List;
@Component
@RequiredArgsConstructor
public class MongoTestDateDao implements TestDateDao{
private final MongoTemplate mongoTemplate;
// 可以定义一些自定义查询方法
public List<TestDate> findByDate(Date date) {
Query query = new Query();
query.addCriteria(Criteria.where("date").is(date));
return mongoTemplate.find(query, TestDate.class);
}
}

View File

@ -0,0 +1,18 @@
package com.guwan.backend.mongodb;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
@Document("testDate")
@Data
public class TestDate {
@Id
private String id;
private Date date;
}

View File

@ -0,0 +1,12 @@
package com.guwan.backend.mongodb;
import org.springframework.data.mongodb.repository.MongoRepository;
import java.time.LocalDate;
import java.util.Date;
import java.util.List;
public interface TestDateDao {
// 可以定义一些自定义查询方法
List<TestDate> findByDate(Date date);
}

View File

@ -0,0 +1,7 @@
package com.guwan.backend.mongodb;
import org.springframework.data.mongodb.repository.MongoRepository;
public interface TestDateRepository extends MongoRepository<TestDate, String> {
// 可以定义一些自定义查询方法
}

View File

@ -0,0 +1,66 @@
package com.guwan.backend.pojo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
/**
*
* @TableName base_exam
*/
@TableName(value ="base_exam")
@Data
public class BaseExam implements Serializable {
/**
* 试题id
*/
@TableId
private String id;
/**
* 考试名称
*/
private String title;
/**
* 是否限时
*/
private Integer timeLimit;
/**
* 考试开始时间
*/
private Date startTime;
/**
* 考试结束时间
*/
private Date endTime;
/**
* 考试时间总长
*/
private Integer totalTime;
/**
* 考试总分
*/
private Integer totalScore;
/**
* 及格分数
*/
private Integer qualifyScore;
/**
* 删除标志位
*/
private Integer deleted;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}

View File

@ -43,6 +43,10 @@ public class UserDetailsServiceImpl implements UserDetailsService {
private UserDetails convertToUserDetails(User user) {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
//TODO 在这加权限校验
//user 表里去查 然后拿到 add
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return new CustomUserDetails(

View File

@ -0,0 +1,16 @@
package com.guwan.backend.service;
import com.guwan.backend.pojo.entity.BaseExam;
import com.baomidou.mybatisplus.extension.service.IService;
import java.util.List;
/**
* @author 12455
* @description 针对表base_exam的数据库操作Service
* @createDate 2025-04-19 22:52:21
*/
public interface BaseExamService extends IService<BaseExam> {
List<BaseExam> queryByPage(Integer page, Integer pageSize, String query);
}

View File

@ -0,0 +1,50 @@
package com.guwan.backend.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.guwan.backend.annotation.CheckExistenceProcessor;
import com.guwan.backend.pojo.entity.BaseExam;
import com.guwan.backend.service.BaseExamService;
import com.guwan.backend.mapper.BaseExamMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* @author 12455
* @description 针对表base_exam的数据库操作Service实现
* @createDate 2025-04-19 22:52:21
*/
@Service
@RequiredArgsConstructor
public class BaseExamServiceImpl extends ServiceImpl<BaseExamMapper, BaseExam>
implements BaseExamService{
private final BaseExamMapper baseExamMapper;
@Override
public List<BaseExam> queryByPage(Integer page, Integer pageSize, String query) {
baseExamMapper.insert(new BaseExam());
return null;
}
@Override
public boolean save(BaseExam entity) {
try {
// 调用检查方法
if (CheckExistenceProcessor.checkFieldExistence(entity, this)) {
return false; // 如果字段值已存在返回 false阻止插入
}
} catch (Exception e) {
e.printStackTrace();
return false;
}
// 如果没有重复数据执行插入操作
return super.save(entity);
}
}

View File

@ -0,0 +1,121 @@
/*
package com.guwan.backend.store;
import dev.langchain4j.data.message.*;
public class ChatMessageUtils {
private ChatMessageUtils() {
// 工具类不允许实例化
}
*/
/**
* 根据类型和文本内容动态构造 ChatMessage 实例
*//*
public static ChatMessage build(ChatMessageType type, String text) {
switch (type) {
case USER:
return new UserMessage(text);
case AI:
return new AiMessage(text);
case SYSTEM:
return new SystemMessage(text);
case TOOL_EXECUTION_RESULT:
return new ToolExecutionResultMessage(text);
case CUSTOM:
return new CustomMessage(text);
default:
throw new IllegalArgumentException("Unsupported ChatMessageType: " + type);
}
}
*/
/**
* 获取 ChatMessage ChatMessageType
*//*
public static ChatMessageType typeOf(ChatMessage message) {
if (message instanceof UserMessage) {
return ChatMessageType.USER;
} else if (message instanceof AiMessage) {
return ChatMessageType.AI;
} else if (message instanceof SystemMessage) {
return ChatMessageType.SYSTEM;
} else if (message instanceof ToolExecutionResultMessage) {
return ChatMessageType.TOOL_EXECUTION_RESULT;
} else if (message instanceof CustomMessage) {
return ChatMessageType.CUSTOM;
} else {
throw new IllegalArgumentException("Unknown message class: " + message.getClass());
}
}
*/
/**
* 获取 ChatMessage 的文本内容
*//*
public static String textOf(ChatMessage message) {
if (message instanceof UserMessage) {
return ((UserMessage) message).text();
} else if (message instanceof AiMessage) {
return ((AiMessage) message).text();
} else if (message instanceof SystemMessage) {
return ((SystemMessage) message).text();
} else if (message instanceof ToolExecutionResultMessage) {
return ((ToolExecutionResultMessage) message).text();
} else if (message instanceof CustomMessage) {
return ((CustomMessage) message).text();
} else {
throw new IllegalArgumentException("Unknown message class: " + message.getClass());
}
}
*/
/**
* 获取 ChatMessage 的角色 (user / assistant / system / tool_execution_result / custom)
*//*
public static String roleOf(ChatMessage message) {
ChatMessageType type = typeOf(message);
switch (type) {
case USER:
return "user";
case AI:
return "assistant";
case SYSTEM:
return "system";
case TOOL_EXECUTION_RESULT:
return "tool_execution_result";
case CUSTOM:
return "custom";
default:
throw new IllegalArgumentException("Unsupported ChatMessageType: " + type);
}
}
*/
/**
* 从角色字符串恢复 ChatMessageType
*//*
public static ChatMessageType typeOfRole(String role) {
switch (role) {
case "user":
return ChatMessageType.USER;
case "assistant":
return ChatMessageType.AI;
case "system":
return ChatMessageType.SYSTEM;
case "tool_execution_result":
return ChatMessageType.TOOL_EXECUTION_RESULT;
case "custom":
return ChatMessageType.CUSTOM;
default:
throw new IllegalArgumentException("Unsupported role: " + role);
}
}
}
*/

View File

@ -0,0 +1,32 @@
package com.guwan.backend.store;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import java.util.List;
/**
* ai消息存储
*/
public class PersistentChatMemoryStore implements ChatMemoryStore {
@Override
public List<ChatMessage> getMessages(Object memoryId) {
ChatMessage message1 = new UserMessage("你好,请自我介绍一下");
ChatMessage message2 = new AiMessage("你好,我是你的智能助手。");
ChatMessage message3 = new SystemMessage("你是一个有礼貌的助手,请简洁回答用户问题。");
return null;
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> list) {
}
@Override
public void deleteMessages(Object memoryId) {
}
}

View File

@ -0,0 +1,52 @@
package com.guwan.backend.util;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectUtil {
// 安全获取字段值
public static Object getFieldValue(Object obj, String fieldName) {
try {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("反射获取字段失败: " + fieldName, e);
}
}
// 安全设置字段值
public static void setFieldValue(Object obj, String fieldName, Object value) {
try {
Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("反射设置字段失败: " + fieldName, e);
}
}
// 通过Getter/Setter方法操作更安全
public static Object invokeGetter(Object obj, String fieldName) {
try {
Method method = obj.getClass().getMethod("get" + capitalize(fieldName));
return method.invoke(obj);
} catch (Exception e) {
throw new RuntimeException("调用Getter失败: " + fieldName, e);
}
}
public static void invokeSetter(Object obj, String fieldName, Object value) {
try {
Method method = obj.getClass().getMethod("set" + capitalize(fieldName), value.getClass());
method.invoke(obj, value);
} catch (Exception e) {
throw new RuntimeException("调用Setter失败: " + fieldName, e);
}
}
private static String capitalize(String str) {
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}

View File

@ -207,8 +207,6 @@ logging:
host: localhost
port: 5044
xxl:
job:
enabled: false # 控制是否启用xxl-job

View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.guwan.backend.mapper.BaseExamMapper">
<resultMap id="BaseResultMap" type="com.guwan.backend.pojo.entity.BaseExam">
<id property="id" column="id" jdbcType="VARCHAR"/>
<result property="title" column="title" jdbcType="VARCHAR"/>
<result property="timeLimit" column="time_limit" jdbcType="TINYINT"/>
<result property="startTime" column="start_time" jdbcType="TIMESTAMP"/>
<result property="endTime" column="end_time" jdbcType="TIMESTAMP"/>
<result property="totalTime" column="total_time" jdbcType="INTEGER"/>
<result property="totalScore" column="total_score" jdbcType="INTEGER"/>
<result property="qualifyScore" column="qualify_score" jdbcType="INTEGER"/>
</resultMap>
<sql id="Base_Column_List">
id,title,time_limit,
start_time,end_time,total_time,
total_score,qualify_score
</sql>
</mapper>