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:
@@ -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";
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.yinlihupo.enlish.service.service;
|
||||
|
||||
public interface LessonPlansService {
|
||||
void generateLessonPlans(Integer studentId, Integer unitId);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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\
|
||||
|
||||
ai:
|
||||
key: app-loC6IrJpj4cS54MAYp73QtGl
|
||||
url: https://chat.cosonggle.com/v1/chat-messages
|
||||
@@ -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"
|
||||
|
||||
@@ -13,5 +13,11 @@
|
||||
where grade_id = #{gradeId}
|
||||
</select>
|
||||
|
||||
<select id="selectByUnitId" resultMap="BaseResultMap">
|
||||
select *
|
||||
from grade_unit
|
||||
where unit_id = #{unitId}
|
||||
</select>
|
||||
|
||||
|
||||
</mapper>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
Binary file not shown.
Binary file not shown.
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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("------------------------");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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="#"
|
||||
|
||||
22
enlish-vue/src/pages/LearningPlan.vue
Normal file
22
enlish-vue/src/pages/LearningPlan.vue
Normal 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>
|
||||
@@ -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: '学案' // 页面标题
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user