fix: 教师端课程与授课记录优化

- 修复 assessment_data JSON 字段:普通文本自动包装为有效 JSON
- 修复返回时页面 ID 丢失:校验无效 ID 并跳转,goBackToDetail 优先使用路由 ID
- 修复上课记录列表:getLessons 支持 pageNum、日期范围、状态映射,list 转 items
- 修复班级与课程取值:LessonResponse 增加 courseName/className,接口返回时自动填充
- 备课/详情页增加 ID 校验,防止跳转到 undefined

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-17 11:24:25 +08:00
parent e8b44b25e0
commit c8ecbe277c
8 changed files with 167 additions and 37 deletions

View File

@ -181,26 +181,52 @@ export interface StudentRecordDto {
notes?: string; notes?: string;
} }
// 后端状态值: scheduled, in_progress, completed, cancelled
const STATUS_TO_BACKEND: Record<string, string> = {
PLANNED: 'scheduled',
IN_PROGRESS: 'in_progress',
COMPLETED: 'completed',
CANCELLED: 'cancelled',
};
// 获取授课记录列表 // 获取授课记录列表
export function getLessons(params?: { export function getLessons(params?: {
pageNum?: number; pageNum?: number;
page?: number;
pageSize?: number; pageSize?: number;
status?: string; status?: string;
courseId?: number; startDate?: string;
endDate?: string;
}): Promise<{ }): Promise<{
items: any[]; items: any[];
total: number; total: number;
page: number; page: number;
pageSize: number; pageSize: number;
}> { }> {
return http.get('/v1/teacher/lessons', { const pageNum = params?.pageNum ?? params?.page ?? 1;
const status = params?.status ? (STATUS_TO_BACKEND[params.status] || params.status) : undefined;
return http
.get<{ list?: any[]; records?: any[]; total?: number | string; pageNum?: number; pageSize?: number }>(
'/v1/teacher/lessons',
{
params: { params: {
pageNum: params?.pageNum, pageNum,
pageSize: params?.pageSize, pageSize: params?.pageSize ?? 10,
status: params?.status, status,
startDate: params?.courseId, // 如果需要可以传其他参数 startDate: params?.startDate,
endDate: params?.endDate,
}, },
}) as any; }
)
.then((res) => {
const list = res?.list ?? res?.records ?? [];
return {
items: Array.isArray(list) ? list : [],
total: typeof res?.total === 'string' ? parseInt(res.total, 10) || 0 : (res?.total ?? 0),
page: res?.pageNum ?? pageNum,
pageSize: res?.pageSize ?? 10,
};
});
} }
// 获取单个授课记录详情id 使用 string 避免 Long 精度丢失) // 获取单个授课记录详情id 使用 string 避免 Long 精度丢失)

View File

