feat(lessonplan): 实现基于AI的学案自动生成与管理功能

- 新增DifyArticleClient工具类,实现基于Dify API的对话与文本生成功能
- 创建LessonPlansService接口及其实现,实现学案按天生成及存储
- 设计LessonPlansDO和StudentLessonPlansDO数据对象及对应MyBatis映射和数据库操作
- 扩展VocabularyBankDO实体及Mapper,支持查询单元词汇和学生未掌握词汇
- 利用deepoove-poi模板技术生成Word格式的学习计划文档,包含词汇、复习和练习
- 开发StringToPlanMapUtil工具类,解析AI返回结果为结构化学案内容
- 新增JUnit测试用例验证AI对话功能及学案生成逻辑正确性
- 更新Spring Boot配置,添加AI接口地址及密钥等参数
- 在前端Vue项目中新建学案页面,路由配置及导航菜单支持学案访问
This commit is contained in:
lbw
2025-12-16 19:08:58 +08:00
parent d027c9c7e6
commit 7f41036193
26 changed files with 831 additions and 5 deletions

View File

@@ -0,0 +1,10 @@
package com.yinlihupo.enlish.service.constant;
public interface LessonPlanConstant {
String TITLE = "Title";
String PASSAGE = "The Passage";
String QUIZ = "Quiz";
String ANSWER_KEY_EXPLANATION = "Answer Key & Explanation";
String FULL_TRANSLATION = "Full Translation";
}

View File

@@ -0,0 +1,27 @@
package com.yinlihupo.enlish.service.domain.dataobject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class LessonPlansDO {
private Integer id;
private String title;
private String gradeId;
private Integer unitId;
private Date createdAt;
private String contentDetails;
}

View File

@@ -0,0 +1,32 @@
package com.yinlihupo.enlish.service.domain.dataobject;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.util.Date;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class StudentLessonPlansDO {
private Integer id;
private Integer studentId;
private Integer planId;
private Date startTime;
private BigDecimal masteryRat;
private Integer totalCount;
private String memorizedWordsJson;
private String unmemorizedWordsJson;
}

View File

