diff --git a/reading-platform-frontend/src/views/teacher/tasks/TaskListView.vue b/reading-platform-frontend/src/views/teacher/tasks/TaskListView.vue index 2cd7a1f..a9c92bf 100644 --- a/reading-platform-frontend/src/views/teacher/tasks/TaskListView.vue +++ b/reading-platform-frontend/src/views/teacher/tasks/TaskListView.vue @@ -264,8 +264,9 @@
-
- {{ completion.photos.length }} 张照片 +
+ 照片
有视频 @@ -323,9 +324,11 @@ style="margin-bottom: 24px; padding: 16px; background: #fafafa; border-radius: 8px;">

提交内容

- - {{ selectedCompletionForFeedback.photos.length }} 张照片 + class="review-photos" style="margin-bottom: 12px;"> +
+ 照片 +
@@ -366,6 +369,12 @@ + + +
@@ -900,6 +909,14 @@ const onCompletionPageChange = (page: number) => { loadCompletions(); }; +// 图片预览 +const imagePreviewVisible = ref(false); +const previewImageUrl = ref(''); +const previewImage = (url: string) => { + previewImageUrl.value = url; + imagePreviewVisible.value = true; +}; + onMounted(() => { loadTasks(); loadOptions(); @@ -1189,5 +1206,46 @@ onMounted(() => { color: #999; text-align: right; } + + .completion-content { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + + .content-photos { + display: flex; + flex-wrap: wrap; + gap: 4px; + + .photo-thumb { + width: 48px; + height: 48px; + object-fit: cover; + border-radius: 6px; + cursor: pointer; + } + } + + .content-preview { + font-size: 13px; + color: #666; + } + } +} + +// 评价弹窗 - 提交内容照片网格 +.review-photos .photos-grid { + display: flex; + flex-wrap: wrap; + gap: 8px; + + img { + width: 100px; + height: 100px; + object-fit: cover; + border-radius: 8px; + cursor: pointer; + } } diff --git a/reading-platform-java/src/main/java/com/reading/platform/mapper/TaskFeedbackMapper.java b/reading-platform-java/src/main/java/com/reading/platform/mapper/TaskFeedbackMapper.java index 9449296..168ca2d 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/mapper/TaskFeedbackMapper.java +++ b/reading-platform-java/src/main/java/com/reading/platform/mapper/TaskFeedbackMapper.java @@ -17,7 +17,7 @@ public interface TaskFeedbackMapper extends BaseMapper { /** * 根据完成记录ID查询评价 */ - @Select("SELECT * FROM task_feedback WHERE completion_id = #{completionId} AND deleted_at IS NULL") + @Select("SELECT * FROM task_feedback WHERE completion_id = #{completionId} AND deleted = 0") Optional findByCompletionId(@Param("completionId") Long completionId); /** @@ -25,7 +25,7 @@ public interface TaskFeedbackMapper extends BaseMapper { */ @Select("SELECT tf.* FROM task_feedback tf " + "INNER JOIN task_completion tc ON tf.completion_id = tc.id " + - "WHERE tf.task_id = #{taskId} AND tc.student_id = #{studentId} AND tf.deleted_at IS NULL") + "WHERE tf.task_id = #{taskId} AND tc.student_id = #{studentId} AND tf.deleted = 0") Optional findByTaskIdAndStudentId(@Param("taskId") Long taskId, @Param("studentId") Long studentId); } diff --git a/reading-platform-java/src/main/java/com/reading/platform/service/impl/TaskServiceImpl.java b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TaskServiceImpl.java index 989b62a..7f76378 100644 --- a/reading-platform-java/src/main/java/com/reading/platform/service/impl/TaskServiceImpl.java +++ b/reading-platform-java/src/main/java/com/reading/platform/service/impl/TaskServiceImpl.java @@ -17,6 +17,7 @@ import com.reading.platform.dto.response.TaskResponse; import com.reading.platform.dto.response.TaskWithCompletionResponse; import com.reading.platform.entity.*; import com.reading.platform.mapper.CoursePackageMapper; +import com.reading.platform.mapper.ParentStudentMapper; import com.reading.platform.mapper.TaskCompletionMapper; import com.reading.platform.mapper.TaskMapper; import com.reading.platform.mapper.TaskTargetMapper; @@ -45,6 +46,7 @@ public class TaskServiceImpl extends ServiceImpl private final TaskTargetMapper taskTargetMapper; private final TaskCompletionMapper taskCompletionMapper; private final CoursePackageMapper coursePackageMapper; + private final ParentStudentMapper parentStudentMapper; private final StudentService studentService; private final ClassService classService; private final TaskFeedbackService taskFeedbackService; @@ -392,6 +394,11 @@ public class TaskServiceImpl extends ServiceImpl // 验证任务存在且属于该租户 Task task = getTaskByIdWithTenantCheck(taskId, tenantId); + // status=PENDING 待提交:返回参与任务且与家长关联、尚未提交的学生(无 task_completion 记录) + if ("PENDING".equalsIgnoreCase(status)) { + return getPendingCompletions(taskId, tenantId, pageNum, pageSize, task); + } + Page page = new Page<>(pageNum, pageSize); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(TaskCompletion::getTaskId, taskId); @@ -410,6 +417,105 @@ public class TaskServiceImpl extends ServiceImpl return PageResult.of(responses, completionPage.getTotal(), completionPage.getCurrent(), completionPage.getSize()); } + /** + * 获取待提交学生列表:参与任务(班级或指定学生)且与家长关联、尚未提交的学生 + */ + private PageResult getPendingCompletions(Long taskId, Long tenantId, Integer pageNum, Integer pageSize, Task task) { + List targetStudentIds = getTargetStudentIdsForTask(taskId); + if (targetStudentIds.isEmpty()) { + return PageResult.empty(); + } + + // 仅保留与家长关联的学生(parent_student 有记录) + List studentIdsWithParent = parentStudentMapper.selectList( + new LambdaQueryWrapper() + .in(ParentStudent::getStudentId, targetStudentIds) + ).stream().map(ParentStudent::getStudentId).distinct().toList(); + + if (studentIdsWithParent.isEmpty()) { + return PageResult.empty(); + } + + // 排除已提交或已评价的学生(已有 task_completion 且 status 非 PENDING) + List existing = taskCompletionMapper.selectList( + new LambdaQueryWrapper() + .eq(TaskCompletion::getTaskId, taskId) + .in(TaskCompletion::getStudentId, studentIdsWithParent) + .in(TaskCompletion::getStatus, "SUBMITTED", "REVIEWED", "submitted", "reviewed") + ); + List submittedStudentIds = existing.stream().map(TaskCompletion::getStudentId).distinct().toList(); + List pendingStudentIds = studentIdsWithParent.stream() + .filter(id -> !submittedStudentIds.contains(id)) + .toList(); + + // 分页 + int from = (pageNum - 1) * pageSize; + int to = Math.min(from + pageSize, pendingStudentIds.size()); + List pagedIds = from < pendingStudentIds.size() ? pendingStudentIds.subList(from, to) : List.of(); + + List responses = pagedIds.stream() + .map(studentId -> buildPendingCompletionResponse(taskId, task.getTitle(), studentId)) + .toList(); + + return PageResult.of(responses, (long) pendingStudentIds.size(), (long) pageNum, (long) pageSize); + } + + /** + * 获取任务目标学生 ID 列表(班级学生或指定学生) + */ + private List getTargetStudentIdsForTask(Long taskId) { + List targets = taskTargetMapper.selectList( + new LambdaQueryWrapper().eq(TaskTarget::getTaskId, taskId) + ); + if (targets.isEmpty()) { + return List.of(); + } + + List studentIds = new ArrayList<>(); + for (TaskTarget t : targets) { + String tt = t.getTargetType() != null ? t.getTargetType().toUpperCase() : ""; + if ("CLASS".equals(tt) || "class".equals(t.getTargetType())) { + studentIds.addAll(studentService.getStudentListByClassId(t.getTargetId()).stream() + .map(Student::getId).toList()); + } else if ("STUDENT".equals(tt) || "student".equals(t.getTargetType())) { + studentIds.add(t.getTargetId()); + } + } + return studentIds.stream().distinct().toList(); + } + + /** + * 构建待提交的完成详情(无 task_completion 记录) + */ + private TaskCompletionDetailResponse buildPendingCompletionResponse(Long taskId, String taskTitle, Long studentId) { + Student student = studentService.getById(studentId); + if (student == null) { + return null; + } + TaskCompletionDetailResponse.StudentInfo studentInfo = TaskCompletionDetailResponse.StudentInfo.builder() + .id(student.getId()) + .name(student.getName()) + .avatar(student.getAvatarUrl()) + .gender(student.getGender()) + .build(); + Clazz clazz = classService.getPrimaryClassByStudentId(student.getId()); + if (clazz != null) { + studentInfo.setClassInfo(TaskCompletionDetailResponse.ClassInfo.builder() + .id(clazz.getId()) + .name(clazz.getName()) + .grade(clazz.getGrade()) + .build()); + } + return TaskCompletionDetailResponse.builder() + .id(null) + .taskId(taskId) + .taskTitle(taskTitle) + .student(studentInfo) + .status("PENDING") + .statusText("待提交") + .build(); + } + @Override public TaskCompletionDetailResponse getCompletionDetail(Long completionId, Long tenantId) { TaskCompletion completion = taskCompletionMapper.selectById(completionId); diff --git a/reading-platform-java/src/main/resources/db/migration/V47__add_task_feedback_audit_columns.sql b/reading-platform-java/src/main/resources/db/migration/V47__add_task_feedback_audit_columns.sql new file mode 100644 index 0000000..4138b1e --- /dev/null +++ b/reading-platform-java/src/main/resources/db/migration/V47__add_task_feedback_audit_columns.sql @@ -0,0 +1,21 @@ +-- ===================================================== +-- 修复 task_feedback 表与 BaseEntity 字段对齐 +-- 版本: V47 +-- 日期: 2026-03-20 +-- 说明: 添加 create_by、update_by,将 deleted_at 改为 deleted +-- 解决 POST /api/v1/teacher/tasks/completions/{id}/feedback 报错 +-- "Unknown column 'create_by' in 'field list'" +-- ===================================================== + +-- 1. 添加 create_by、update_by 审计字段 +ALTER TABLE task_feedback ADD COLUMN create_by VARCHAR(50) DEFAULT NULL COMMENT '创建人'; +ALTER TABLE task_feedback ADD COLUMN update_by VARCHAR(50) DEFAULT NULL COMMENT '更新人'; + +-- 2. 添加 deleted 字段(与 BaseEntity 一致) +ALTER TABLE task_feedback ADD COLUMN deleted TINYINT DEFAULT 0 COMMENT '删除标识(0-未删除,1-已删除)'; + +-- 3. 迁移 deleted_at 数据到 deleted +UPDATE task_feedback SET deleted = 1 WHERE deleted_at IS NOT NULL; + +-- 4. 删除 deleted_at 列 +ALTER TABLE task_feedback DROP COLUMN deleted_at;