diff --git a/.github/workflows/depoly.yml b/.github/workflows/depoly.yml
index 7dd62c0..8e99c5a 100644
--- a/.github/workflows/depoly.yml
+++ b/.github/workflows/depoly.yml
@@ -58,14 +58,17 @@ jobs:
script: |
set -e
cd ./refine
+
echo "pulling latest code..."
- git pull
- git checkout develop
+ git fetch --all
+ BRANCH_NAME="${{ github.ref_name }}"
+ echo "Detected branch: $BRANCH_NAME"
+ git checkout $BRANCH_NAME
+ git pull origin $BRANCH_NAME
echo "pull completed."
+
echo "Deploying..."
- sh build.sh stop
- sh build.sh rm && sh build.sh rmi
+ echo "Pulling latest Docker image..."
+ docker pull docker.cnb.cool/hamster-yhz/refine:latest
sh build.sh base && sh build.sh services
echo "Deployment completed."
-
-
diff --git a/README.md b/README.md
index df32ac7..73d0162 100644
--- a/README.md
+++ b/README.md
@@ -5,4 +5,5 @@ AchoBeta Refine 琢玉系统-2025年-J组-复试项目
一个面向学生的AI学习助手项目后端,通过上传错题,系统自动识题目内容、分错误原因、讲解知识点并生成个性化练习,帮助学生快速查漏补缺
> 快速了解项目结构 请阅读: `docs/PROJECT_GUIDE.md`
+>
> 快速了解贡献指南 请阅读: `docs/CONTRIBUTING_GUIDE.md`
\ No newline at end of file
diff --git a/docs/dev-ops/docker-compose-app.yml b/docs/dev-ops/docker-compose-app.yml
index 5d68f4d..d387f44 100644
--- a/docs/dev-ops/docker-compose-app.yml
+++ b/docs/dev-ops/docker-compose-app.yml
@@ -6,6 +6,7 @@ services:
refine:
image: docker.cnb.cool/hamster-yhz/refine:latest
container_name: refine
+ pull_policy: always
restart: on-failure
ports:
- "8091:8091"
diff --git a/pom.xml b/pom.xml
index f51ba2e..bdebeaf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -46,7 +46,8 @@
UTF-8
21
21
- 1.1.0-beta7
+ 1.6.0-beta12
+ 2.19.0
@@ -64,11 +65,11 @@
-
- dev.langchain4j
- langchain4j-core
- 1.1.0
-
+
+
+
+
+
org.postgresql
@@ -85,7 +86,7 @@
dev.langchain4j
langchain4j-embeddings-all-minilm-l6-v2
- 1.1.0-beta6
+ 1.0.1-beta6
@@ -97,7 +98,12 @@
com.fasterxml.jackson.core
jackson-databind
- 2.19.0
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ ${jackson.version}
@@ -202,11 +208,6 @@
java-jwt
4.4.0
-
- commons-codec
- commons-codec
- 1.15
-
org.projectlombok
lombok
@@ -227,7 +228,7 @@
dev.langchain4j
langchain4j
- 1.1.0
+ 1.6.0
dev.langchain4j
diff --git a/refine-api/src/main/java/com/achobeta/api/dto/AiSolveRequestDTO.java b/refine-api/src/main/java/com/achobeta/api/dto/AiSolveRequestDTO.java
index 4c289f1..7dda8c9 100644
--- a/refine-api/src/main/java/com/achobeta/api/dto/AiSolveRequestDTO.java
+++ b/refine-api/src/main/java/com/achobeta/api/dto/AiSolveRequestDTO.java
@@ -1,22 +1,30 @@
package com.achobeta.api.dto;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @Auth : Malog
- * @Desc :
+ * @Desc : AI解题请求DTO
* @Time : 2025/11/7 11:17
*/
@Data
@Builder
+@NoArgsConstructor
+@AllArgsConstructor
public class AiSolveRequestDTO implements Serializable {
/**
* 问题内容
*/
- private String questionContext;
+ @NotBlank(message = "问题内容不能为空")
+ @Size(max = 10000, message = "问题内容长度不能超过10000字符")
+ private String question;
}
diff --git a/refine-api/src/main/java/com/achobeta/api/dto/KeyPointsDTO.java b/refine-api/src/main/java/com/achobeta/api/dto/KeyPointsDTO.java
index 43e0965..0a4eba7 100644
--- a/refine-api/src/main/java/com/achobeta/api/dto/KeyPointsDTO.java
+++ b/refine-api/src/main/java/com/achobeta/api/dto/KeyPointsDTO.java
@@ -11,7 +11,7 @@ public class KeyPointsDTO {
/**
* 知识点id
*/
- private int id;
+ private String id;
/**
* 知识点内容
*/
diff --git a/refine-api/src/main/java/com/achobeta/api/dto/MistakeReasonToggleRequestDTO.java b/refine-api/src/main/java/com/achobeta/api/dto/MistakeReasonToggleRequestDTO.java
new file mode 100644
index 0000000..c38ac82
--- /dev/null
+++ b/refine-api/src/main/java/com/achobeta/api/dto/MistakeReasonToggleRequestDTO.java
@@ -0,0 +1,32 @@
+package com.achobeta.api.dto;
+
+import com.achobeta.types.annotation.FieldDesc;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import jakarta.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+/**
+ * @Auth : Malog
+ * @Desc : 简化错因切换请求DTO
+ * @Time : 2025/11/10
+ */
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class MistakeReasonToggleRequestDTO implements Serializable {
+
+ @NotBlank(message = "题目ID不能为空")
+ @FieldDesc(name = "题目ID")
+ private String questionId;
+
+ @NotBlank(message = "错因参数名不能为空")
+ @FieldDesc(name = "错因参数名")
+ private String reasonName;
+}
\ No newline at end of file
diff --git a/refine-api/src/main/java/com/achobeta/api/dto/StudyNoteRequestDTO.java b/refine-api/src/main/java/com/achobeta/api/dto/StudyNoteRequestDTO.java
index 7a17262..f05f610 100644
--- a/refine-api/src/main/java/com/achobeta/api/dto/StudyNoteRequestDTO.java
+++ b/refine-api/src/main/java/com/achobeta/api/dto/StudyNoteRequestDTO.java
@@ -14,12 +14,6 @@
@Data
public class StudyNoteRequestDTO implements Serializable {
- /**
- * 用户ID
- */
- @NotBlank(message = "用户ID不能为空")
- private String userId;
-
/**
* 题目ID
*/
diff --git a/refine-api/src/main/java/com/achobeta/api/dto/UpdateOtherReasonRequestDTO.java b/refine-api/src/main/java/com/achobeta/api/dto/UpdateOtherReasonRequestDTO.java
new file mode 100644
index 0000000..da48b26
--- /dev/null
+++ b/refine-api/src/main/java/com/achobeta/api/dto/UpdateOtherReasonRequestDTO.java
@@ -0,0 +1,32 @@
+package com.achobeta.api.dto;
+
+import com.achobeta.types.annotation.FieldDesc;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+import jakarta.validation.constraints.NotBlank;
+import java.io.Serializable;
+
+/**
+ * @Auth : Malog
+ * @Desc : 更新其他原因文本请求DTO
+ * @Time : 2025/11/10
+ */
+@Getter
+@Setter
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+public class UpdateOtherReasonRequestDTO implements Serializable {
+
+ @NotBlank(message = "题目ID不能为空")
+ @FieldDesc(name = "题目ID")
+ private String questionId;
+
+ @NotBlank(message = "其他原因描述不能为空")
+ @FieldDesc(name = "其他原因描述")
+ private String otherReasonText;
+}
\ No newline at end of file
diff --git a/refine-app/src/main/java/com/achobeta/aop/GlobalOperationAspect.java b/refine-app/src/main/java/com/achobeta/aop/GlobalOperationAspect.java
index 2945bf9..ea3b2f2 100644
--- a/refine-app/src/main/java/com/achobeta/aop/GlobalOperationAspect.java
+++ b/refine-app/src/main/java/com/achobeta/aop/GlobalOperationAspect.java
@@ -43,10 +43,10 @@ public void interceptorDo(JoinPoint point) {
checkLogin();
} catch (UnauthorizedException e) {
log.error("登录验证失败:{}", e.getMessage());
- throw new AppException(e.getCode(),e.getMessage());
+ throw new AppException(e.getCode(), e.getMessage());
} catch (AppException e) {
log.error("全局拦截异常", e);
- throw new AppException(e.getMessage());
+ throw new AppException(e.getCode(), e.getMessage());
} catch (Throwable e) {
log.error("全局拦截异常", e);
throw new AppException(GlobalServiceStatusCode.PARAM_FAILED_VALIDATE);
@@ -74,7 +74,7 @@ private void checkLogin() {
// 从请求头获取token
String token = request.getHeader("access-token");
if (StringTools.isEmpty(token)) {
- throw new AppException(GlobalServiceStatusCode.UNAUTHORIZED); // token为空,未登录
+ throw new AppException(401, "token为空"); // token为空,未登录
}
// 先JWT技术验证,验签名、过期时间
diff --git a/refine-app/src/main/java/com/achobeta/config/AiThreadPoolConfig.java b/refine-app/src/main/java/com/achobeta/config/AiThreadPoolConfig.java
index b585593..d06e35a 100644
--- a/refine-app/src/main/java/com/achobeta/config/AiThreadPoolConfig.java
+++ b/refine-app/src/main/java/com/achobeta/config/AiThreadPoolConfig.java
@@ -1,48 +1,71 @@
package com.achobeta.config;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+/**
+ * AI 专属线程池配置(用于 AI 流式调用、API 请求等业务逻辑)
+ * 核心优化:配置容错、参数合理性校验、资源占用控制、生产环境监控
+ */
@Configuration
+@Slf4j
public class AiThreadPoolConfig {
- // 最大并发数
- @Value("${ai.dashscope.max-concurrency}")
+ /**
+ * AI 最大并发数(从配置文件读取,默认值 10,避免配置缺失导致启动失败)
+ * 用途:限制 AI 调用的最大并发量,避免触发第三方 API(如 DashScope)限流
+ */
+ @Value("${ai.dashscope.max-concurrency:10}")
private int aiMaxConcurrency;
- @Bean
+ /**
+ * AI 专属线程池(业务层面:执行 AI 流式调用、模型推理等耗时操作)
+ */
+ @Bean(name = "aiExclusiveThreadPool") // 明确 bean 名称,避免注入冲突
public ThreadPoolTaskExecutor aiExclusiveThreadPool() {
- ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ // 1. 基础参数计算(CPU 核心数)
int cpuCore = Runtime.getRuntime().availableProcessors();
+ log.info("系统 CPU 核心数:{},配置的 AI 最大并发数:{}", cpuCore, aiMaxConcurrency);
- int baseCoreSize = cpuCore * 2 + 1;
+ // 2. 配置容错校验(避免无效配置)
+ if (aiMaxConcurrency < 1) {
+ log.warn("AI 最大并发数配置无效({}),使用默认值 10", aiMaxConcurrency);
+ aiMaxConcurrency = 10;
+ }
- int corePoolSize = Math.max(baseCoreSize, aiMaxConcurrency);
+ // 3. 线程池参数优化(核心逻辑)
+ // 核心线程数:取「CPU核心数*2 +1」和「AI最大并发数的1/2」的较小值(避免空闲线程浪费)
+ int corePoolSize = Math.min(cpuCore * 2 + 1, aiMaxConcurrency / 2);
+ // 最大线程数:不超过配置的 AI 最大并发数(避免超预期并发导致限流/资源耗尽)
+ int maxPoolSize = Math.min(cpuCore * 4 + 1, aiMaxConcurrency);
+ // 队列容量:核心线程数*3(上限 50),平衡缓冲和内存占用(避免队列过大导致任务堆积)
+ int queueCapacity = Math.min(corePoolSize * 3, 50);
+ // 4. 初始化线程池
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
+ executor.setMaxPoolSize(maxPoolSize);
+ executor.setQueueCapacity(queueCapacity);
+ executor.setKeepAliveSeconds(120); // 空闲线程存活时间(120秒,适配 AI 长耗时调用)
+ executor.setThreadNamePrefix("ai-exclusive-thread-"); // 线程名前缀(日志排查)
+ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 拒绝策略
- // 最大线程数:从配置文件读取,确保不超过系统承受能力
- // 若配置值小于核心线程数,则强制使用核心线程数作为上限
- executor.setMaxPoolSize(corePoolSize * 2);
-
- // 设置缓冲队列大小,避免任务瞬间涌入导致OOM
- executor.setQueueCapacity(corePoolSize * 5);
+ // 5. 线程池销毁时的优雅关闭(等待任务执行完成,避免任务丢失)
+ executor.setWaitForTasksToCompleteOnShutdown(true);
+ executor.setAwaitTerminationSeconds(30); // 等待 30 秒后强制关闭(适配 AI 长耗时任务)
+ executor.setAwaitTerminationMillis(30 * 1000L); // 兼容低版本 Spring(可选)
- // 超过核心线程数的线程,空闲120秒后销毁,
- executor.setKeepAliveSeconds(120);
+ // 6. 初始化并打印配置(生产环境监控关键)
+ executor.initialize();
+ log.info("AI 专属线程池初始化完成:核心线程数={},最大线程数={},队列容量={},拒绝策略={}",
+ corePoolSize, maxPoolSize, queueCapacity, executor.getThreadPoolExecutor().getRejectedExecutionHandler().getClass().getSimpleName());
- // 线程名称前缀,便于日志排查
- executor.setThreadNamePrefix("ai-exclusive-thread-");
-
- // 当线程池和队列都满时,让提交任务的线程自己执行,避免任务丢失
- executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
-
- executor.initialize();//初始化
return executor;
}
-
-}
+}
\ No newline at end of file
diff --git a/refine-app/src/main/java/com/achobeta/config/ChatModelListenerConfig.java b/refine-app/src/main/java/com/achobeta/config/ChatModelListenerConfig.java
index b34e055..078e684 100644
--- a/refine-app/src/main/java/com/achobeta/config/ChatModelListenerConfig.java
+++ b/refine-app/src/main/java/com/achobeta/config/ChatModelListenerConfig.java
@@ -28,7 +28,7 @@ public void onResponse(ChatModelResponseContext responseContext) {
@Override
public void onError(ChatModelErrorContext errorContext) {
- log.info("onError(): {}", errorContext.error().getMessage());
+ log.warn("onError(): {}", errorContext.error().getMessage());
}
};
}
diff --git a/refine-app/src/main/java/com/achobeta/config/RagConfig.java b/refine-app/src/main/java/com/achobeta/config/RagConfig.java
index 4ea9db1..9f7d305 100644
--- a/refine-app/src/main/java/com/achobeta/config/RagConfig.java
+++ b/refine-app/src/main/java/com/achobeta/config/RagConfig.java
@@ -3,17 +3,22 @@
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.loader.FileSystemDocumentLoader;
import dev.langchain4j.data.document.splitter.DocumentByParagraphSplitter;
+import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.model.embedding.EmbeddingModel;
+import dev.langchain4j.rag.content.Content;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
-import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
+import dev.langchain4j.rag.query.Query;
+import dev.langchain4j.store.embedding.EmbeddingSearchRequest;
+import dev.langchain4j.store.embedding.EmbeddingSearchResult;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
+import dev.langchain4j.store.embedding.filter.Filter;
+import dev.langchain4j.store.embedding.filter.MetadataFilterBuilder;
import dev.langchain4j.store.embedding.pgvector.PgVectorEmbeddingStore;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -101,13 +106,43 @@ public ContentRetriever contentRetriever() {
}
// 自定义内容加载器
- EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
- .embeddingStore(embeddingStore)
- .embeddingModel(qwenEmbeddingModel)
- .maxResults(5) // 最多返回5条结果
- .minScore(0.75) // 过滤掉分数小于0.75的结果
- .build();
- return retriever;
+ return new ContentRetriever() {
+ @Override
+ public List retrieve(Query query) {
+ // 1. 从查询元数据中动态提取业务传入的学科(target)
+ Object obj = query.metadata().invocationContext().chatMemoryId();
+ if (null == obj) {
+ log.warn("RAG检索 - 未传递 file_name 筛选条件,返回空结果");
+ return List.of();
+ }
+ String target = String.valueOf(obj);
+ log.info("RAG检索 - 动态过滤 file_name(模糊匹配): {}", target);
+
+ // 2. 生成查询向量
+ Embedding embeddedQuery = qwenEmbeddingModel.embed(query.text()).content();
+
+ // 3. 用自定义的Filter构建动态过滤条件
+ //Filter filter = MetadataFilterBuilder.metadataKey("subject").isEqualTo(subject);
+
+ //模糊匹配,自定义的Filter构建动态过滤条件
+ Filter filter = MetadataFilterBuilder.metadataKey("file_name").containsString(target);
+
+ // 4. 执行带动态过滤的检索
+ EmbeddingSearchRequest searchRequest = EmbeddingSearchRequest.builder()
+ .queryEmbedding(embeddedQuery)
+ .maxResults(5) // 最多返回5条
+ .minScore(0.75) // 相关度阈值
+ .filter(filter) // 动态过滤条件
+ .build();
+
+ EmbeddingSearchResult searchResult = embeddingStore.search(searchRequest);
+
+ // 5. 转换结果为Content列表
+ return searchResult.matches().stream()
+ .map(match -> Content.from(match.embedded()))
+ .collect(Collectors.toList());
+ }
+ };
}
private void truncateEmbeddingTable() {
diff --git a/refine-app/src/main/java/com/achobeta/config/WebConfig.java b/refine-app/src/main/java/com/achobeta/config/WebConfig.java
index 1744b16..9269031 100644
--- a/refine-app/src/main/java/com/achobeta/config/WebConfig.java
+++ b/refine-app/src/main/java/com/achobeta/config/WebConfig.java
@@ -2,11 +2,17 @@
import com.achobeta.intercepter.LogInterceptor;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.core.task.AsyncTaskExecutor;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import java.util.concurrent.ThreadPoolExecutor;
+
/**
* @author BanTanger 半糖
* @date 2024/11/4
@@ -40,4 +46,24 @@ public void addCorsMappings(CorsRegistry registry) {
.allowCredentials(true);
}
+ @Bean(name = "mvcAsyncTaskExecutor")
+ public AsyncTaskExecutor mvcAsyncTaskExecutor() {
+ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+ int cpuCore = Runtime.getRuntime().availableProcessors();
+ executor.setCorePoolSize(cpuCore + 1); // 框架层面线程数精简
+ executor.setMaxPoolSize(2 * cpuCore + 1);
+ executor.setQueueCapacity(50); // 避免请求堆积
+ executor.setKeepAliveSeconds(60);
+ executor.setThreadNamePrefix("mvc-async-");
+ executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+ executor.initialize();
+ return executor;
+ }
+
+ @Override
+ public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
+ configurer.setTaskExecutor(mvcAsyncTaskExecutor());
+ configurer.setDefaultTimeout(60 * 1000L); // 异步请求超时(适配 AI 长流式响应)
+ }
+
}
diff --git a/refine-app/src/main/java/com/achobeta/jwt/JwtTool.java b/refine-app/src/main/java/com/achobeta/jwt/JwtTool.java
index e82969a..7cea9fc 100644
--- a/refine-app/src/main/java/com/achobeta/jwt/JwtTool.java
+++ b/refine-app/src/main/java/com/achobeta/jwt/JwtTool.java
@@ -24,6 +24,7 @@
import java.util.UUID;
import static com.achobeta.types.common.Constants.USER_REFRESH_TOKEN_KEY;
+import static com.achobeta.types.common.Constants.USER_TOKEN_ID_KEY;
@Component
@EnableConfigurationProperties(JwtProperties.class)
@@ -79,6 +80,11 @@ public String createRefreshToken(String userId) {
userId,
jwtProperties.getRefreshTokenTtl().toMillis()
);
+ redis.setValue(
+ USER_TOKEN_ID_KEY + userId,
+ refreshToken,
+ jwtProperties.getRefreshTokenTtl().toMillis()
+ );
return refreshToken;
}
@@ -86,7 +92,7 @@ public String createRefreshToken(String userId) {
* 通用Token生成方法
*/
private String createToken(Map claims, Duration ttl) {
- claims.put("iat", new Date()); // 添加签发时间
+ claims.put("iat", System.currentTimeMillis()/1000); // 添加签发时间,毫秒值
return JWT.create()
.addPayloads(claims) // 设置载荷
.setExpiresAt(new Date(System.currentTimeMillis() + ttl.toMillis())) // 过期时间
@@ -139,7 +145,7 @@ private TokenPayload parseToken(String token, String expectedType) {
log.debug("Token有效:{}", jwt);
} catch (ValidateException e) {
// 细分异常:过期/签名无效
- if (e.getMessage().contains("expired")) {
+ if (e.getMessage().contains("is before now:")) {
throw new UnauthorizedException(expectedType + "-token已过期", e);
} else {
throw new UnauthorizedException(expectedType + "-token签名无效", e);
@@ -151,7 +157,7 @@ private TokenPayload parseToken(String token, String expectedType) {
// 4. 校验Token类型
String actualType = Convert.toStr(jwt.getPayload("type"));
if (!StrUtil.equals(actualType, expectedType)) {
- throw new UnauthorizedException("无效的" + expectedType + "-token:类型错误");
+ throw new UnauthorizedException("无效的" + expectedType + "-token:token类型错误");
}
// 5. 提取用户ID
@@ -167,7 +173,7 @@ private TokenPayload parseToken(String token, String expectedType) {
}
// 7. 提取iat
- Date iat = Convert.toDate(jwt.getPayload("iat"));
+ Long iat = Convert.toLong(jwt.getPayload("iat"));
if (iat == null) {
throw new UnauthorizedException("无效的" + expectedType + "-token:缺失签发时间");
}
@@ -190,7 +196,7 @@ public Map refreshAccessToken(String refreshToken, Map 0 && remainingTime < (jwtProperties.getRefreshTokenTtl().toMillis() / 2)) {
// 同时会自动更新 Redis 的refresh-token
@@ -232,11 +238,16 @@ public void invalidateRefreshToken(String refreshToken) {
/**
* 提取refresh-token中的iat值
*/
- public Date getRefreshTokenIat(String refreshToken) {
+ public Long getRefreshTokenIat(String refreshToken) {
return parseToken(refreshToken, "refresh").getIat();
}
+ public String getRefreshToken4UserId(String userId) {
+ return redis.getValue(USER_TOKEN_ID_KEY + userId);
+ }
+
+
/**
* 内部类:封装解析后的Token载荷
*/
@@ -245,7 +256,7 @@ public Date getRefreshTokenIat(String refreshToken) {
private static class TokenPayload {
private final String userId;
private final String jti;
- private final Date iat;
+ private final Long iat;
}
}
\ No newline at end of file
diff --git a/refine-app/src/main/resources/AiAnalyze.txt b/refine-app/src/main/resources/AiAnalyze.txt
index 3685ca5..9eb4f3d 100644
--- a/refine-app/src/main/resources/AiAnalyze.txt
+++ b/refine-app/src/main/resources/AiAnalyze.txt
@@ -1,3 +1,5 @@
你是一个专业判题助手,可以判断答案是否正确并给出正确答案,根据问题针对学生的答案详细解析,解析详细一点
(对于填数据类型的填空题,允许答案有少量误差)
+一步一步详细分析答案是怎么得出来的
不添加其他多余文字,例如:"希望这可以帮助澄清你的疑问!如果有其他问题,请随时告诉我。"
+以纯文本形式输出,不要带有没必要的符号如**
\ No newline at end of file
diff --git a/refine-app/src/main/resources/AnalyzeKnowledge.txt b/refine-app/src/main/resources/AnalyzeKnowledge.txt
new file mode 100644
index 0000000..1cd6eb5
--- /dev/null
+++ b/refine-app/src/main/resources/AnalyzeKnowledge.txt
@@ -0,0 +1,15 @@
+我只给一道题目给你
+
+请严格按照以下规则提取题目核心知识点,仅输出结果,不添加任何额外解释:
+
+1. 领域分类优先:先明确题目所属大领域(如“化学”“Java编程”“数据结构”“小学数学”),再补充子分类(如需),最后聚焦核心考点;
+2. 去冗余去细节:忽略题目中的具体数值、案例、干扰项等非关键信息,只保留“考察什么能力/概念/公式/方法”;
+3. 格式统一规范:采用“大领域-子分类-核心知识点”的层级结构(子分类可省略,核心知识点不超过15字);
+4. 精准不泛化:不扩大知识点范围,也不缩小(如题目考“氧化还原反应配平”,不写成“化学反应”或“氧化还原反应细节”);
+5. 多知识点限制:若题目跨多个考点,仅提取2个最核心的,用“|”分隔。
+
+示例:
+- 题目:“写出H₂+O₂=H₂O的配平化学方程式” → 输出:“化学-化学反应-氧化还原反应配平”
+- 题目:“Spring Boot中如何通过注解实现属性注入?” → 输出:“Java编程-Spring Boot-@Value属性注入”
+- 题目:“一个长方形长5cm、宽3cm,求它的周长” → 输出:“小学数学-几何图形-长方形周长计算”
+- 题目:“简述哈希表的原理及解决冲突的方法” → 输出:“数据结构-哈希表-原理|哈希冲突解决”
diff --git a/refine-app/src/main/resources/com/achobeta/infrastructure/dao/UserDataMapper.xml b/refine-app/src/main/resources/com/achobeta/infrastructure/dao/UserDataMapper.xml
new file mode 100644
index 0000000..726dd42
--- /dev/null
+++ b/refine-app/src/main/resources/com/achobeta/infrastructure/dao/UserDataMapper.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+ INSERT INTO UserData (user_id, questions_num, review_rate, hard_questions, study_time)
+ VALUES (#{userId}, 0, 0.0, 0, 0)
+
+
\ No newline at end of file
diff --git "a/refine-app/src/main/resources/docs/\345\214\226\345\255\246.txt" "b/refine-app/src/main/resources/docs/\345\214\226\345\255\246.txt"
new file mode 100644
index 0000000..6851ceb
--- /dev/null
+++ "b/refine-app/src/main/resources/docs/\345\214\226\345\255\246.txt"
@@ -0,0 +1 @@
+化学能是没用的
\ No newline at end of file
diff --git "a/refine-app/src/main/resources/docs/\346\265\213\350\257\225\346\226\207\346\241\243.txt" "b/refine-app/src/main/resources/docs/\346\265\213\350\257\225\346\226\207\346\241\243.txt"
deleted file mode 100644
index 78182ac..0000000
--- "a/refine-app/src/main/resources/docs/\346\265\213\350\257\225\346\226\207\346\241\243.txt"
+++ /dev/null
@@ -1 +0,0 @@
-111test
\ No newline at end of file
diff --git a/refine-app/src/main/resources/mybatis/mapper/AILearningSuggessionRepository.xml b/refine-app/src/main/resources/mybatis/mapper/AILearningSuggessionRepository.xml
index 4aadb99..543a9f8 100644
--- a/refine-app/src/main/resources/mybatis/mapper/AILearningSuggessionRepository.xml
+++ b/refine-app/src/main/resources/mybatis/mapper/AILearningSuggessionRepository.xml
@@ -5,6 +5,6 @@
\ No newline at end of file
diff --git a/refine-app/src/main/resources/mybatis/mapper/MistakeQuestion_Mapper.xml b/refine-app/src/main/resources/mybatis/mapper/MistakeQuestion_Mapper.xml
index 2dcae3a..27c3a7f 100644
--- a/refine-app/src/main/resources/mybatis/mapper/MistakeQuestion_Mapper.xml
+++ b/refine-app/src/main/resources/mybatis/mapper/MistakeQuestion_Mapper.xml
@@ -28,9 +28,9 @@
- INSERT INTO mistake_question (
+ INSERT INTO MistakeQuestion (
user_id, question_id, question_content, subject, is_careless, is_unfamiliar, is_calculate_err,
- is_time_shortage, other_reason, knowledge_point_id, study_note, question_status,
+ is_time_shortage, other_reason, other_reason_flag, knowledge_point_id, study_note, question_status,
create_time, update_time
) VALUES (
#{userId,jdbcType=VARCHAR},
@@ -54,14 +54,14 @@
@@ -69,12 +69,12 @@
- UPDATE mistake_question
+ UPDATE MistakeQuestion
SET
is_careless = #{isCareless,jdbcType=INTEGER},
is_unfamiliar = #{isUnfamiliar,jdbcType=INTEGER},
@@ -87,14 +87,14 @@
- UPDATE mistake_question
+ UPDATE MistakeQuestion
SET other_reason = #{otherReasonText,jdbcType=VARCHAR},
update_time = NOW()
WHERE user_id = #{userId,jdbcType=VARCHAR} AND question_id = #{questionId,jdbcType=VARCHAR}
- UPDATE mistake_question
+ UPDATE MistakeQuestion
SET study_note = #{studyNote,jdbcType=LONGVARCHAR},
update_time = NOW()
WHERE user_id = #{userId,jdbcType=VARCHAR} AND question_id = #{questionId,jdbcType=VARCHAR}
diff --git a/refine-app/src/main/resources/mybatis/mapper/ReviewQuestionMapper.xml b/refine-app/src/main/resources/mybatis/mapper/ReviewQuestionMapper.xml
index add7378..2888332 100644
--- a/refine-app/src/main/resources/mybatis/mapper/ReviewQuestionMapper.xml
+++ b/refine-app/src/main/resources/mybatis/mapper/ReviewQuestionMapper.xml
@@ -2,21 +2,21 @@
- delete from MistakeQuestion where user_id = #{userId} and id in
+ delete from MistakeQuestion where user_id = #{userId} and question_id in
#{questionId}
diff --git a/refine-infrastructure/src/main/resources/mybatis/mapper/VectorMapper.xml b/refine-app/src/main/resources/mybatis/mapper/VectorMapper.xml
similarity index 77%
rename from refine-infrastructure/src/main/resources/mybatis/mapper/VectorMapper.xml
rename to refine-app/src/main/resources/mybatis/mapper/VectorMapper.xml
index 6fed997..43e0953 100644
--- a/refine-infrastructure/src/main/resources/mybatis/mapper/VectorMapper.xml
+++ b/refine-app/src/main/resources/mybatis/mapper/VectorMapper.xml
@@ -1,38 +1,41 @@
-
+
-
+
INSERT INTO user_learning_vectors
- (user_id, question_id, action_type, question_content, subject, knowledge_point_id, embedding, metadata)
+ (user_id, question_id, action_type, question_content, subject, knowledge_point_id)
VALUES
- (#{userId}, #{questionId}, #{actionType}, #{questionContent}, #{subject}, #{knowledgePointId},
- #{embedding}::vector, #{metadata}::jsonb)
+ (#{userId}, #{questionId}, #{actionType}, #{questionContent}, #{subject}, #{knowledgePointId})
-
+
-
+
-
+
@@ -56,9 +59,11 @@
AND is_active = true
ORDER BY created_at DESC
-
+