@ -17,7 +17,7 @@
<a-button @click="createSchoolVersion"> <a-button @click="createSchoolVersion">
<CopyOutlined /> 创建校本版本 <CopyOutlined /> 创建校本版本
</a-button> </a-button>
<a-button type="primary" @click="startPrepare"> <a-button type="primary" @click="startPrepare" :disabled="!course.id || loading">
<EditOutlined /> 开始备课 <EditOutlined /> 开始备课
</a-button> </a-button>
</div> </div>
@ -281,7 +281,7 @@
<div class="lesson-section-title">教学环节 ({{ lesson.steps.length }})</div> <div class="lesson-section-title">教学环节 ({{ lesson.steps.length }})</div>
<div class="steps-timeline"> <div class="steps-timeline">
<div v-for="(step, index) in lesson.steps" :key="step.id || index" class="step-item"> <div v-for="(step, index) in lesson.steps" :key="step.id || index" class="step-item">
<div class="step-dot">{{ index + 1 }}</div> <div class="step-dot">{{ Number(index) + 1 }}</div>
<div class="step-content"> <div class="step-content">
<div class="step-name">{{ step.name }}</div> <div class="step-name">{{ step.name }}</div>
<div class="step-duration">{{ step.duration }}分钟</div> <div class="step-duration">{{ step.duration }}分钟</div>
@ -870,7 +870,12 @@ const createSchoolVersion = () => {
}; };
const startPrepare = () => { const startPrepare = () => {
router.push(`/teacher/courses/${course.value.id}/prepare`); const id = route.params.id || course.value?.id;
if (!id || id === 'undefined') {
message.warning('课程信息未加载完成,请稍后再试');
return;
}
router.push(`/teacher/courses/${id}/prepare`);
}; };
const toggleFavorite = () => { const toggleFavorite = () => {

View File

@ -318,7 +318,12 @@ const viewCourseDetail = (course: any) => {
}; };
const prepareCourse = (course: any) => { const prepareCourse = (course: any) => {
router.push(`/teacher/courses/${course.id}/prepare`); const id = course?.id;
if (id == null || id === 'undefined') {
message.warning('课程信息异常,无法进入备课');
return;
}
router.push(`/teacher/courses/${id}/prepare`);
}; };
onMounted(() => { onMounted(() => {

View File

@ -121,7 +121,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { import {
LeftOutlined, BookOutlined, ClockCircleOutlined, TagOutlined, LeftOutlined, BookOutlined, ClockCircleOutlined, TagOutlined,
@ -210,8 +210,14 @@ const getFileUrl = (filePath: string | null | undefined): string => {
}; };
const loadCourseData = async () => { const loadCourseData = async () => {
courseId.value = (route.params.id as string) || ''; const id = route.params.id as string;
if (!courseId.value) return; // ID /courses/undefined
if (!id || id === 'undefined' || id === 'null') {
message.warning('课程 ID 无效,已返回课程列表');
router.replace('/teacher/courses');
return;
}
courseId.value = id;
loading.value = true; loading.value = true;
try { try {
@ -358,7 +364,7 @@ const handleSelectStep = (step: any) => {
selectedStep.value = step; selectedStep.value = step;
}; };
const handlePreviewResource = (type: string, resource: any) => { const handlePreviewResource = (_type: string, resource: any) => {
previewFileUrl.value = resource.url; previewFileUrl.value = resource.url;
previewFileName.value = resource.name || '资源文件'; previewFileName.value = resource.name || '资源文件';
previewModalVisible.value = true; previewModalVisible.value = true;
@ -452,12 +458,28 @@ const handleExit = () => {
}; };
const goBackToDetail = () => { const goBackToDetail = () => {
router.push(`/teacher/courses/${courseId.value}`); // 使 ID courseId /courses/undefined
const id = route.params.id || courseId.value;
if (id && id !== 'undefined' && id !== 'null') {
router.push(`/teacher/courses/${id}`);
} else {
router.push('/teacher/courses');
}
}; };
onMounted(() => { onMounted(() => {
loadCourseData(); loadCourseData();
}); });
// A B
watch(
() => route.params.id,
(newId, oldId) => {
if (newId && newId !== oldId) {
loadCourseData();
}
}
);
</script> </script>
<style scoped> <style scoped>

View File

@ -46,16 +46,16 @@
</div> </div>
<div class="card-content"> <div class="card-content">
<div class="card-header"> <div class="card-header">
<h3 class="course-name">{{ lesson.course?.name || '未知课程' }}</h3> <h3 class="course-name">{{ lesson.courseName || lesson.course?.name || lesson.title || '未知课程' }}</h3>
<span class="lesson-time"> <span class="lesson-time">
<ClockCircleOutlined /> <ClockCircleOutlined />
{{ formatDateTime(lesson.startDatetime || lesson.plannedDatetime) }} {{ formatDateTime(lesson.startDatetime || lesson.plannedDatetime || (lesson.lessonDate && lesson.startTime ? `${lesson.lessonDate}T${lesson.startTime}` : null)) }}
</span> </span>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="info-item"> <div class="info-item">
<TeamOutlined /> <TeamOutlined />
<span>{{ lesson.class?.name || '未知班级' }}</span> <span>{{ lesson.className || lesson.class?.name || '未知班级' }}</span>
</div> </div>
<div class="info-item" v-if="lesson.actualDuration"> <div class="info-item" v-if="lesson.actualDuration">
<FieldTimeOutlined /> <FieldTimeOutlined />
@ -108,10 +108,10 @@
<div class="detail-content" v-if="selectedLesson"> <div class="detail-content" v-if="selectedLesson">
<a-descriptions :column="1" bordered> <a-descriptions :column="1" bordered>
<a-descriptions-item label="课程名称"> <a-descriptions-item label="课程名称">
{{ selectedLesson.course?.name }} {{ selectedLesson.courseName || selectedLesson.course?.name || selectedLesson.title || '-' }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="授课班级"> <a-descriptions-item label="授课班级">
{{ selectedLesson.class?.name }} {{ selectedLesson.className || selectedLesson.class?.name || '-' }}
</a-descriptions-item> </a-descriptions-item>
<a-descriptions-item label="课程状态"> <a-descriptions-item label="课程状态">
<a-tag :color="getStatusColor(selectedLesson.status)"> <a-tag :color="getStatusColor(selectedLesson.status)">
@ -152,7 +152,7 @@
<!-- 操作按钮 --> <!-- 操作按钮 -->
<div class="action-section"> <div class="action-section">
<!-- 已计划状态 --> <!-- 已计划状态 -->
<template v-if="selectedLesson.status === 'PLANNED'"> <template v-if="selectedLesson.status === 'PLANNED' || selectedLesson.status === 'scheduled'">
<a-button type="primary" block @click="startPlannedLesson" style="margin-bottom: 12px;"> <a-button type="primary" block @click="startPlannedLesson" style="margin-bottom: 12px;">
开始上课 开始上课
</a-button> </a-button>
@ -226,12 +226,16 @@ const filters = reactive({
const detailDrawerVisible = ref(false); const detailDrawerVisible = ref(false);
const selectedLesson = ref<any>(null); const selectedLesson = ref<any>(null);
// // scheduled/in_progress/completed/cancelled PLANNED/IN_PROGRESS
const statusMap: Record<string, { text: string; color: string; class: string }> = { const statusMap: Record<string, { text: string; color: string; class: string }> = {
PLANNED: { text: '已计划', color: 'blue', class: 'status-planned' }, PLANNED: { text: '已计划', color: 'blue', class: 'status-planned' },
scheduled: { text: '已计划', color: 'blue', class: 'status-planned' },
IN_PROGRESS: { text: '进行中', color: 'orange', class: 'status-progress' }, IN_PROGRESS: { text: '进行中', color: 'orange', class: 'status-progress' },
in_progress: { text: '进行中', color: 'orange', class: 'status-progress' },
COMPLETED: { text: '已完成', color: 'green', class: 'status-completed' }, COMPLETED: { text: '已完成', color: 'green', class: 'status-completed' },
completed: { text: '已完成', color: 'green', class: 'status-completed' },
CANCELLED: { text: '已取消', color: 'default', class: 'status-cancelled' }, CANCELLED: { text: '已取消', color: 'default', class: 'status-cancelled' },
cancelled: { text: '已取消', color: 'default', class: 'status-cancelled' },
}; };
const getStatusText = (status: string) => statusMap[status]?.text || status; const getStatusText = (status: string) => statusMap[status]?.text || status;
@ -247,12 +251,16 @@ const loadLessons = async () => {
loading.value = true; loading.value = true;
try { try {
const params: any = { const params: any = {
page: currentPage.value, pageNum: currentPage.value,
pageSize: pageSize.value, pageSize: pageSize.value,
}; };
if (filters.status) { if (filters.status) {
params.status = filters.status; params.status = filters.status;
} }
if (filters.dateRange && filters.dateRange[0] && filters.dateRange[1]) {
params.startDate = filters.dateRange[0].format('YYYY-MM-DD');
params.endDate = filters.dateRange[1].format('YYYY-MM-DD');
}
const data = await teacherApi.getLessons(params); const data = await teacherApi.getLessons(params);
lessons.value = data.items || []; lessons.value = data.items || [];
@ -288,8 +296,9 @@ const viewDetail = (lesson: any) => {
}; };
const goToPrepare = () => { const goToPrepare = () => {
if (selectedLesson.value?.course?.id) { const courseId = selectedLesson.value?.courseId ?? selectedLesson.value?.course?.id;
router.push(`/teacher/courses/${selectedLesson.value.course.id}/prepare`); if (courseId) {
router.push(`/teacher/courses/${courseId}/prepare`);
detailDrawerVisible.value = false; detailDrawerVisible.value = false;
} }
}; };
@ -309,8 +318,9 @@ const goToRecords = () => {
}; };
const goToCourseDetail = () => { const goToCourseDetail = () => {
if (selectedLesson.value?.course?.id) { const courseId = selectedLesson.value?.courseId ?? selectedLesson.value?.course?.id;
router.push(`/teacher/courses/${selectedLesson.value.course.id}`); if (courseId) {
router.push(`/teacher/courses/${courseId}`);
detailDrawerVisible.value = false; detailDrawerVisible.value = false;
} }
}; };

View File

@ -12,9 +12,13 @@ import com.reading.platform.dto.request.LessonUpdateRequest;
import com.reading.platform.dto.request.StudentRecordRequest; import com.reading.platform.dto.request.StudentRecordRequest;
import com.reading.platform.dto.response.LessonResponse; import com.reading.platform.dto.response.LessonResponse;
import com.reading.platform.dto.response.StudentRecordResponse; import com.reading.platform.dto.response.StudentRecordResponse;
import com.reading.platform.entity.Clazz;
import com.reading.platform.entity.Course;
import com.reading.platform.entity.Lesson; import com.reading.platform.entity.Lesson;
import com.reading.platform.entity.LessonFeedback; import com.reading.platform.entity.LessonFeedback;
import com.reading.platform.entity.StudentRecord; import com.reading.platform.entity.StudentRecord;
import com.reading.platform.mapper.ClazzMapper;
import com.reading.platform.mapper.CourseMapper;
import com.reading.platform.service.LessonService; import com.reading.platform.service.LessonService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@ -35,13 +39,17 @@ public class TeacherLessonController {
private final LessonService lessonService; private final LessonService lessonService;
private final LessonMapper lessonMapper; private final LessonMapper lessonMapper;
private final StudentRecordMapper studentRecordMapper; private final StudentRecordMapper studentRecordMapper;
private final CourseMapper courseMapper;
private final ClazzMapper clazzMapper;
@Operation(summary = "Create lesson") @Operation(summary = "Create lesson")
@PostMapping @PostMapping
public Result<LessonResponse> createLesson(@Valid @RequestBody LessonCreateRequest request) { public Result<LessonResponse> createLesson(@Valid @RequestBody LessonCreateRequest request) {
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
Lesson lesson = lessonService.createLesson(tenantId, request); Lesson lesson = lessonService.createLesson(tenantId, request);
return Result.success(lessonMapper.toVO(lesson)); LessonResponse vo = lessonMapper.toVO(lesson);
enrichWithCourseAndClass(vo);
return Result.success(vo);
} }
@Operation(summary = "Update lesson") @Operation(summary = "Update lesson")
@ -55,7 +63,9 @@ public class TeacherLessonController {
@GetMapping("/{id}") @GetMapping("/{id}")
public Result<LessonResponse> getLesson(@PathVariable Long id) { public Result<LessonResponse> getLesson(@PathVariable Long id) {
Lesson lesson = lessonService.getLessonById(id); Lesson lesson = lessonService.getLessonById(id);
return Result.success(lessonMapper.toVO(lesson)); LessonResponse vo = lessonMapper.toVO(lesson);
enrichWithCourseAndClass(vo);
return Result.success(vo);
} }
@Operation(summary = "Get my lessons") @Operation(summary = "Get my lessons")
@ -69,6 +79,7 @@ public class TeacherLessonController {
Long teacherId = SecurityUtils.getCurrentUserId(); Long teacherId = SecurityUtils.getCurrentUserId();
Page<Lesson> page = lessonService.getTeacherLessons(teacherId, pageNum, pageSize, status, startDate, endDate); Page<Lesson> page = lessonService.getTeacherLessons(teacherId, pageNum, pageSize, status, startDate, endDate);
List<LessonResponse> voList = lessonMapper.toVO(page.getRecords()); List<LessonResponse> voList = lessonMapper.toVO(page.getRecords());
voList.forEach(this::enrichWithCourseAndClass);
return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize())); return Result.success(PageResult.of(voList, page.getTotal(), page.getCurrent(), page.getSize()));
} }
@ -98,7 +109,9 @@ public class TeacherLessonController {
public Result<List<LessonResponse>> getTodayLessons() { public Result<List<LessonResponse>> getTodayLessons() {
Long tenantId = SecurityUtils.getCurrentTenantId(); Long tenantId = SecurityUtils.getCurrentTenantId();
List<Lesson> lessons = lessonService.getTodayLessons(tenantId); List<Lesson> lessons = lessonService.getTodayLessons(tenantId);
return Result.success(lessonMapper.toVO(lessons)); List<LessonResponse> voList = lessonMapper.toVO(lessons);
voList.forEach(this::enrichWithCourseAndClass);
return Result.success(voList);
} }
@Operation(summary = "Get student records") @Operation(summary = "Get student records")
@ -159,7 +172,24 @@ public class TeacherLessonController {
@GetMapping("/{id}/progress") @GetMapping("/{id}/progress")
public Result<LessonResponse> getLessonProgress(@PathVariable Long id) { public Result<LessonResponse> getLessonProgress(@PathVariable Long id) {
Lesson lesson = lessonService.getLessonProgress(id); Lesson lesson = lessonService.getLessonProgress(id);
return Result.success(lessonMapper.toVO(lesson)); LessonResponse vo = lessonMapper.toVO(lesson);
enrichWithCourseAndClass(vo);
return Result.success(vo);
}
/**
* LessonResponse 补充课程名称和班级名称
*/
private void enrichWithCourseAndClass(LessonResponse vo) {
if (vo == null) return;
if (vo.getCourseId() != null) {
Course course = courseMapper.selectById(vo.getCourseId());
vo.setCourseName(course != null ? course.getName() : null);
}
if (vo.getClassId() != null) {
Clazz clazz = clazzMapper.selectById(vo.getClassId());
vo.setClassName(clazz != null ? clazz.getName() : null);
}
} }
} }

View File

@ -29,6 +29,12 @@ public class LessonResponse {
@Schema(description = "班级 ID") @Schema(description = "班级 ID")
private Long classId; private Long classId;
@Schema(description = "课程名称(用于列表展示)")
private String courseName;
@Schema(description = "班级名称(用于列表展示)")
private String className;
@Schema(description = "教师 ID") @Schema(description = "教师 ID")
private Long teacherId; private Long teacherId;

View File

@ -1,5 +1,6 @@
package com.reading.platform.service; package com.reading.platform.service;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.reading.platform.common.exception.BusinessException; import com.reading.platform.common.exception.BusinessException;
@ -112,8 +113,8 @@ public class CourseLessonService extends ServiceImpl<CourseLessonMapper, CourseL
lesson.setPreparation(preparation); lesson.setPreparation(preparation);
lesson.setExtension(extension); lesson.setExtension(extension);
lesson.setReflection(reflection); lesson.setReflection(reflection);
// 处理空字符串为 null避免 MySQL JSON 字段错误 // 确保 assessment_data 为有效 JSONMySQL JSON 列不接受纯文本
lesson.setAssessmentData(assessmentData == null || assessmentData.isEmpty() ? null : assessmentData); lesson.setAssessmentData(toValidJsonOrNull(assessmentData));
lesson.setUseTemplate(useTemplate); lesson.setUseTemplate(useTemplate);
lesson.setSortOrder(maxSortOrder + 1); lesson.setSortOrder(maxSortOrder + 1);
lesson.setCreatedAt(LocalDateTime.now()); lesson.setCreatedAt(LocalDateTime.now());
@ -178,7 +179,7 @@ public class CourseLessonService extends ServiceImpl<CourseLessonMapper, CourseL
lesson.setReflection(reflection); lesson.setReflection(reflection);
} }
if (assessmentData != null) { if (assessmentData != null) {
lesson.setAssessmentData(assessmentData); lesson.setAssessmentData(toValidJsonOrNull(assessmentData));
} }
if (useTemplate != null) { if (useTemplate != null) {
lesson.setUseTemplate(useTemplate); lesson.setUseTemplate(useTemplate);
@ -355,6 +356,31 @@ public class CourseLessonService extends ServiceImpl<CourseLessonMapper, CourseL
log.info("教学环节重新排序成功lessonId={}", lessonId); log.info("教学环节重新排序成功lessonId={}", lessonId);
} }
/**
* assessmentData 转为 MySQL JSON 列可接受的有效 JSON
* 空值返回 null已是有效 JSON 则原样返回否则包装为 JSON 字符串
*/
private String toValidJsonOrNull(String assessmentData) {
if (assessmentData == null || assessmentData.isEmpty()) {
return null;
}
String trimmed = assessmentData.trim();
if (trimmed.isEmpty()) {
return null;
}
// 已是有效 JSON对象或数组则直接使用
if ((trimmed.startsWith("{") && trimmed.endsWith("}")) || (trimmed.startsWith("[") && trimmed.endsWith("]"))) {
try {
JSON.parse(trimmed);
return trimmed;
} catch (Exception ignored) {
// 解析失败当作普通文本处理
}
}
// 普通文本包装为 JSON 字符串
return JSON.toJSONString(assessmentData);
}
/** /**
* 查询教师的课程环节带权限检查 * 查询教师的课程环节带权限检查
*/ */