fix: task_feedback 表字段对齐与完成情况图片展示

- 新增 V47 迁移:为 task_feedback 添加 create_by、update_by、deleted 字段
- 修复 TaskFeedbackMapper 使用 deleted 替代 deleted_at
- 教师端完成情况:将「X张照片」改为图片缩略图展示,支持点击预览
- 评价弹窗提交内容预览区显示图片网格,参考家长端提交页面

Made-with: Cursor
This commit is contained in:
zhonghua 2026-03-20 19:05:59 +08:00
parent 5cb903d7ed
commit c55b2266fb
4 changed files with 192 additions and 7 deletions

View File

@ -264,8 +264,9 @@
</a-tag> </a-tag>
</div> </div>
<div class="completion-content"> <div class="completion-content">
<div v-if="completion.photos && completion.photos.length > 0" class="content-preview"> <div v-if="completion.photos && completion.photos.length > 0" class="content-photos">
<PictureOutlined /> {{ completion.photos.length }} 张照片 <img v-for="(photo, idx) in completion.photos" :key="idx" :src="photo" alt="照片"
class="photo-thumb" @click="previewImage(photo)" />
</div> </div>
<div v-if="completion.videoUrl" class="content-preview"> <div v-if="completion.videoUrl" class="content-preview">
<VideoCameraOutlined /> 有视频 <VideoCameraOutlined /> 有视频
@ -323,9 +324,11 @@
style="margin-bottom: 24px; padding: 16px; background: #fafafa; border-radius: 8px;"> style="margin-bottom: 24px; padding: 16px; background: #fafafa; border-radius: 8px;">
<h4 style="margin-bottom: 12px;">提交内容</h4> <h4 style="margin-bottom: 12px;">提交内容</h4>
<div v-if="selectedCompletionForFeedback.photos && selectedCompletionForFeedback.photos.length > 0" <div v-if="selectedCompletionForFeedback.photos && selectedCompletionForFeedback.photos.length > 0"
style="margin-bottom: 12px;"> class="review-photos" style="margin-bottom: 12px;">
<PictureOutlined style="margin-right: 8px;" /> <div class="photos-grid">
<span>{{ selectedCompletionForFeedback.photos.length }} 张照片</span> <img v-for="(photo, idx) in selectedCompletionForFeedback.photos" :key="idx" :src="photo" alt="照片"
@click="previewImage(photo)" />
</div>
</div> </div>
<div v-if="selectedCompletionForFeedback.videoUrl" style="margin-bottom: 12px;"> <div v-if="selectedCompletionForFeedback.videoUrl" style="margin-bottom: 12px;">
<VideoCameraOutlined style="margin-right: 8px;" /> <VideoCameraOutlined style="margin-right: 8px;" />
@ -366,6 +369,12 @@
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
<!-- 图片预览 -->
<a-image :style="{ display: 'none' }" :preview="{
visible: imagePreviewVisible,
onVisibleChange: (visible: boolean) => { imagePreviewVisible = visible; },
}" :src="previewImageUrl" />
</div> </div>
</template> </template>
@ -900,6 +909,14 @@ const onCompletionPageChange = (page: number) => {
loadCompletions(); loadCompletions();
}; };
//
const imagePreviewVisible = ref(false);
const previewImageUrl = ref('');
const previewImage = (url: string) => {
previewImageUrl.value = url;
imagePreviewVisible.value = true;
};
onMounted(() => { onMounted(() => {
loadTasks(); loadTasks();
loadOptions(); loadOptions();
@ -1189,5 +1206,46 @@ onMounted(() => {
color: #999; color: #999;
text-align: right; 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;
}
} }
</style> </style>

View File

@ -17,7 +17,7 @@ public interface TaskFeedbackMapper extends BaseMapper<TaskFeedback> {
/** /**
* 根据完成记录ID查询评价 * 根据完成记录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<TaskFeedback> findByCompletionId(@Param("completionId") Long completionId); Optional<TaskFeedback> findByCompletionId(@Param("completionId") Long completionId);
/** /**
@ -25,7 +25,7 @@ public interface TaskFeedbackMapper extends BaseMapper<TaskFeedback> {
*/ */
@Select("SELECT tf.* FROM task_feedback tf " + @Select("SELECT tf.* FROM task_feedback tf " +
"INNER JOIN task_completion tc ON tf.completion_id = tc.id " + "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<TaskFeedback> findByTaskIdAndStudentId(@Param("taskId") Long taskId, @Param("studentId") Long studentId); Optional<TaskFeedback> findByTaskIdAndStudentId(@Param("taskId") Long taskId, @Param("studentId") Long studentId);
} }

View File

@ -17,6 +17,7 @@ import com.reading.platform.dto.response.TaskResponse;
import com.reading.platform.dto.response.TaskWithCompletionResponse; import com.reading.platform.dto.response.TaskWithCompletionResponse;
import com.reading.platform.entity.*; import com.reading.platform.entity.*;
import com.reading.platform.mapper.CoursePackageMapper; import com.reading.platform.mapper.CoursePackageMapper;
import com.reading.platform.mapper.ParentStudentMapper;
import com.reading.platform.mapper.TaskCompletionMapper; import com.reading.platform.mapper.TaskCompletionMapper;
import com.reading.platform.mapper.TaskMapper; import com.reading.platform.mapper.TaskMapper;
import com.reading.platform.mapper.TaskTargetMapper; import com.reading.platform.mapper.TaskTargetMapper;
@ -45,6 +46,7 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
private final TaskTargetMapper taskTargetMapper; private final TaskTargetMapper taskTargetMapper;
private final TaskCompletionMapper taskCompletionMapper; private final TaskCompletionMapper taskCompletionMapper;
private final CoursePackageMapper coursePackageMapper; private final CoursePackageMapper coursePackageMapper;
private final ParentStudentMapper parentStudentMapper;
private final StudentService studentService; private final StudentService studentService;
private final ClassService classService; private final ClassService classService;
private final TaskFeedbackService taskFeedbackService; private final TaskFeedbackService taskFeedbackService;
@ -392,6 +394,11 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
// 验证任务存在且属于该租户 // 验证任务存在且属于该租户
Task task = getTaskByIdWithTenantCheck(taskId, tenantId); Task task = getTaskByIdWithTenantCheck(taskId, tenantId);
// status=PENDING 待提交返回参与任务且与家长关联尚未提交的学生 task_completion 记录
if ("PENDING".equalsIgnoreCase(status)) {
return getPendingCompletions(taskId, tenantId, pageNum, pageSize, task);
}
Page<TaskCompletion> page = new Page<>(pageNum, pageSize); Page<TaskCompletion> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<TaskCompletion> wrapper = new LambdaQueryWrapper<>(); LambdaQueryWrapper<TaskCompletion> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TaskCompletion::getTaskId, taskId); wrapper.eq(TaskCompletion::getTaskId, taskId);
@ -410,6 +417,105 @@ public class TaskServiceImpl extends ServiceImpl<TaskMapper, Task>
return PageResult.of(responses, completionPage.getTotal(), completionPage.getCurrent(), completionPage.getSize()); return PageResult.of(responses, completionPage.getTotal(), completionPage.getCurrent(), completionPage.getSize());
} }
/**
* 获取待提交学生列表参与任务班级或指定学生且与家长关联尚未提交的学生
*/
private PageResult<TaskCompletionDetailResponse> getPendingCompletions(Long taskId, Long tenantId, Integer pageNum, Integer pageSize, Task task) {
List<Long> targetStudentIds = getTargetStudentIdsForTask(taskId);
if (targetStudentIds.isEmpty()) {
return PageResult.empty();
}
// 仅保留与家长关联的学生parent_student 有记录
List<Long> studentIdsWithParent = parentStudentMapper.selectList(
new LambdaQueryWrapper<ParentStudent>()
.in(ParentStudent::getStudentId, targetStudentIds)
).stream().map(ParentStudent::getStudentId).distinct().toList();
if (studentIdsWithParent.isEmpty()) {
return PageResult.empty();
}
// 排除已提交或已评价的学生已有 task_completion status PENDING
List<TaskCompletion> existing = taskCompletionMapper.selectList(
new LambdaQueryWrapper<TaskCompletion>()
.eq(TaskCompletion::getTaskId, taskId)
.in(TaskCompletion::getStudentId, studentIdsWithParent)
.in(TaskCompletion::getStatus, "SUBMITTED", "REVIEWED", "submitted", "reviewed")
);
List<Long> submittedStudentIds = existing.stream().map(TaskCompletion::getStudentId).distinct().toList();
List<Long> pendingStudentIds = studentIdsWithParent.stream()
.filter(id -> !submittedStudentIds.contains(id))
.toList();
// 分页
int from = (pageNum - 1) * pageSize;
int to = Math.min(from + pageSize, pendingStudentIds.size());
List<Long> pagedIds = from < pendingStudentIds.size() ? pendingStudentIds.subList(from, to) : List.of();
List<TaskCompletionDetailResponse> 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<Long> getTargetStudentIdsForTask(Long taskId) {
List<TaskTarget> targets = taskTargetMapper.selectList(
new LambdaQueryWrapper<TaskTarget>().eq(TaskTarget::getTaskId, taskId)
);
if (targets.isEmpty()) {
return List.of();
}
List<Long> 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 @Override
public TaskCompletionDetailResponse getCompletionDetail(Long completionId, Long tenantId) { public TaskCompletionDetailResponse getCompletionDetail(Long completionId, Long tenantId) {
TaskCompletion completion = taskCompletionMapper.selectById(completionId); TaskCompletion completion = taskCompletionMapper.selectById(completionId);

View File

@ -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;