@@ -10,6 +10,7 @@ import lombok.NoArgsConstructor;
@Data
@Builder
public class VocabularyBankDO {
private Integer id;
private String word;
@@ -18,6 +19,8 @@ public class VocabularyBankDO {
private String pronunciation;
private String pos;
private Integer unitId;
}

View File

@@ -1,5 +1,6 @@
package com.yinlihupo.enlish.service.domain.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.GradeUnitDO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@@ -7,4 +8,6 @@ import java.util.List;
public interface GradeUnitDOMapper {
List<Integer> selectUnitIdsByGradeId(@Param("gradeId") Integer gradeId);
GradeUnitDO selectByUnitId(@Param("unitId") Integer unitId);
}

View File

@@ -0,0 +1,10 @@
package com.yinlihupo.enlish.service.domain.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO;
public interface LessonPlansDOMapper {
void insert(LessonPlansDO lessonPlansDO);
LessonPlansDO selectById(Integer id);
}

View File

@@ -0,0 +1,8 @@
package com.yinlihupo.enlish.service.domain.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.StudentLessonPlansDO;
public interface StudentLessonPlansDOMapper {
void insert(StudentLessonPlansDO studentLessonPlansDO);
}

View File

@@ -14,4 +14,10 @@ public interface VocabularyBankDOMapper {
List<VocabularyBankDO> selectVocabularyBankDOListByIds(@Param("ids") List<Integer> ids);
List<Integer> selectAllIds();
List<VocabularyBankDO> selectVocabularyBankDOAllByUnitId(@Param("unitId") Integer unitId);
List<VocabularyBankDO> selectVocabularyBankListStudentNotMaster(@Param("gradeId") Integer gradeId, @Param("studentId") Integer studentId);
List<VocabularyBankDO> selectVocabularyBankListSelfCheck(@Param("gradeId") Integer gradeId, @Param("studentId") Integer studentId, @Param("ids") List<Integer> ids, @Param("wordCount") Integer wordCount);
}

View File

@@ -0,0 +1,5 @@
package com.yinlihupo.enlish.service.service;
public interface LessonPlansService {
void generateLessonPlans(Integer studentId, Integer unitId);
}

View File

@@ -0,0 +1,291 @@
package com.yinlihupo.enlish.service.service.plan;
import com.deepoove.poi.XWPFTemplate;
import com.deepoove.poi.config.Configure;
import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy;
import com.yinlihupo.enlish.service.constant.LessonPlanConstant;
import com.yinlihupo.enlish.service.domain.dataobject.*;
import com.yinlihupo.enlish.service.domain.mapper.*;
import com.yinlihupo.enlish.service.service.LessonPlansService;
import com.yinlihupo.enlish.service.utils.DifyArticleClient;
import com.yinlihupo.enlish.service.utils.StringToPlanMapUtil;
import com.yinlihupo.framework.common.util.JsonUtils;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;
@Service
@Slf4j
public class LessonPlansServiceImpl implements LessonPlansService {
@Resource
private LessonPlansDOMapper lessonPlansDOMapper;
@Resource
private StudentLessonPlansDOMapper studentLessonPlansDOMapper;
@Resource
private VocabularyBankDOMapper vocabularyBankDOMapper;
@Resource
private UnitDOMapper unitDOMapper;
@Resource
private GradeUnitDOMapper gradeUnitDOMapper;
@Resource
private GradeDOMapper gradeDOMapper;
@Resource
private DifyArticleClient difyArticleClient;
@Value("${templates.plan.weekday}")
private String planWeekday;
@Value("${templates.plan.weekend}")
private String planWeekend;
@Override
public void generateLessonPlans(Integer studentId, Integer unitId) {
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOAllByUnitId(unitId);
UnitDO unitDO = unitDOMapper.selectByPrimaryKey(unitId);
GradeUnitDO gradeUnitDO = gradeUnitDOMapper.selectByUnitId(unitId);
GradeDO gradeDO = gradeDOMapper.selectById(gradeUnitDO.getGradeId());
// 补差词汇所用词汇的
List<VocabularyBankDO> vocabularyBankListStudentNotMaster = getVocabListRandom(vocabularyBankDOMapper
.selectVocabularyBankListStudentNotMaster(gradeUnitDO.getGradeId(), studentId), 50);
int gapSize = vocabularyBankListStudentNotMaster.size();
int countGap = gapSize / 5;
int syncSize = vocabularyBankDOS.size();
int countSync = syncSize / 5;
int checkTotal = 50;
List<List<VocabularyBankDO>> weeksSync = new ArrayList<>();
List<List<VocabularyBankDO>> weeksGap = new ArrayList<>();
for (int i = 0; i < 5; i++) {
List<VocabularyBankDO> syncVocabList = vocabularyBankDOS.subList(i * countSync, Math.min((i + 1) * countSync, syncSize));
List<VocabularyBankDO> gapVocabList = vocabularyBankListStudentNotMaster.subList(i * countGap, Math.min(i * countGap + countGap, gapSize));
weeksSync.add(syncVocabList);
weeksGap.add(gapVocabList);
List<VocabularyBankDO> reviewVocabList = new ArrayList<>();
List<VocabularyBankDO> checkList = new ArrayList<>();
// 艾宾浩斯遗忘曲线
switch (i) {
case 1 -> reviewVocabList.addAll(syncVocabList);
case 2 -> {
reviewVocabList.addAll(weeksSync.get(0));
reviewVocabList.addAll(weeksSync.get(1));
checkList.addAll(weeksSync.get(1));
checkList.addAll(weeksSync.get(1));
}
case 3 -> {
reviewVocabList.addAll(weeksSync.get(1));
reviewVocabList.addAll(weeksGap.get(2));
checkList.addAll(weeksSync.get(1));
checkList.addAll(weeksSync.get(2));
}
case 4 -> {
reviewVocabList.addAll(weeksSync.get(2));
reviewVocabList.addAll(weeksGap.get(3));
checkList.addAll(weeksSync.get(2));
checkList.addAll(weeksSync.get(3));
}
}
List<VocabularyBankDO> checkWords = vocabularyBankDOMapper
.selectVocabularyBankListSelfCheck(gradeUnitDO.getGradeId(), studentId, checkList.stream().map(VocabularyBankDO::getId).toList(), Math.max(checkTotal - checkList.size(), 0));
checkList.addAll(checkWords);
Map<String, Object> lessonPlanMap = null;
try {
lessonPlanMap = generateWeekdayPlans(syncVocabList, gapVocabList, reviewVocabList, checkList, i + 1, gradeDO, unitDO, studentId);
LessonPlansDO lessonPlansDO = LessonPlansDO.builder()
.title(lessonPlanMap.get("title").toString())
.gradeId(gradeDO.getId().toString())
.unitId(unitDO.getId())
.createdAt(new Date())
.contentDetails(JsonUtils.toJsonString(lessonPlanMap))
.build();
lessonPlansDOMapper.insert(lessonPlansDO);
StudentLessonPlansDO studentLessonPlansDO = StudentLessonPlansDO.builder()
.studentId(studentId)
.planId(lessonPlansDO.getId())
.build();
studentLessonPlansDOMapper.insert(studentLessonPlansDO);
} catch (Exception e) {
throw new RuntimeException(e);
}
log.info("生成第{}天计划成功", i + 1);
}
try {
int syncWeekender = syncSize / 2;
for (int i = 0; i < 2; i++) {
List<VocabularyBankDO> checkList = vocabularyBankDOS.subList(i * syncWeekender, Math.min((i + 1) * syncWeekender, syncSize));
Map<String, Object> map = generateWeekendPlans(checkList, i + 6, gradeDO, unitDO, studentId);
LessonPlansDO lessonPlansDO = LessonPlansDO.builder()
.title(map.get("title").toString())
.gradeId(gradeDO.getId().toString())
.unitId(unitDO.getId())
.createdAt(new Date())
.contentDetails(JsonUtils.toJsonString(map))
.build();
lessonPlansDOMapper.insert(lessonPlansDO);
StudentLessonPlansDO studentLessonPlansDO = StudentLessonPlansDO.builder()
.studentId(studentId)
.planId(lessonPlansDO.getId())
.build();
studentLessonPlansDOMapper.insert(studentLessonPlansDO);
log.info("生成第{}天计划成功", i + 6);
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private Map<String, Object> generateWeekendPlans(List<VocabularyBankDO> checkList,
int day,
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws IOException {
Map<String, Object> data = new HashMap<>();
data.put("title", "" + day + "" + "复习" + gradeDO.getTitle() + unitDO.getTitle() + studentId);
data.put("checkList", checkList);
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
Configure config = Configure.builder()
.bind("checkList", policy)
.build();
XWPFTemplate template = XWPFTemplate.compile(planWeekend, config);
template.render(data);
template.writeAndClose(new FileOutputStream("C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\tmp\\word" + "复习" + day + ".docx"));
return data;
}
private Map<String, Object> generateWeekdayPlans(List<VocabularyBankDO> syncVocabList,
List<VocabularyBankDO> gapVocabList,
List<VocabularyBankDO> reviewVocabList,
List<VocabularyBankDO> checkList,
int day,
GradeDO gradeDO, UnitDO unitDO, Integer studentId) throws Exception {
String title = gradeDO.getTitle() + " " + unitDO.getTitle() + " " + "" + day + "" + studentId;
Map<String, Object> data = new HashMap<>();
data.put("title", title);
data.put("syncVocabList", syncVocabList);
data.put("gapVocabList", gapVocabList);
data.put("reviewVocabList", reviewVocabList);
data.put("checkList", checkList);
data.put("checkListAns", checkList);
// 中译英
List<VocabularyBankDO> drillRound1 = new ArrayList<>(syncVocabList);
Collections.shuffle(drillRound1);
data.put("drillRound1", drillRound1);
List<VocabularyBankDO> drillRound2 = new ArrayList<>(syncVocabList);
Collections.shuffle(drillRound2);
data.put("drillRound2", drillRound2);
List<VocabularyBankDO> drillRound3 = new ArrayList<>(syncVocabList);
Collections.shuffle(drillRound3);
data.put("drillRound3", drillRound3);
// 英译中
List<VocabularyBankDO> mixedDrill = new ArrayList<>();
mixedDrill.addAll(syncVocabList);
mixedDrill.addAll(gapVocabList);
mixedDrill.addAll(reviewVocabList);
Collections.shuffle(mixedDrill);
data.put("mixedDrill", mixedDrill);
// 文章 A
log.info("生成文章 A 中文开始");
Map<String, String> mapA = getArticleStringMap(gradeDO, unitDO, studentId, syncVocabList);
log.info("生成文章 A 成功 {}", mapA);
data.put("articleATitle", mapA.get(LessonPlanConstant.TITLE));
data.put("articleApassage", mapA.get(LessonPlanConstant.PASSAGE));
data.put("articleAquiz", mapA.get(LessonPlanConstant.QUIZ));
data.put("articleAans", mapA.get(LessonPlanConstant.ANSWER_KEY_EXPLANATION));
data.put("articleAtran", mapA.get(LessonPlanConstant.FULL_TRANSLATION));
// 文章 B
log.info("生成文章 B 中文开始");
Map<String, String> mapB;
List<VocabularyBankDO> wordsArticleB = new ArrayList<>();
wordsArticleB.addAll(syncVocabList);
wordsArticleB.addAll(gapVocabList);
wordsArticleB.addAll(reviewVocabList);
mapB = getArticleStringMap(gradeDO, unitDO, studentId, wordsArticleB);
log.info("生成文章 B 成功 {}", mapB);
data.put("articleBTitle", mapB.get(LessonPlanConstant.TITLE));
data.put("articleBpassage", mapB.get(LessonPlanConstant.PASSAGE));
data.put("articleBquiz", mapB.get(LessonPlanConstant.QUIZ));
data.put("articleBans", mapB.get(LessonPlanConstant.ANSWER_KEY_EXPLANATION));
data.put("articleBtran", mapB.get(LessonPlanConstant.FULL_TRANSLATION));
LoopRowTableRenderPolicy policy = new LoopRowTableRenderPolicy();
Configure config = Configure.builder()
.bind("syncVocabList", policy)
.bind("gapVocabList", policy)
.bind("reviewVocabList", policy)
.bind("drillRound1", policy)
.bind("drillRound2", policy)
.bind("drillRound3", policy)
.bind("mixedDrill", policy)
.bind("checkList", policy)
.bind("checkListAns", policy)
.build();
XWPFTemplate template = XWPFTemplate.compile(planWeekday, config);
template.render(data);
template.writeAndClose(new FileOutputStream("C:\\project\\java\\enlish_edu\\enlish\\enlish-service\\src\\main\\resources\\tmp\\word" + title + ".docx"));
return data;
}
@NonNull
private Map<String, String> getArticleStringMap(GradeDO gradeDO, UnitDO unitDO, Integer studentId, List<VocabularyBankDO> words) throws Exception {
Map<String, String> map;
StringBuilder sb = new StringBuilder();
words.forEach(word -> sb.append(word.getWord()).append(","));
sb.deleteCharAt(sb.length() - 1);
String string = sb.toString();
int i = 0;
do {
log.info("第{}次生成文章中文开始", ++i);
String answer = difyArticleClient.sendChat(string, String.valueOf(studentId) + UUID.randomUUID(), null).getAnswer();
map = StringToPlanMapUtil.parseTextToMap(answer);
} while (map.get(LessonPlanConstant.TITLE) == null
|| map.get(LessonPlanConstant.PASSAGE) == null
|| map.get(LessonPlanConstant.QUIZ) == null
|| map.get(LessonPlanConstant.ANSWER_KEY_EXPLANATION) == null
|| map.get(LessonPlanConstant.FULL_TRANSLATION) == null
);
return map;
}
public List<VocabularyBankDO> getVocabListRandom(List<VocabularyBankDO> list, int count) {
List<VocabularyBankDO> randomResultList;
int listSize = list.size();
if (listSize <= count) {
randomResultList = new ArrayList<>(list);
} else {
List<VocabularyBankDO> tempList = new ArrayList<>(list);
Collections.shuffle(tempList); // 随机打乱列表顺序
randomResultList = new ArrayList<>(tempList.subList(0, count));
}
return randomResultList;
}
}

View File

@@ -0,0 +1,118 @@
package com.yinlihupo.enlish.service.utils; // 修改为你的包名
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
@Component
public class DifyArticleClient {
@Value("${ai.key}")
private String apiKey;
@Value("${ai.url}")
private String baseUrl;
private final HttpClient httpClient;
private final ObjectMapper objectMapper;
// 构造函数
public DifyArticleClient() {
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10)) // 连接超时
.build();
this.objectMapper = new ObjectMapper();
}
/**
* 发送对话请求 (阻塞模式)
*
* @param query 用户的问题
* @param userId 用户唯一标识
* @param conversationId 会话ID (首次传 null 或 空字符串)
* @return DifyResponse 包含回复内容和新的 conversationId
*/
public DifyResponse sendChat(String query, String userId, String conversationId) throws Exception {
String endpoint = this.baseUrl;
// 1. 构建请求体对象
ChatRequest payload = new ChatRequest();
payload.setQuery(query);
payload.setUser(userId);
payload.setResponseMode("blocking"); // 使用阻塞模式,一次性返回
// 如果有 conversationId带上它以保持上下文
if (conversationId != null && !conversationId.isEmpty()) {
payload.setConversationId(conversationId);
}
// 如果你的 Dify 应用没有定义变量inputs 传空 Map 即可,但字段必须存在
payload.setInputs(new HashMap<>());
// 2. 序列化为 JSON 字符串
String jsonBody = objectMapper.writeValueAsString(payload);
// 3. 构建 HTTP 请求
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(endpoint))
.header("Authorization", "Bearer " + this.apiKey)
.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);
}
// ================= 内部类DTO (数据传输对象) =================
// 请求体结构
@Data
@ToString
public static class ChatRequest {
private Map<String, Object> inputs;
private String query;
@JsonProperty("response_mode")
private String responseMode;
@JsonProperty("conversation_id")
private String conversationId;
private String user;
}
// 响应体结构
// ignoreUnknown = true 非常重要Dify 返回很多元数据,我们只映射需要的字段,防止报错
@JsonIgnoreProperties(ignoreUnknown = true)
@Data
@ToString
public static class DifyResponse {
private String answer; // 核心回复内容
@JsonProperty("conversation_id")
private String conversationId; // 会话 ID
@JsonProperty("message_id")
private String messageId;
private long created_at;
}
}

View File

@@ -0,0 +1,73 @@
package com.yinlihupo.enlish.service.utils;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class StringToPlanMapUtil {
public static Map<String, String> parseTextToMap(String text) {
// 使用 LinkedHashMap 保持插入顺序
Map<String, String> map = new LinkedHashMap<>();
// 正则表达式:匹配行首的 **Key:** 格式
Pattern headerPattern = Pattern.compile("^\\s*\\*\\*(.+?):\\*\\*\\s*(.*)");
String currentKey = null;
StringBuilder currentValue = new StringBuilder();
try (Scanner scanner = new Scanner(text)) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
Matcher matcher = headerPattern.matcher(line);
if (matcher.matches()) {
// 如果发现新的 Header
// 1. 保存上一个 Key-Value 对(如果存在)
if (currentKey != null) {
// 修改处:在存入 Map 前,调用 cleanMdBold 去除粗体语法
map.put(currentKey, cleanMdBold(currentValue.toString()));
}
// 2. 更新当前的 Key
currentKey = matcher.group(1).trim();
// 3. 重置 StringBuilder
currentValue = new StringBuilder();
String remainingText = matcher.group(2);
if (!remainingText.isEmpty()) {
currentValue.append(remainingText).append("\n");
}
} else {
// 如果不是 Header 行,追加到当前 Value 中
if (currentKey != null) {
currentValue.append(line).append("\n");
}
}
}
// 循环结束后,保存最后一个 Key-Value 对
if (currentKey != null) {
// 修改处:同样在最后存入时去除粗体语法
map.put(currentKey, cleanMdBold(currentValue.toString()));
}
}
return map;
}
/**
* 辅助方法:去除字符串两端的空白以及内部的 Markdown 粗体符号 (**)
*/
private static String cleanMdBold(String text) {
if (text == null) {
return "";
}
// 1. 替换掉所有的 ** 符号
// 2. 去除首尾空白
return text.replace("**", "").trim();
}
}

