feat(plan): 支持学案生成时指定单词数

- 在 AddLessonPlanReqVO 中新增 wordSize 字段
- 修改 LessonPlansService 接口及实现,支持 wordSize 参数
- 优化学案生成逻辑,按指定单词数切分词汇列表
- 更新前端 LessonPlanDialog,添加单词数输入框
- 修改生成学案接口及调用,传递 wordSize 参数
- 增加查询学生词汇掌握详情接口及实现
- 添加学生词汇统计展示组件及页面集成
- 调整词汇相关 Mapper,修正记忆强度条件范围
- 更新权限配置,允许访问学生单词详情接口
This commit is contained in:
lbw
2025-12-27 17:21:25 +08:00
parent d3cfa80613
commit 494ab77486
20 changed files with 148 additions and 19 deletions

View File

@@ -37,6 +37,7 @@ public class SaTokenConfigure implements WebMvcConfigurer {
.notMatch("/student/mastery/detail") .notMatch("/student/mastery/detail")
.notMatch("/unit/list") .notMatch("/unit/list")
.notMatch("/vocabulary/list") .notMatch("/vocabulary/list")
.notMatch("/vocabulary/student/detail")
.notMatch("/plan/download") .notMatch("/plan/download")
.notMatch("/login/**") .notMatch("/login/**")
.check(r -> StpUtil.checkLogin()); .check(r -> StpUtil.checkLogin());

View File

@@ -41,8 +41,9 @@ public class LessonPlanController {
public Response<String> generateLessonPlan(@RequestBody AddLessonPlanReqVO addLessonPlanReqVO) { public Response<String> generateLessonPlan(@RequestBody AddLessonPlanReqVO addLessonPlanReqVO) {
Integer studentId = addLessonPlanReqVO.getStudentId(); Integer studentId = addLessonPlanReqVO.getStudentId();
Integer unitId = addLessonPlanReqVO.getUnitId(); Integer unitId = addLessonPlanReqVO.getUnitId();
Integer wordSize = addLessonPlanReqVO.getWordSize();
try { try {
taskExecutor.execute(() -> lessonPlanService.generateLessonPlans(studentId, unitId)); taskExecutor.execute(() -> lessonPlanService.generateLessonPlans(studentId, unitId, wordSize));
return Response.success("生成学案成功,请等待 10 分钟"); return Response.success("生成学案成功,请等待 10 分钟");
} catch (Exception e) { } catch (Exception e) {
log.error(e.getMessage()); log.error(e.getMessage());

View File

@@ -1,6 +1,8 @@
package com.yinlihupo.enlish.service.controller; package com.yinlihupo.enlish.service.controller;
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO; import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
import com.yinlihupo.enlish.service.model.vo.vocabulary.FindStudentWordDetailReqVO;
import com.yinlihupo.enlish.service.model.vo.vocabulary.FindStudentWordDetailRspVO;
import com.yinlihupo.enlish.service.model.vo.vocabulary.FindWordTitleReqVO; import com.yinlihupo.enlish.service.model.vo.vocabulary.FindWordTitleReqVO;
import com.yinlihupo.enlish.service.model.vo.vocabulary.FindWordTitleRspVO; import com.yinlihupo.enlish.service.model.vo.vocabulary.FindWordTitleRspVO;
import com.yinlihupo.enlish.service.service.VocabularyService; import com.yinlihupo.enlish.service.service.VocabularyService;
@@ -30,4 +32,11 @@ public class VocabularyController {
.build(); .build();
return Response.success(findWordTitleRspVO); return Response.success(findWordTitleRspVO);
} }
@PostMapping("student/detail")
@ApiOperationLog(description = "查询学生单词详情")
public Response<FindStudentWordDetailRspVO> findStudentWordDetail(@RequestBody FindStudentWordDetailReqVO vo) {
return Response.success(vocabularyService.findStudentWordDetail(vo.getId()));
}
} }

View File

@@ -20,4 +20,8 @@ public interface WordMasteryLogDOMapper {
List<WordMasteryLogDO> selectByStudentIdAndLimitTime(@Param("studentId") Integer studentId); List<WordMasteryLogDO> selectByStudentIdAndLimitTime(@Param("studentId") Integer studentId);
List<WordMasteryLogDO> selectAllByStudentId(@Param("studentId") Integer studentId); List<WordMasteryLogDO> selectAllByStudentId(@Param("studentId") Integer studentId);
Integer selectMasteryCount(@Param("studentId") Integer studentId);
Integer selectNotMasteryCount(@Param("studentId") Integer studentId);
} }

View File

@@ -13,4 +13,5 @@ public class AddLessonPlanReqVO {
private Integer studentId; private Integer studentId;
private Integer unitId; private Integer unitId;
private Integer wordSize;
} }

View File

@@ -0,0 +1,15 @@
package com.yinlihupo.enlish.service.model.vo.vocabulary;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class FindStudentWordDetailReqVO {
private Integer id;
}

View File

@@ -0,0 +1,27 @@
package com.yinlihupo.enlish.service.model.vo.vocabulary;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class FindStudentWordDetailRspVO {
/**
* 已掌握单词数
*/
private Integer masteredWordCount;
/**
* 未掌握单词数
*/
private Integer unmasteredWordCount;
/**
* 待审查单词数(推荐使用,简洁通用)
*/
private Integer pendingReviewWordCount;
}

View File

@@ -5,7 +5,7 @@ import com.yinlihupo.enlish.service.domain.dataobject.LessonPlansDO;
import java.util.List; import java.util.List;
public interface LessonPlansService { public interface LessonPlansService {
void generateLessonPlans(Integer studentId, Integer unitId); void generateLessonPlans(Integer studentId, Integer unitId, Integer wordSize);
List<LessonPlansDO> findLessonPlans(List<Integer> ids); List<LessonPlansDO> findLessonPlans(List<Integer> ids);

View File

@@ -2,10 +2,13 @@ package com.yinlihupo.enlish.service.service;
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO; import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
import com.yinlihupo.enlish.service.model.vo.vocabulary.FindStudentWordDetailRspVO;
import java.util.List; import java.util.List;
public interface VocabularyService { public interface VocabularyService {
List<VocabularyBankDO> findVocabularyBankDOListById(List<Integer> ids); List<VocabularyBankDO> findVocabularyBankDOListById(List<Integer> ids);
FindStudentWordDetailRspVO findStudentWordDetail(Integer studentId);
} }

View File

@@ -37,15 +37,10 @@ public class LessonPlansServiceImpl implements LessonPlansService {
@Resource @Resource
private DifyArticleClient difyArticleClient; private DifyArticleClient difyArticleClient;
@Value("${templates.plan.weekday}")
private String planWeekday;
@Value("${templates.plan.weekend}")
private String planWeekend;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void generateLessonPlans(Integer studentId, Integer unitId) { public void generateLessonPlans(Integer studentId, Integer unitId, Integer wordSize) {
List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOAllByUnitId(unitId); List<VocabularyBankDO> vocabularyBankDOS = vocabularyBankDOMapper.selectVocabularyBankDOAllByUnitId(unitId);
UnitDO unitDO = unitDOMapper.selectByPrimaryKey(unitId); UnitDO unitDO = unitDOMapper.selectByPrimaryKey(unitId);
GradeUnitDO gradeUnitDO = gradeUnitDOMapper.selectByUnitId(unitId); GradeUnitDO gradeUnitDO = gradeUnitDOMapper.selectByUnitId(unitId);
@@ -58,12 +53,19 @@ public class LessonPlansServiceImpl implements LessonPlansService {
int countGap = gapSize / 5; int countGap = gapSize / 5;
int syncSize = vocabularyBankDOS.size(); int syncSize = vocabularyBankDOS.size();
int countSync = syncSize / 5; wordSize = wordSize <= 0 ? syncSize / 5 : wordSize;
int checkTotal = 50; int checkTotal = 50;
List<List<VocabularyBankDO>> weeksSync = new ArrayList<>(); List<List<VocabularyBankDO>> weeksSync = new ArrayList<>();
List<List<VocabularyBankDO>> weeksGap = new ArrayList<>(); List<List<VocabularyBankDO>> weeksGap = new ArrayList<>();
for (int i = 0; i < 5; i++) { for (int i = 0; i < 5; i++) {
List<VocabularyBankDO> syncVocabList = vocabularyBankDOS.subList(i * countSync, Math.min((i + 1) * countSync, syncSize)); List<VocabularyBankDO> syncVocabList;
if ((i + 1) * wordSize < syncSize) {
syncVocabList = vocabularyBankDOS.subList(i * wordSize, (i + 1) * wordSize);
} else if (i == 4) {
syncVocabList = vocabularyBankDOS.subList(i * wordSize, syncSize);
} else {
syncVocabList = vocabularyBankDOS.subList((syncSize - i) * wordSize, Math.min((syncSize - i + 1) * wordSize, syncSize));
}
List<VocabularyBankDO> gapVocabList = vocabularyBankListStudentNotMaster.subList(i * countGap, Math.min(i * countGap + countGap, gapSize)); List<VocabularyBankDO> gapVocabList = vocabularyBankListStudentNotMaster.subList(i * countGap, Math.min(i * countGap + countGap, gapSize));
weeksSync.add(syncVocabList); weeksSync.add(syncVocabList);
weeksGap.add(gapVocabList); weeksGap.add(gapVocabList);

View File

@@ -2,6 +2,8 @@ package com.yinlihupo.enlish.service.service.vocabulary;
import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO; import com.yinlihupo.enlish.service.domain.dataobject.VocabularyBankDO;
import com.yinlihupo.enlish.service.domain.mapper.VocabularyBankDOMapper; import com.yinlihupo.enlish.service.domain.mapper.VocabularyBankDOMapper;
import com.yinlihupo.enlish.service.domain.mapper.WordMasteryLogDOMapper;
import com.yinlihupo.enlish.service.model.vo.vocabulary.FindStudentWordDetailRspVO;
import com.yinlihupo.enlish.service.service.VocabularyService; import com.yinlihupo.enlish.service.service.VocabularyService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -14,9 +16,19 @@ public class VocabularyServiceImpl implements VocabularyService {
@Resource @Resource
private VocabularyBankDOMapper vocabularyBankDOMapper; private VocabularyBankDOMapper vocabularyBankDOMapper;
@Resource
private WordMasteryLogDOMapper wordMasteryLogDOMapper;
@Override @Override
public List<VocabularyBankDO> findVocabularyBankDOListById(List<Integer> ids) { public List<VocabularyBankDO> findVocabularyBankDOListById(List<Integer> ids) {
return vocabularyBankDOMapper.selectVocabularyBankDOListByIds(ids); return vocabularyBankDOMapper.selectVocabularyBankDOListByIds(ids);
} }
@Override
public FindStudentWordDetailRspVO findStudentWordDetail(Integer studentId) {
Integer wordMastery = wordMasteryLogDOMapper.selectMasteryCount(studentId);
Integer wordNotMastery = wordMasteryLogDOMapper.selectNotMasteryCount(studentId);
Integer total = vocabularyBankDOMapper.selectWordTotal();
return FindStudentWordDetailRspVO.builder().masteredWordCount(wordMastery).unmasteredWordCount(wordNotMastery).pendingReviewWordCount(total - wordMastery - wordNotMastery).build();
}
} }

View File

@@ -26,7 +26,7 @@ templates:
count: 100 count: 100
data: C:\project\tess data: C:\project\tess
plan: plan:
weekday: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\tem_study_plan_v1.docx weekday: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\tem_study_plan_v2.docx
weekend: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\study_plan_review_v1.docx weekend: C:\project\java\enlish_edu\enlish\enlish-service\src\main\resources\templates\study_plan_review_v1.docx
plan_day: 7 plan_day: 7
tmp: tmp:

View File

@@ -94,7 +94,7 @@
and id in ( and id in (
select word_id select word_id
from word_mastery_log from word_mastery_log
where memory_strength < 0 where memory_strength <= 0
and student_id = #{studentId} and student_id = #{studentId}
) )
]]> ]]>

View File

@@ -22,6 +22,7 @@
order by memory_strength desc order by memory_strength desc
limit #{limit} limit #{limit}
</select> </select>
<select id="selectStudentStrengthCount" resultType="java.lang.Integer"> <select id="selectStudentStrengthCount" resultType="java.lang.Integer">
select count(*) select count(*)
from word_mastery_log from word_mastery_log
@@ -58,9 +59,27 @@
where student_id = #{studentId} where student_id = #{studentId}
and update_time between date_sub(now(), interval 7 day) and now() and update_time between date_sub(now(), interval 7 day) and now()
</select> </select>
<select id="selectAllByStudentId" resultMap="BaseResultMap"> <select id="selectAllByStudentId" resultMap="BaseResultMap">
select * select *
from word_mastery_log from word_mastery_log
where student_id = #{studentId} where student_id = #{studentId}
</select> </select>
<select id="selectMasteryCount" resultType="java.lang.Integer">
select count(*)
from word_mastery_log
where student_id = #{studentId}
and memory_strength >= 1
</select>
<select id="selectNotMasteryCount" resultType="java.lang.Integer">
<![CDATA[
select count(*)
from word_mastery_log
where student_id = #{studentId}
and memory_strength < 0
]]>
</select>
</mapper> </mapper>

View File

@@ -13,6 +13,6 @@ public class PlanTest {
@Test @Test
public void test() { public void test() {
lessonPlansService.generateLessonPlans(2, 146);
} }
} }

View File

@@ -1,9 +1,10 @@
import axios from "@/axios"; import axios from "@/axios";
export function generateLessonPlan(studentId, unitId) { export function generateLessonPlan(studentId, unitId, wordSize = 0) {
return axios.post('/plan/generate', { return axios.post('/plan/generate', {
studentId: studentId, studentId: studentId,
unitId: unitId unitId: unitId,
wordSize: wordSize
}) })
} }

View File

@@ -4,4 +4,10 @@ export function getWordsListByIds(ids) {
return axios.post('/vocabulary/list', { return axios.post('/vocabulary/list', {
ids: ids ids: ids
}) })
} }
export function getWordStudentDetail(studentId) {
return axios.post('/vocabulary/student/detail', {
id: studentId
})
}

View File

@@ -12,6 +12,9 @@
/> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="单词数">
<el-input v-model="wordSize" type="number" placeholder="请输入单词数" style="width: 300px" />
</el-form-item>
</el-form> </el-form>
<div class="text-sm text-gray-500"> <div class="text-sm text-gray-500">
学生ID{{ studentId }} 学生ID{{ studentId }}
@@ -45,6 +48,7 @@ const visible = computed({
const loading = ref(false) const loading = ref(false)
const unitId = ref(null) const unitId = ref(null)
const wordSize = ref(0)
const unitOptions = ref([]) const unitOptions = ref([])
async function fetchUnits() { async function fetchUnits() {
@@ -63,7 +67,7 @@ async function fetchUnits() {
async function handleGenerate() { async function handleGenerate() {
if (!unitId.value || !props.studentId) return if (!unitId.value || !props.studentId) return
const res = await generateLessonPlan(Number(props.studentId), Number(unitId.value)) const res = await generateLessonPlan(Number(props.studentId), Number(unitId.value), Number(wordSize.value))
const d = res?.data const d = res?.data
if (d.success) { if (d.success) {
ElMessage.success('生成学案任务已提交,请等待十分钟') ElMessage.success('生成学案任务已提交,请等待十分钟')

View File

@@ -6,8 +6,7 @@
</el-header> </el-header>
<el-main class="p-4"> <el-main class="p-4">
<div class="grid grid-cols-1 lg:grid-cols-1 gap-6" <div class="grid grid-cols-1 lg:grid-cols-2 gap-6" v-loading="loading">
v-loading="loading">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6"> <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="text-lg font-semibold mb-4">学生详情</div> <div class="text-lg font-semibold mb-4">学生详情</div>
<template v-if="detail"> <template v-if="detail">
@@ -24,6 +23,20 @@
</template> </template>
</div> </div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="text-lg font-semibold mb-4">学生词汇统计</div>
<template v-if="wordStat">
<el-descriptions :column="1" border>
<el-descriptions-item label="已掌握">{{ wordStat.masteredWordCount }}</el-descriptions-item>
<el-descriptions-item label="未掌握">{{ wordStat.unmasteredWordCount }}</el-descriptions-item>
<el-descriptions-item label="待复习">{{ wordStat.pendingReviewWordCount }}</el-descriptions-item>
</el-descriptions>
</template>
<template v-else>
<el-empty description="暂无统计" />
</template>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6"> <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<div class="text-md font-semibold mb-3">学生考试记录</div> <div class="text-md font-semibold mb-3">学生考试记录</div>
<ExamHistoryChart :data="history" /> <ExamHistoryChart :data="history" />
@@ -63,6 +76,7 @@ import { ref, onMounted, computed } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { getStudentDetail, getStudentStudyAnalyze } from '@/api/student' import { getStudentDetail, getStudentStudyAnalyze } from '@/api/student'
import { getStudentExamHistory } from '@/api/exam' import { getStudentExamHistory } from '@/api/exam'
import { getWordStudentDetail } from '@/api/words'
import ExamHistoryChart from '@/layouts/components/student/ExamHistoryChart.vue' import ExamHistoryChart from '@/layouts/components/student/ExamHistoryChart.vue'
import PlanHistoryChart from '@/layouts/components/student/PlanHistoryChart.vue' import PlanHistoryChart from '@/layouts/components/student/PlanHistoryChart.vue'
import WordMasteryHeatmap from '@/layouts/components/student/WordMasteryHeatmap.vue' import WordMasteryHeatmap from '@/layouts/components/student/WordMasteryHeatmap.vue'
@@ -74,6 +88,7 @@ const route = useRoute()
const history = ref([]) const history = ref([])
const analyzeLoading = ref(false) const analyzeLoading = ref(false)
const analysisText = ref('') const analysisText = ref('')
const wordStat = ref(null)
const md = new MarkdownIt({ const md = new MarkdownIt({
html: false, html: false,
linkify: true, linkify: true,
@@ -122,8 +137,17 @@ async function fetchStudyAnalyze() {
} }
} }
async function fetchWordStat() {
const id = route.params.id
if (!id) return
const res = await getWordStudentDetail(Number(id))
const d = res.data
wordStat.value = d?.data || null
}
onMounted(() => { onMounted(() => {
fetchDetail() fetchDetail()
fetchExamHistory() fetchExamHistory()
fetchWordStat()
}) })
</script> </script>