From c1f90a5e64ba53f169b9878adf6e4c25c10b18ea Mon Sep 17 00:00:00 2001 From: chenyuan Date: Sat, 1 Aug 2026 23:47:03 +0800 Subject: [PATCH] feat: support dynamic training task steps --- .../entity/StudentTrainingAnswer.java | 11 +++ .../linkCommerce/entity/TrainingTaskStep.java | 40 ++++++++++ .../impl/AiTrainingEvaluationServiceImpl.java | 4 +- .../StudentTrainingAnswerServiceImpl.java | 47 +++++++++++- .../service/impl/TrainingTaskServiceImpl.java | 75 ++++++++++++++----- .../mappers/StudentTrainingAnswerMapper.xml | 6 +- .../StudentTrainingAnswerServiceImplTest.java | 20 +++++ .../impl/TrainingTaskServiceImplTest.java | 33 +++++++- 8 files changed, 208 insertions(+), 28 deletions(-) create mode 100644 src/main/java/com/sztzjy/linkCommerce/entity/TrainingTaskStep.java diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/StudentTrainingAnswer.java b/src/main/java/com/sztzjy/linkCommerce/entity/StudentTrainingAnswer.java index 6d15dda..82e0bdf 100644 --- a/src/main/java/com/sztzjy/linkCommerce/entity/StudentTrainingAnswer.java +++ b/src/main/java/com/sztzjy/linkCommerce/entity/StudentTrainingAnswer.java @@ -35,6 +35,9 @@ public class StudentTrainingAnswer { @ApiModelProperty("步骤四答案") private String step4Answer; + @ApiModelProperty("自定义步骤答案,按步骤ID保存") + private String dynamicStepAnswers; + @ApiModelProperty("当前步骤") private Integer currentStep; @@ -138,6 +141,14 @@ public class StudentTrainingAnswer { this.step4Answer = step4Answer == null ? null : step4Answer.trim(); } + public String getDynamicStepAnswers() { + return dynamicStepAnswers; + } + + public void setDynamicStepAnswers(String dynamicStepAnswers) { + this.dynamicStepAnswers = dynamicStepAnswers == null ? null : dynamicStepAnswers.trim(); + } + public Integer getCurrentStep() { return currentStep; } diff --git a/src/main/java/com/sztzjy/linkCommerce/entity/TrainingTaskStep.java b/src/main/java/com/sztzjy/linkCommerce/entity/TrainingTaskStep.java new file mode 100644 index 0000000..d0731cb --- /dev/null +++ b/src/main/java/com/sztzjy/linkCommerce/entity/TrainingTaskStep.java @@ -0,0 +1,40 @@ +package com.sztzjy.linkCommerce.entity; + +public class TrainingTaskStep { + private String id; + private String name; + private String kind; + + public TrainingTaskStep() { + } + + public TrainingTaskStep(String id, String name, String kind) { + this.id = id; + this.name = name; + this.kind = kind; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getKind() { + return kind; + } + + public void setKind(String kind) { + this.kind = kind; + } +} diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImpl.java index 5bca9fb..54318b4 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/AiTrainingEvaluationServiceImpl.java @@ -222,7 +222,8 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ private boolean hasAnyAnswer(StudentTrainingAnswer answer) { return StringUtils.isNotBlank(answer.getStep1Answer()) || StringUtils.isNotBlank(answer.getStep2Answer()) - || StringUtils.isNotBlank(answer.getStep3Answer()) || StringUtils.isNotBlank(answer.getStep4Answer()); + || StringUtils.isNotBlank(answer.getStep3Answer()) || StringUtils.isNotBlank(answer.getStep4Answer()) + || StringUtils.isNotBlank(answer.getDynamicStepAnswers()); } private boolean isTerminalOrProcessing(String status) { @@ -243,6 +244,7 @@ public class AiTrainingEvaluationServiceImpl implements AiTrainingEvaluationServ node.put("step2Answer", answer.getStep2Answer()); node.put("step3Answer", answer.getStep3Answer()); node.put("step4Answer", answer.getStep4Answer()); + node.put("dynamicStepAnswers", answer.getDynamicStepAnswers()); return json(node); } diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentTrainingAnswerServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentTrainingAnswerServiceImpl.java index 5ac6db4..fc56b9c 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentTrainingAnswerServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/StudentTrainingAnswerServiceImpl.java @@ -1,5 +1,7 @@ package com.sztzjy.linkCommerce.service.impl; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; import com.sztzjy.linkCommerce.config.exception.handler.ServiceException; import com.sztzjy.linkCommerce.config.security.JwtUser; import com.sztzjy.linkCommerce.entity.StudentTrainingAnswer; @@ -163,6 +165,7 @@ public class StudentTrainingAnswerServiceImpl implements StudentTrainingAnswerSe record.setStep2Answer(existing.getStep2Answer()); record.setStep3Answer(existing.getStep3Answer()); record.setStep4Answer(existing.getStep4Answer()); + record.setDynamicStepAnswers(existing.getDynamicStepAnswers()); record.setSubmitTime(existing.getSubmitTime()); } @@ -171,6 +174,7 @@ public class StudentTrainingAnswerServiceImpl implements StudentTrainingAnswerSe record.setStep2Answer(""); record.setStep3Answer(""); record.setStep4Answer(""); + record.setDynamicStepAnswers(""); record.setCurrentStep(1); record.setProgressStatus(STATUS_NOT_STARTED); record.setSubmitted(Boolean.FALSE); @@ -190,10 +194,13 @@ public class StudentTrainingAnswerServiceImpl implements StudentTrainingAnswerSe if (request.getStep4Answer() != null) { record.setStep4Answer(request.getStep4Answer()); } + if (request.getDynamicStepAnswers() != null) { + record.setDynamicStepAnswers(request.getDynamicStepAnswers()); + } boolean submitted = ACTION_SUBMIT.equals(StringUtils.upperCase(StringUtils.trimToEmpty(request.getSaveAction()))); record.setSubmitted(submitted); - record.setCurrentStep(submitted ? 4 : normalizeCurrentStepValue(request.getCurrentStep())); + record.setCurrentStep(submitted ? taskStepCount(task) : normalizeCurrentStepValue(request.getCurrentStep())); record.setProgressStatus(submitted ? STATUS_COMPLETED : resolveProgressStatus(record)); if (submitted) { record.setSubmitTime(new Date()); @@ -205,17 +212,28 @@ public class StudentTrainingAnswerServiceImpl implements StudentTrainingAnswerSe if (currentStep == null || currentStep < 1) { return 1; } - if (currentStep > 4) { + return currentStep; + } + + private int taskStepCount(TrainingTask task) { + if (task == null || StringUtils.isBlank(task.getSteps())) { return 4; } - return currentStep; + try { + JSONArray steps = JSON.parseArray(task.getSteps()); + return steps == null || steps.isEmpty() ? 4 : steps.size(); + } catch (Exception ignored) { + String[] steps = StringUtils.split(task.getSteps(), "\n,>"); + return steps == null || steps.length == 0 ? 4 : steps.length; + } } private String resolveProgressStatus(StudentTrainingAnswer answer) { if (StringUtils.isNotBlank(answer.getStep1Answer()) || StringUtils.isNotBlank(answer.getStep2Answer()) || StringUtils.isNotBlank(answer.getStep3Answer()) - || StringUtils.isNotBlank(answer.getStep4Answer())) { + || StringUtils.isNotBlank(answer.getStep4Answer()) + || StringUtils.isNotBlank(answer.getDynamicStepAnswers())) { return STATUS_IN_PROGRESS; } return STATUS_NOT_STARTED; @@ -248,6 +266,7 @@ public class StudentTrainingAnswerServiceImpl implements StudentTrainingAnswerSe "step2_answer longtext NULL COMMENT 'step 2 answer json'," + "step3_answer longtext NULL COMMENT 'step 3 answer json'," + "step4_answer longtext NULL COMMENT 'step 4 answer json'," + + "dynamic_step_answers longtext NULL COMMENT 'custom step answers json'," + "current_step int DEFAULT 1 COMMENT 'current step'," + "progress_status varchar(32) DEFAULT 'NOT_STARTED' COMMENT 'progress status'," + "submitted bit(1) DEFAULT b'0' COMMENT 'submitted'," + @@ -260,6 +279,7 @@ public class StudentTrainingAnswerServiceImpl implements StudentTrainingAnswerSe "KEY idx_student_training_answer_student (student_user_id)," + "KEY idx_student_training_answer_class (teaching_class_id)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='student training answer'"); + ensureDynamicStepAnswersColumn(); jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS student_training_progress (" + "id varchar(64) NOT NULL COMMENT 'primary id'," + "student_user_id varchar(64) NOT NULL COMMENT 'student user id'," + @@ -303,6 +323,25 @@ public class StudentTrainingAnswerServiceImpl implements StudentTrainingAnswerSe studentTrainingAnswerTableChecked = true; } + private void ensureDynamicStepAnswersColumn() { + try { + Integer count = jdbcTemplate.queryForObject( + "select count(*) from information_schema.columns where table_schema = database() " + + "and table_name = 'student_training_answer' and column_name = 'dynamic_step_answers'", + Integer.class); + if (count != null && count > 0) { + return; + } + } catch (Exception ignored) { + // The column alteration below is still safe for databases that do not expose metadata here. + } + try { + jdbcTemplate.execute("ALTER TABLE student_training_answer ADD COLUMN dynamic_step_answers longtext NULL COMMENT 'custom step answers json' AFTER step4_answer"); + } catch (Exception ignored) { + // Existing deployments may already have been migrated by another application instance. + } + } + private void syncDetailTables(StudentTrainingAnswer answer) { if (jdbcTemplate == null || answer == null || StringUtils.isBlank(answer.getId())) { return; diff --git a/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java b/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java index 44f9f80..2338ddc 100644 --- a/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java +++ b/src/main/java/com/sztzjy/linkCommerce/service/impl/TrainingTaskServiceImpl.java @@ -1,6 +1,7 @@ package com.sztzjy.linkCommerce.service.impl; import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; import com.sztzjy.linkCommerce.config.security.JwtUser; import com.sztzjy.linkCommerce.entity.SchoolClass; import com.sztzjy.linkCommerce.entity.SchoolClassExample; @@ -9,6 +10,7 @@ import com.sztzjy.linkCommerce.entity.TaskAllocationExample; import com.sztzjy.linkCommerce.entity.TeachingClassStudent; import com.sztzjy.linkCommerce.entity.TrainingTask; import com.sztzjy.linkCommerce.entity.TrainingTaskClassConfig; +import com.sztzjy.linkCommerce.entity.TrainingTaskStep; import com.sztzjy.linkCommerce.entity.importDto.TrainingTaskImportDTO; import com.sztzjy.linkCommerce.mapper.SchoolClassMapper; import com.sztzjy.linkCommerce.mapper.TaskAllocationMapper; @@ -95,7 +97,7 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { normalizeTask(task); TrainingTask builtInTask = requireBuiltInTask(task.getTaskKey()); task.setProjectName(builtInTask.getProjectName()); - task.setSteps(normalizeFixedSteps(task.getSteps(), builtInTask)); + task.setSteps(normalizeSteps(task.getSteps(), builtInTask)); task.setId(UUID.randomUUID().toString()); task.setCreatedBy(operatorId); task.setCreateTime(new Date()); @@ -118,7 +120,7 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { task.setProjectName(existing.getProjectName()); task.setSort(existing.getSort()); task.setEnabled(existing.getEnabled()); - task.setSteps(normalizeFixedSteps(task.getSteps(), builtInTask)); + task.setSteps(normalizeSteps(task.getSteps(), builtInTask)); task.setUpdateTime(new Date()); trainingTaskMapper.updateByPrimaryKeySelective(task); return trainingTaskMapper.selectByPrimaryKey(id); @@ -243,7 +245,7 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { task.setProjectName(baseTask.getProjectName()); task.setSort(baseTask.getSort()); task.setEnabled(task.getEnabled() == null ? Boolean.TRUE : task.getEnabled()); - task.setSteps(normalizeFixedSteps(task.getSteps(), builtInTask)); + task.setSteps(normalizeSteps(task.getSteps(), builtInTask)); TrainingTaskClassConfig existing = trainingTaskClassConfigMapper.selectByTeachingClassAndTaskKey(teachingClassId, normalizedTaskKey); TrainingTaskClassConfig config = toConfig(teachingClassId, task, operatorId); @@ -647,7 +649,7 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { task.setBackground(StringUtils.trimToEmpty(row.getBackground())); task.setObjectives(toJsonArray(splitList(row.getObjectives()))); task.setRequirements(StringUtils.trimToEmpty(row.getRequirements())); - task.setSteps(normalizeFixedSteps(row.getSteps(), builtInTask)); + task.setSteps(normalizeSteps(row.getSteps(), builtInTask)); task.setMaterialName(StringUtils.trimToEmpty(row.getMaterialName())); task.setMaterialUrl(StringUtils.trimToEmpty(row.getMaterialUrl())); task.setSort(row.getSort()); @@ -711,23 +713,60 @@ public class TrainingTaskServiceImpl implements TrainingTaskService { return null; } - private String normalizeFixedSteps(String value, TrainingTask builtInTask) { - int expectedCount = getFixedStepCount(builtInTask); - List steps = parseJsonOrDelimitedList(value); - List normalized = steps.stream() - .map(StringUtils::trimToEmpty) - .filter(StringUtils::isNotBlank) - .limit(expectedCount) - .collect(Collectors.toList()); - while (normalized.size() < expectedCount) { - normalized.add("Step " + (normalized.size() + 1)); + private String normalizeSteps(String value, TrainingTask builtInTask) { + List steps = parseSteps(value, builtInTask); + if (steps.isEmpty()) { + throw new IllegalArgumentException("至少保留一个步骤"); } - return toJsonArray(normalized); + return JSON.toJSONString(steps); } - private int getFixedStepCount(TrainingTask builtInTask) { - List steps = parseJsonOrDelimitedList(builtInTask == null ? null : builtInTask.getSteps()); - return steps.isEmpty() ? 4 : steps.size(); + private List parseSteps(String value, TrainingTask builtInTask) { + List result = new ArrayList<>(); + String source = StringUtils.isBlank(value) && builtInTask != null ? builtInTask.getSteps() : value; + if (StringUtils.isBlank(source)) { + return result; + } + String trimmed = StringUtils.trimToEmpty(source); + if (trimmed.startsWith("[") && trimmed.endsWith("]")) { + try { + List values = JSON.parseArray(trimmed, Object.class); + if (values != null) { + for (Object valueItem : values) { + if (valueItem instanceof JSONObject) { + JSONObject item = (JSONObject) valueItem; + addStep(result, item.getString("id"), item.getString("name"), item.getString("kind"), builtInTask); + } else { + addStep(result, null, String.valueOf(valueItem), null, builtInTask); + } + } + return result; + } + } catch (Exception ignored) { + // Fall through to legacy delimiter parsing. + } + } + for (String name : parseJsonOrDelimitedList(source)) { + addStep(result, null, name, null, builtInTask); + } + return result; + } + + private void addStep(List steps, String id, String name, String kind, TrainingTask builtInTask) { + String normalizedName = StringUtils.trimToEmpty(name); + if (StringUtils.isBlank(normalizedName)) { + return; + } + int position = steps.size() + 1; + int builtInCount = parseJsonOrDelimitedList(builtInTask == null ? null : builtInTask.getSteps()).size(); + boolean builtIn = "builtin".equalsIgnoreCase(StringUtils.trimToEmpty(kind)) + || (StringUtils.isBlank(kind) && position <= builtInCount); + String normalizedKind = builtIn ? "builtin" : "custom"; + String normalizedId = StringUtils.trimToEmpty(id); + if (StringUtils.isBlank(normalizedId)) { + normalizedId = (builtIn ? "builtin-" : "custom-") + position; + } + steps.add(new TrainingTaskStep(normalizedId, normalizedName, normalizedKind)); } private List parseJsonOrDelimitedList(String value) { diff --git a/src/main/resources/mappers/StudentTrainingAnswerMapper.xml b/src/main/resources/mappers/StudentTrainingAnswerMapper.xml index 69d68b5..a359573 100644 --- a/src/main/resources/mappers/StudentTrainingAnswerMapper.xml +++ b/src/main/resources/mappers/StudentTrainingAnswerMapper.xml @@ -12,6 +12,7 @@ + @@ -23,7 +24,7 @@ id, student_user_id, teaching_class_id, task_id, task_key, task_name, step1_answer, step2_answer, - step3_answer, step4_answer, current_step, progress_status, submitted, ai_assessment_score, submit_time, create_time, update_time + step3_answer, step4_answer, dynamic_step_answers, current_step, progress_status, submitted, ai_assessment_score, submit_time, create_time, update_time