feat(student): 实现学生学习分析功能
- 新增AnalyzeStudentStudyReqVO用于分析请求参数封装 - StudentService接口新增analyzeStudentStudy方法及其实现 - 实现分析逻辑,查询最近7天学生考试及单词掌握记录,构造分析数据 - 通过DifyArticleClient调用外部AI服务生成学习分析结果 - 使用Redis缓存分析结果,设置3天过期 - 新增ExamWordsJudgeResultDetail和WordMasteryDetail数据模型 - Mapper新增支持根据学生ID和时间范围查询考试结果和单词掌握日志 - DifyArticleClient新增sendStudentAnalyze方法调用分析接口 - 前端学生页面新增学习分析面板及调用接口,支持超时设置 - 修改路由权限配置,允许访问学习分析接口 - 添加markdown-it库支持分析结果富文本渲染 - 移除RoleServiceImpl中redis设置过期时间,改为永久保存
This commit is contained in:
@@ -33,6 +33,7 @@ public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
.notMatch("/student/detail")
|
||||
.notMatch("/studentLessonPlans/list")
|
||||
.notMatch("/studentLessonPlans/history")
|
||||
.notMatch("/student/analyze")
|
||||
.notMatch("/unit/list")
|
||||
.notMatch("/vocabulary/list")
|
||||
.notMatch("/plan/download")
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.yinlihupo.enlish.service.constant;
|
||||
|
||||
public class StudentConstant {
|
||||
|
||||
public static final String ANALYZE_STUDENT_STUDY = "analyzeStudentStudy";
|
||||
|
||||
public static String buildAnalyzeStudentStudyKey(Integer studentId) {
|
||||
return ANALYZE_STUDENT_STUDY + ":" + studentId;
|
||||
}
|
||||
}
|
||||
@@ -85,4 +85,11 @@ public class StudentController {
|
||||
studentService.deleteStudent(deleteStudentReqVO.getStudentId());
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@PostMapping("analyze")
|
||||
@ApiOperationLog(description = "学生学习分析")
|
||||
public Response<String> analyzeStudentStudy(@RequestBody AnalyzeStudentStudyReqVO analyzeStudentStudyReqVO) {
|
||||
String analyzeStudentStudy = studentService.analyzeStudentStudy(analyzeStudentStudyReqVO.getStudentId());
|
||||
return Response.success(analyzeStudentStudy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,4 +22,6 @@ public interface ExamWordsJudgeResultDOMapper {
|
||||
ExamWordsJudgeResultDO selectDetailById(@Param("id") Integer id);
|
||||
|
||||
List<ExamWordsJudgeResultDO> selectByStudentId(@Param("studentId") Integer studentId);
|
||||
|
||||
List<ExamWordsJudgeResultDO> selectByStudentIdAndLimitTime(@Param("studentId") Integer studentId);
|
||||
}
|
||||
@@ -16,4 +16,6 @@ public interface WordMasteryLogDOMapper {
|
||||
int batchUpdateStudentMastery(@Param("wordMasteryLogDOs") List<WordMasteryLogDO> wordMasteryLogDOs);
|
||||
|
||||
int selectStudentStrengthCount(@Param("studentId") Integer studentId);
|
||||
|
||||
List<WordMasteryLogDO> selectByStudentIdAndLimitTime(@Param("studentId") Integer studentId);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.yinlihupo.enlish.service.model.bo.exam;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Builder
|
||||
public class ExamWordsJudgeResultDetail {
|
||||
|
||||
private Integer correctWordCount;
|
||||
|
||||
private Integer wrongWordCount;
|
||||
|
||||
private LocalDateTime startDate;
|
||||
|
||||
private List<String> correctWords;
|
||||
|
||||
private List<String> wrongWords;
|
||||
|
||||
private String msg;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.yinlihupo.enlish.service.model.bo.exam;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Builder
|
||||
public class WordMasteryDetail {
|
||||
|
||||
private String word;
|
||||
|
||||
private Integer reviewCount;
|
||||
|
||||
private Double memoryStrength;
|
||||
|
||||
private LocalDateTime update_time;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.yinlihupo.enlish.service.model.vo.student;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Data
|
||||
@Builder
|
||||
public class AnalyzeStudentStudyReqVO {
|
||||
|
||||
private Integer studentId;
|
||||
}
|
||||
@@ -20,4 +20,6 @@ public interface StudentService {
|
||||
void addStudent(AddStudentReqVO addStudentReqVO);
|
||||
|
||||
void deleteStudent(Integer studentId);
|
||||
|
||||
String analyzeStudentStudy(Integer studentId);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class RoleServiceImpl implements RoleService {
|
||||
List<RoleDO> roleDOs = roleIds.stream().map(roleId2RoleDO::get).toList();
|
||||
List<String> user2RoleKeys = roleDOs.stream().map(RoleDO::getRoleKey).toList();
|
||||
log.info("将用户 {} 的角色同步到 redis 中, {}", userId, roleKeys);
|
||||
stringRedisTemplate.opsForValue().set(RoleConstants.buildUserRoleKey(userId), JsonUtils.toJsonString(user2RoleKeys), 60 * 60 * 24);
|
||||
stringRedisTemplate.opsForValue().set(RoleConstants.buildUserRoleKey(userId), JsonUtils.toJsonString(user2RoleKeys));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
package com.yinlihupo.enlish.service.service.student;
|
||||
|
||||
|
||||
import com.yinlihupo.enlish.service.domain.dataobject.ClassDO;
|
||||
import com.yinlihupo.enlish.service.domain.dataobject.GradeDO;
|
||||
import com.yinlihupo.enlish.service.domain.dataobject.StudentDO;
|
||||
import com.yinlihupo.enlish.service.constant.StudentConstant;
|
||||
import com.yinlihupo.enlish.service.domain.dataobject.*;
|
||||
import com.yinlihupo.enlish.service.domain.mapper.*;
|
||||
import com.yinlihupo.enlish.service.model.bo.StudentDetail;
|
||||
import com.yinlihupo.enlish.service.model.bo.exam.ExamWordsJudgeResultDetail;
|
||||
import com.yinlihupo.enlish.service.model.bo.exam.WordMasteryDetail;
|
||||
import com.yinlihupo.enlish.service.model.vo.student.AddStudentReqVO;
|
||||
import com.yinlihupo.enlish.service.service.StudentService;
|
||||
import com.yinlihupo.enlish.service.utils.DifyArticleClient;
|
||||
import com.yinlihupo.framework.common.util.JsonUtils;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@@ -29,6 +34,12 @@ public class StudentServiceImpl implements StudentService {
|
||||
private VocabularyBankDOMapper vocabularyBankMapper;
|
||||
@Resource
|
||||
private WordMasteryLogDOMapper wordMasteryLogDOMapper;
|
||||
@Resource
|
||||
private ExamWordsJudgeResultDOMapper examWordsJudgeResultDOMapper;
|
||||
@Resource
|
||||
private DifyArticleClient difyArticleClient;
|
||||
@Resource
|
||||
private RedisTemplate<String, Object> redisTemplate;
|
||||
|
||||
@Override
|
||||
public List<StudentDO> getStudentsByClassIdAndGradeId(Integer classId, Integer gradeId, String name, Integer pageNo, Integer pageSize) {
|
||||
@@ -94,4 +105,66 @@ public class StudentServiceImpl implements StudentService {
|
||||
public void deleteStudent(Integer studentId) {
|
||||
studentDOMapper.deleteById(studentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String analyzeStudentStudy(Integer studentId) {
|
||||
|
||||
String key = StudentConstant.buildAnalyzeStudentStudyKey(studentId);
|
||||
if (redisTemplate.hasKey(key)) {
|
||||
Object ans = redisTemplate.opsForValue().get(key);
|
||||
return JsonUtils.toJsonString(ans);
|
||||
}
|
||||
|
||||
List<ExamWordsJudgeResultDO> examWordsJudgeResultDOS = examWordsJudgeResultDOMapper.selectByStudentIdAndLimitTime(studentId);
|
||||
|
||||
List<Integer> wordIds = new java.util.ArrayList<>(examWordsJudgeResultDOS.stream().map(ExamWordsJudgeResultDO::getCorrectWordIds).flatMap(List::stream).toList());
|
||||
wordIds.addAll(examWordsJudgeResultDOS.stream().map(ExamWordsJudgeResultDO::getWrongWordIds).flatMap(List::stream).toList());
|
||||
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankMapper.selectVocabularyBankDOListByIds(wordIds);
|
||||
Map<Integer, VocabularyBankDO> id2Word = vocabularyBankDOS.stream().collect(Collectors.toMap(VocabularyBankDO::getId, vocabularyBankDO -> vocabularyBankDO));
|
||||
|
||||
List<ExamWordsJudgeResultDetail> examWordsJudgeResultDetails = new ArrayList<>();
|
||||
for (ExamWordsJudgeResultDO examWordsJudgeResultDO : examWordsJudgeResultDOS) {
|
||||
List<Integer> correctWordIds = examWordsJudgeResultDO.getCorrectWordIds();
|
||||
List<String> correctWords = correctWordIds.stream().map(id2Word::get).map(VocabularyBankDO::getWord).toList();
|
||||
List<Integer> wrongWordIds = examWordsJudgeResultDO.getWrongWordIds();
|
||||
List<String> wrongWords = wrongWordIds.stream().map(id2Word::get).map(VocabularyBankDO::getWord).toList();
|
||||
|
||||
examWordsJudgeResultDetails.add(ExamWordsJudgeResultDetail.builder()
|
||||
.correctWordCount(examWordsJudgeResultDO.getCorrectWordCount())
|
||||
.wrongWordCount(examWordsJudgeResultDO.getWrongWordCount())
|
||||
.startDate(examWordsJudgeResultDO.getStartDate())
|
||||
.correctWords(correctWords)
|
||||
.wrongWords(wrongWords)
|
||||
.msg(examWordsJudgeResultDO.getMsg()).build()
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, Object> studentStudyInfo = new HashMap<>();
|
||||
studentStudyInfo.put("考试记录", examWordsJudgeResultDetails);
|
||||
|
||||
List<WordMasteryLogDO> wordMasteryLogDOS = wordMasteryLogDOMapper.selectByStudentIdAndLimitTime(studentId);
|
||||
List<VocabularyBankDO> masteredWords = vocabularyBankMapper.selectVocabularyBankDOListByIds(wordMasteryLogDOS.stream().map(WordMasteryLogDO::getWordId).toList());
|
||||
Map<Integer, VocabularyBankDO> id2MasteryWord = masteredWords.stream().collect(Collectors.toMap(VocabularyBankDO::getId, vocabularyBankDO -> vocabularyBankDO));
|
||||
List<WordMasteryDetail> wordMasteryDetails = new ArrayList<>();
|
||||
for (WordMasteryLogDO wordMasteryLogDO : wordMasteryLogDOS) {
|
||||
wordMasteryDetails.add(WordMasteryDetail.builder()
|
||||
.word(id2MasteryWord.get(wordMasteryLogDO.getWordId()).getWord())
|
||||
.reviewCount(wordMasteryLogDO.getReviewCount())
|
||||
.memoryStrength(wordMasteryLogDO.getMemoryStrength())
|
||||
.update_time(wordMasteryLogDO.getUpdate_time())
|
||||
.build());
|
||||
}
|
||||
studentStudyInfo.put("单词掌握情况", wordMasteryDetails);
|
||||
|
||||
try {
|
||||
String analyze = difyArticleClient.sendStudentAnalyze(JsonUtils.toJsonString(studentStudyInfo)).getAnswer();
|
||||
// 设置过期时间 3 天
|
||||
redisTemplate.opsForValue().set(key, analyze);
|
||||
redisTemplate.expire(key, 3, TimeUnit.DAYS);
|
||||
return analyze;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public class DifyArticleClient {
|
||||
|
||||
@Value("${ai.key}")
|
||||
private String apiKey;
|
||||
private String anaKey = "app-hrUFcopdcpnflsvpHWRuBfCp";
|
||||
@Value("${ai.url}")
|
||||
private String baseUrl;
|
||||
private final HttpClient httpClient;
|
||||
@@ -35,6 +36,40 @@ public class DifyArticleClient {
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
public DifyResponse sendStudentAnalyze(String query) throws Exception {
|
||||
String endpoint = this.baseUrl;
|
||||
|
||||
// 1. 构建请求体对象
|
||||
ChatRequest payload = new ChatRequest();
|
||||
payload.setQuery(query);
|
||||
payload.setUser(String.valueOf(1));
|
||||
payload.setResponseMode("blocking"); // 使用阻塞模式,一次性返回
|
||||
payload.setInputs(new HashMap<>());
|
||||
|
||||
// 2. 序列化为 JSON 字符串
|
||||
String jsonBody = objectMapper.writeValueAsString(payload);
|
||||
|
||||
// 3. 构建 HTTP 请求
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(endpoint))
|
||||
.header("Authorization", "Bearer " + anaKey)
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
||||
.timeout(Duration.ofSeconds(30)) // 读取超时
|
||||
.build();
|
||||
|
||||
// 4. 发送请求
|
||||
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
// 5. 检查状态码
|
||||
if (response.statusCode() != 200) {
|
||||
throw new RuntimeException("Dify 请求失败: HTTP " + response.statusCode() + " | Body: " + response.body());
|
||||
}
|
||||
|
||||
// 6. 反序列化响应体
|
||||
return objectMapper.readValue(response.body(), DifyResponse.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送对话请求 (阻塞模式)
|
||||
*
|
||||
@@ -64,7 +99,7 @@ public class DifyArticleClient {
|
||||
// 3. 构建 HTTP 请求
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(endpoint))
|
||||
.header("Authorization", "Bearer " + this.apiKey)
|
||||
.header("Authorization", "Bearer " + apiKey)
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
|
||||
.timeout(Duration.ofSeconds(30)) // 读取超时
|
||||
|
||||
@@ -74,5 +74,11 @@
|
||||
order by start_date desc
|
||||
limit 500;
|
||||
</select>
|
||||
<select id="selectByStudentIdAndLimitTime" resultMap="ResultMapWithBLOBs">
|
||||
select *
|
||||
from exam_words_judge_result
|
||||
where student_id = #{studentId}
|
||||
and start_date between date_sub(now(), interval 7 day) and now()
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
@@ -52,4 +52,10 @@
|
||||
</foreach>
|
||||
</update>
|
||||
|
||||
<select id="selectByStudentIdAndLimitTime" resultMap="BaseResultMap">
|
||||
select *
|
||||
from word_mastery_log
|
||||
where student_id = #{studentId}
|
||||
and update_time between date_sub(now(), interval 7 day) and now()
|
||||
</select>
|
||||
</mapper>
|
||||
@@ -2,6 +2,8 @@ package com.yinlihupo.enlish.service.service.exam;
|
||||
|
||||
import com.yinlihupo.enlish.service.domain.dataobject.ExamWordsJudgeResultDO;
|
||||
import com.yinlihupo.enlish.service.service.ExamWordsJudgeService;
|
||||
import com.yinlihupo.enlish.service.service.StudentService;
|
||||
import com.yinlihupo.enlish.service.utils.DifyArticleClient;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -16,7 +18,10 @@ public class ExamWordsJudgeServiceTest {
|
||||
|
||||
@Resource
|
||||
private ExamWordsJudgeService examWordsJudgeService;
|
||||
|
||||
@Resource
|
||||
private StudentService studentService;
|
||||
@Resource
|
||||
private DifyArticleClient difyArticleClient;
|
||||
@Test
|
||||
public void judgeExamWords() {
|
||||
examWordsJudgeService.judgeExamWords(1);
|
||||
@@ -27,4 +32,17 @@ public class ExamWordsJudgeServiceTest {
|
||||
List<ExamWordsJudgeResultDO> examWordsJudgeResult = examWordsJudgeService.getExamWordsJudgeResult(1, 10);
|
||||
log.info("examWordsJudgeResult:{}", examWordsJudgeResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void selectExamWordsJudgeResult2() {
|
||||
String s = studentService.analyzeStudentStudy(1);
|
||||
try {
|
||||
DifyArticleClient.DifyResponse difyResponse = difyArticleClient.sendStudentAnalyze(s);
|
||||
String answer = difyResponse.getAnswer();
|
||||
log.info("answer:{}", answer);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
log.info("s:{}", s);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user