View File

@@ -25,5 +25,13 @@ templates:
word: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\assessment_v5.docx
count: 100
data: C:\project\tess
plan:
weekday: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\tem_study_plan_v1.docx
weekend: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\study_plan_review_v1.docx
plan_day: 7
tmp:
png: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\tmp\png\
png: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\tmp\png\
ai:
key: app-loC6IrJpj4cS54MAYp73QtGl
url: https://chat.cosonggle.com/v1/chat-messages

View File

@@ -45,7 +45,7 @@
targetProject="src/main/java"/>
<!-- 需要生成的表-实体类 -->
<table tableName="grade_class" domainObjectName="GradeClassDO"
<table tableName="student_lesson_plans" domainObjectName="StudentLessonPlansDO"
enableCountByExample="false"
enableUpdateByExample="false"
enableDeleteByExample="false"

View File

@@ -13,5 +13,11 @@
where grade_id = #{gradeId}
</select>
<select id="selectByUnitId" resultMap="BaseResultMap">
select *
from grade_unit
where unit_id = #{unitId}
</select>
</mapper>

View File

@@ -0,0 +1,25 @@
<?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.yinlihupo.enlish.service.domain.mapper.LessonPlansDOMapper">
<resultMap id="BaseResultMap" type="com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="title" jdbcType="VARCHAR" property="title" />
<result column="grade_id" jdbcType="VARCHAR" property="gradeId" />
<result column="unit_id" jdbcType="INTEGER" property="unitId" />
<result column="created_at" jdbcType="TIMESTAMP" property="createdAt" />
</resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO">
<result column="content_details" jdbcType="LONGVARCHAR" property="contentDetails" />
</resultMap>
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into lesson_plans (title, grade_id, unit_id, content_details, created_at)
values (#{title}, #{gradeId}, #{unitId}, #{contentDetails}, now())
</insert>
<select id="selectById" resultMap="ResultMapWithBLOBs">
select *
from lesson_plans
where id = #{id}
</select>
</mapper>

View File

@@ -0,0 +1,25 @@
<?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.yinlihupo.enlish.service.domain.mapper.StudentLessonPlansDOMapper">
<resultMap id="BaseResultMap" type="com.yinlihupo.enlish.service.domain.dataobject.StudentLessonPlansDO">
<id column="id" jdbcType="INTEGER" property="id" />
<result column="student_id" jdbcType="INTEGER" property="studentId" />
<result column="plan_id" jdbcType="INTEGER" property="planId" />
<result column="start_time" jdbcType="TIMESTAMP" property="startTime" />
<result column="mastery_rat" jdbcType="DECIMAL" property="masteryRat" />
<result column="total_count" jdbcType="INTEGER" property="totalCount" />
</resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="com.yinlihupo.enlish.service.domain.dataobject.StudentLessonPlansDO">
<result column="memorized_words_json" jdbcType="LONGVARCHAR" property="memorizedWordsJson" />
<result column="unmemorized_words_json" jdbcType="LONGVARCHAR" property="unmemorizedWordsJson" />
</resultMap>
<insert id="insert">
insert into student_lesson_plans (student_id, plan_id, start_time, mastery_rat, total_count)
values (#{studentId,jdbcType=INTEGER}, #{planId,jdbcType=INTEGER},
now(), 0.0, 0
)
</insert>
</mapper>

View File

@@ -7,6 +7,7 @@
<result column="definition" jdbcType="VARCHAR" property="definition" />
<result column="pronunciation" jdbcType="VARCHAR" property="pronunciation" />
<result column="unit_id" jdbcType="INTEGER" property="unitId" />
<result column="pos" jdbcType="VARCHAR" property="pos" />
</resultMap>
@@ -28,6 +29,9 @@
<if test="unitId != null">
unit_id,
</if>
<if test="pos != null">
pos,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
@@ -45,6 +49,9 @@
<if test="unitId != null">
#{unitId,jdbcType=INTEGER},
</if>
<if test="pos != null">
#{pos,jdbcType=VARCHAR},
</if>
</trim>
</insert>
@@ -69,4 +76,46 @@
from vocabulary_bank
</select>
<select id="selectVocabularyBankDOAllByUnitId" resultMap="BaseResultMap">
select *
from vocabulary_bank
where unit_id = #{unitId}
</select>
<select id="selectVocabularyBankListStudentNotMaster" resultMap="BaseResultMap">
<![CDATA[
select *
from vocabulary_bank
where unit_id in (
select unit_id
from grade_unit
where grade_id = #{gradeId}
)
and id in (
select word_id
from word_mastery_log
where memory_strength < 0
and student_id = #{studentId}
)
]]>
</select>
<select id="selectVocabularyBankListSelfCheck" resultMap="BaseResultMap">
select *
from vocabulary_bank vb
inner join grade_unit gu on vb.unit_id = gu.unit_id
inner join word_mastery_log wml on vb.id = wml.word_id
where gu.grade_id = #{gradeId}
and wml.student_id = #{studentId}
<!-- 修复not in的语法错误 + 空集合防护 -->
<if test="ids != null and ids.size() > 0">
and vb.id not in (
<foreach item="id" collection="ids" separator=",">
#{id}
</foreach>
)
</if>
limit #{wordCount}
</select>
</mapper>

View File

@@ -0,0 +1,50 @@
package com.yinlihupo.enlish.service.ai;
import com.yinlihupo.enlish.service.utils.DifyArticleClient;
import com.yinlihupo.enlish.service.utils.StringToPlanMapUtil;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.IOException;
import java.util.Map;
@SpringBootTest
public class AITest {
@Resource
private DifyArticleClient client;
@Test
public void test1() throws IOException {
try {
// 2. 第一轮对话 (没有 conversation_id)
System.out.println("--- Round 1 ---");
String userId = "user-1001";
DifyArticleClient.DifyResponse response1 = client.sendChat("ruler, pencil, eraser, crayon, bag, pen, book, red, green, yellow, blue, face, ear, eye, nose, mouth, duck, pig, cat, bear, dog, elephant, monkey, bird, tiger, panda, bread, juice, egg, milk", userId, null);
//System.out.println("AI 回复: " + response1.getAnswer());
System.out.println("当前会话ID: " + response1.getConversationId());
// // 3. 第二轮对话 (传入上一轮的 conversation_id 以保持记忆)
// System.out.println("\n--- Round 2 ---");
// // 注意这里传入了 response1.getConversationId()
// DifyClient.DifyResponse response2 = client.sendChat("我刚才说了我叫什么?", userId, response1.getConversationId());
//
// System.out.println("AI 回复: " + response2.getAnswer());
System.out.println("\n--- Round 2 ---");
Map<String, String> stringStringMap = StringToPlanMapUtil.parseTextToMap(response1.getAnswer());
System.out.println(stringStringMap.get("Title"));
System.out.println(stringStringMap.get("The Passage"));
System.out.println(stringStringMap.get("Quiz"));
System.out.println(stringStringMap.get("Answer Key & Explanation"));
System.out.println(stringStringMap.get("Full Translation"));
} catch (Exception e) {
e.printStackTrace();
}
}
}

View File

@@ -0,0 +1,28 @@
package com.yinlihupo.enlish.service.mapper;
import com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO;
import com.yinlihupo.enlish.service.domain.mapper.LessonPlansDOMapper;
import com.yinlihupo.framework.common.util.JsonUtils;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Map;
@SpringBootTest
public class PlanTest {
@Resource
private LessonPlansDOMapper lessonPlansDOMapper;
@Test
public void test() throws Exception {
LessonPlansDO lessonPlansDO = lessonPlansDOMapper.selectById(21);
Map<String, Object> stringObjectMap = JsonUtils.parseMap(lessonPlansDO.getContentDetails(), String.class, Object.class);
for (Map.Entry<String, Object> entry : stringObjectMap.entrySet()) {
System.out.println(entry.getKey());
System.out.println(entry.getValue());
System.out.println("------------------------");
}
}
}

View File

@@ -0,0 +1,18 @@
package com.yinlihupo.enlish.service.service.plan;
import com.yinlihupo.enlish.service.service.LessonPlansService;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
public class PlanTest {
@Resource
private LessonPlansService lessonPlansService;
@Test
public void test() {
lessonPlansService.generateLessonPlans(2, 146);
}
}

View File

@@ -45,10 +45,11 @@
</router-link>
</li>
<li>
<a href="#"
<router-link
to="/learningplan"
class="block py-2 pr-4 pl-3 text-gray-700 border-b border-gray-100 hover:bg-gray-50 lg:hover:bg-transparent lg:border-0 lg:hover:text-primary-700 lg:p-0 dark:text-gray-400 lg:dark:hover:text-white dark:hover:bg-gray-700 dark:hover:text-white lg:dark:hover:bg-transparent dark:border-gray-700">
Marketplace
</a>
学案
</router-link>
</li>
<li>
<a href="#"

View File

@@ -0,0 +1,22 @@
<template>
<div class="common-layout">
<el-container>
<el-header>
<Header></Header>
</el-header>
<el-main class="p-4">
<el-tabs v-model="activeTab" type="border-card" class="demo-tabs">
<el-tab-pane label="学习计划" name="first">学习计划</el-tab-pane>
<el-tab-pane label="学习记录" name="second">学习记录</el-tab-pane>
</el-tabs>
</el-main>
</el-container>
</div>
</template>
<script setup>
import Header from '@/layouts/components/Header.vue'
</script>

View File

@@ -1,5 +1,6 @@
import Index from '@/pages/index.vue'
import Uploadpng from '@/pages/uploadpng.vue'
import LearningPlan from '@/pages/LearningPlan.vue'
import Class from '@/pages/class.vue'
import { createRouter, createWebHashHistory } from 'vue-router'
@@ -18,6 +19,13 @@ const routes = [
meta: { // meta 信息
title: '上传图片' // 页面标题
}
},
{
path: '/learningplan', // 路由地址
component: LearningPlan, // 对应组件
meta: { // meta 信息
title: '学案' // 页面标题
}
}
]