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; import com.sztzjy.linkCommerce.entity.TaskAllocation; 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; import com.sztzjy.linkCommerce.mapper.TeachingClassStudentMapper; import com.sztzjy.linkCommerce.mapper.TrainingTaskClassConfigMapper; import com.sztzjy.linkCommerce.mapper.TrainingTaskMapper; import com.sztzjy.linkCommerce.service.TrainingTaskService; import com.sztzjy.linkCommerce.service.StudentTeachingClassResolver; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; import java.util.regex.Pattern; import java.util.stream.Collectors; @Service public class TrainingTaskServiceImpl implements TrainingTaskService { private static final Pattern SPLIT_PATTERN = Pattern.compile("[;;||]"); private static final String COMPREHENSIVE_CASE_TRAINING_TASK_KEY = "comprehensive-case-training"; @Autowired public TrainingTaskMapper trainingTaskMapper; @Autowired public TrainingTaskClassConfigMapper trainingTaskClassConfigMapper; @Autowired public SchoolClassMapper schoolClassMapper; @Autowired public TeachingClassStudentMapper teachingClassStudentMapper; @Autowired(required = false) public TaskAllocationMapper taskAllocationMapper; @Autowired(required = false) public StudentTeachingClassResolver studentTeachingClassResolver; @Autowired(required = false) public JdbcTemplate jdbcTemplate; private boolean trainingTaskTableChecked = false; private boolean trainingTaskClassConfigTableChecked = false; @Override public List list(String projectName, String taskName, Boolean enabledOnly) { return listDefaults(projectName, taskName, enabledOnly); } @Override public List listDefaults(String projectName, String taskName, Boolean enabledOnly) { ensureDefaultTasks(null); Boolean enabledFilter = Boolean.TRUE.equals(enabledOnly) ? Boolean.TRUE : null; return trainingTaskMapper.selectList(StringUtils.trimToNull(projectName), StringUtils.trimToNull(taskName), enabledFilter); } @Override public TrainingTask getById(String id) { ensureDefaultTasks(null); return trainingTaskMapper.selectByPrimaryKey(id); } @Override public TrainingTask getByTaskKey(String taskKey) { ensureDefaultTasks(null); return trainingTaskMapper.selectByTaskKey(StringUtils.trimToEmpty(taskKey)); } @Override public TrainingTask create(TrainingTask task, String operatorId) { ensureTrainingTaskTable(); normalizeTask(task); TrainingTask builtInTask = requireBuiltInTask(task.getTaskKey()); task.setProjectName(builtInTask.getProjectName()); task.setSteps(normalizeSteps(task.getSteps(), builtInTask)); task.setId(UUID.randomUUID().toString()); task.setCreatedBy(operatorId); task.setCreateTime(new Date()); task.setUpdateTime(new Date()); trainingTaskMapper.insertSelective(task); return task; } @Override public TrainingTask update(String id, TrainingTask task) { ensureTrainingTaskTable(); TrainingTask existing = trainingTaskMapper.selectByPrimaryKey(id); if (existing == null) { throw new IllegalArgumentException("实训任务不存在"); } normalizeTask(task); TrainingTask builtInTask = requireBuiltInTask(existing.getTaskKey()); task.setId(id); task.setTaskKey(existing.getTaskKey()); task.setProjectName(existing.getProjectName()); task.setSort(existing.getSort()); task.setEnabled(existing.getEnabled()); task.setSteps(normalizeSteps(task.getSteps(), builtInTask)); task.setUpdateTime(new Date()); trainingTaskMapper.updateByPrimaryKeySelective(task); return trainingTaskMapper.selectByPrimaryKey(id); } @Override public void delete(String id) { ensureTrainingTaskTable(); throw new IllegalArgumentException("系统内置实训任务数量固定,不允许删除"); } @Override @Transactional(rollbackFor = Exception.class) public int importTasks(List rows, String operatorId, List errors) { ensureTrainingTaskTable(); if (rows == null || rows.isEmpty()) { errors.add("Excel中没有数据"); return 0; } int count = 0; for (int i = 0; i < rows.size(); i++) { TrainingTaskImportDTO row = rows.get(i); int rowNumber = i + 2; if (isBlankRow(row)) { continue; } TrainingTask task = buildTask(row, rowNumber, errors); if (task == null) { continue; } TrainingTask existing = trainingTaskMapper.selectByTaskKey(task.getTaskKey()); if (existing == null) { errors.add("第" + rowNumber + "行:实训任务为系统内置任务,不允许通过导入新增"); continue; } update(existing.getId(), task); count++; } return count; } @Override public List templateRows() { TrainingTaskImportDTO row = new TrainingTaskImportDTO(); row.setProjectName("互联网产品开发基础认知"); row.setTaskKey("new-product-survey"); row.setTaskName("新产品调查与分析"); row.setKnowledge("产品三层次理论用于拆解互联网新产品的价值构成。"); row.setBackground("围绕真实电商产品完成调研、分析与方案输出。"); row.setObjectives("掌握产品价值调查方法;能够完成三层次分析;形成调研结论"); row.setRequirements("按步骤完成信息收集、分析记录和结论输出。"); row.setSteps("选定产品;三层次分析;AI技术价值;总结建议"); row.setMaterialName(""); row.setMaterialUrl(""); row.setSort(1); row.setEnabled("是"); return Collections.singletonList(row); } @Override @Transactional(rollbackFor = Exception.class) public void ensureDefaultTasks(String operatorId) { ensureTrainingTaskTable(); ensureTrainingTaskClassConfigTable(); int sort = 1; for (TrainingTask task : defaultTasks()) { TrainingTask existing = trainingTaskMapper.selectByTaskKey(task.getTaskKey()); if (existing == null) { task.setSort(sort); create(task, operatorId); } sort++; } ensureAllTeachingClassTasks(trainingTaskMapper.selectList(null, null, null), operatorId); } @Override public List listForTeachingClass(String teachingClassId, Boolean enabledOnly, JwtUser operator) { requireReadableTeachingClass(teachingClassId, operator); List defaults = trainingTaskMapper.selectList(null, null, null); return filterEnabled(readClassTasks(teachingClassId, defaults), enabledOnly); } @Override public TrainingTask getClassTaskByTaskKey(String teachingClassId, String taskKey, String operatorId) { requireOwnedTeachingClass(teachingClassId, operatorId); ensureDefaultTasks(null); String normalizedTaskKey = StringUtils.trimToEmpty(taskKey); List defaults = trainingTaskMapper.selectList(null, null, null); ensureClassTasksInitialized(teachingClassId, defaults, operatorId); TrainingTaskClassConfig classTask = trainingTaskClassConfigMapper.selectByTeachingClassAndTaskKey(teachingClassId, normalizedTaskKey); if (classTask != null) { return toTask(classTask); } return trainingTaskMapper.selectByTaskKey(normalizedTaskKey); } @Override @Transactional(rollbackFor = Exception.class) public TrainingTask saveClassTask(String teachingClassId, String taskKey, TrainingTask task, String operatorId) { requireOwnedTeachingClass(teachingClassId, operatorId); ensureDefaultTasks(null); String normalizedTaskKey = StringUtils.trimToEmpty(taskKey); TrainingTask builtInTask = requireBuiltInTask(normalizedTaskKey); TrainingTask defaultTask = trainingTaskMapper.selectByTaskKey(normalizedTaskKey); TrainingTask baseTask = defaultTask == null ? builtInTask : defaultTask; ensureClassTasksInitialized(teachingClassId, trainingTaskMapper.selectList(null, null, null), operatorId); if (task == null) { throw new IllegalArgumentException("Training task cannot be null"); } if (StringUtils.isBlank(task.getTaskKey())) { task.setTaskKey(baseTask.getTaskKey()); } if (StringUtils.isBlank(task.getProjectName())) { task.setProjectName(baseTask.getProjectName()); } if (StringUtils.isBlank(task.getTaskName())) { task.setTaskName(baseTask.getTaskName()); } normalizeTask(task); task.setTaskKey(baseTask.getTaskKey()); task.setProjectName(baseTask.getProjectName()); task.setSort(baseTask.getSort()); task.setEnabled(task.getEnabled() == null ? Boolean.TRUE : task.getEnabled()); task.setSteps(normalizeSteps(task.getSteps(), builtInTask)); TrainingTaskClassConfig existing = trainingTaskClassConfigMapper.selectByTeachingClassAndTaskKey(teachingClassId, normalizedTaskKey); TrainingTaskClassConfig config = toConfig(teachingClassId, task, operatorId); config.setUpdateTime(new Date()); if (existing == null) { config.setId(UUID.randomUUID().toString()); config.setCreateTime(new Date()); trainingTaskClassConfigMapper.insertSelective(config); return toTask(config); } config.setId(existing.getId()); trainingTaskClassConfigMapper.updateByPrimaryKeySelective(config); TrainingTaskClassConfig saved = trainingTaskClassConfigMapper.selectByPrimaryKey(existing.getId()); return toTask(saved == null ? config : saved); } @Override @Transactional(rollbackFor = Exception.class) public void restoreClassTaskDefault(String teachingClassId, String taskKey, String operatorId) { requireOwnedTeachingClass(teachingClassId, operatorId); trainingTaskClassConfigMapper.deleteByTeachingClassAndTaskKey( StringUtils.trimToEmpty(teachingClassId), StringUtils.trimToEmpty(taskKey)); } @Override @Transactional(rollbackFor = Exception.class) public void restoreClassTasksDefaults(String teachingClassId, String operatorId) { requireOwnedTeachingClass(teachingClassId, operatorId); trainingTaskClassConfigMapper.deleteByTeachingClass(StringUtils.trimToEmpty(teachingClassId)); } @Override public TrainingTask getStudentTaskByTaskKey(String taskKey, String studentUserId) { return getStudentTaskByTeachingClass(taskKey, resolveStudentTeachingClassId(studentUserId)); } @Override public TrainingTask getStudentTaskByTaskKey(String taskKey, JwtUser student) { return getStudentTaskByTeachingClass(taskKey, requireStudentTeachingClass(student)); } private TrainingTask getStudentTaskByTeachingClass(String taskKey, String teachingClassId) { ensureDefaultTasks(null); String normalizedTaskKey = StringUtils.trimToEmpty(taskKey); TrainingTask defaultTask = trainingTaskMapper.selectByTaskKey(normalizedTaskKey); if (StringUtils.isBlank(teachingClassId) && requiresExplicitPublication(normalizedTaskKey)) { return null; } if (StringUtils.isNotBlank(teachingClassId)) { Set publishedTaskKeys = getPublishedTaskKeys(teachingClassId); if ((publishedTaskKeys != null && !publishedTaskKeys.contains(normalizedTaskKey)) || (requiresExplicitPublication(normalizedTaskKey) && publishedTaskKeys == null)) { return null; } ensureClassTasksInitialized(teachingClassId, trainingTaskMapper.selectList(null, null, null), null); TrainingTaskClassConfig classTask = trainingTaskClassConfigMapper.selectByTeachingClassAndTaskKey(teachingClassId, normalizedTaskKey); if (classTask != null) { return Boolean.TRUE.equals(classTask.getEnabled()) ? toTask(classTask) : null; } if (requiresExplicitPublication(normalizedTaskKey)) { return null; } } return defaultTask; } @Override public List listForStudent(String studentUserId, Boolean enabledOnly) { return listForStudentTeachingClass(resolveStudentTeachingClassId(studentUserId), enabledOnly); } @Override public List listForStudent(JwtUser student, Boolean enabledOnly) { return listForStudentTeachingClass(requireStudentTeachingClass(student), enabledOnly); } private List listForStudentTeachingClass(String teachingClassId, Boolean enabledOnly) { ensureDefaultTasks(null); List defaults = trainingTaskMapper.selectList(null, null, null); if (StringUtils.isBlank(teachingClassId)) { return filterEnabled(sortTasks(defaults).stream() .filter(task -> !requiresExplicitPublication(task.getTaskKey())) .collect(Collectors.toList()), enabledOnly); } List classTasks = ensureClassTasksInitialized(teachingClassId, defaults, null); List tasks = filterEnabled(toTasks(classTasks), enabledOnly); Set publishedTaskKeys = getPublishedTaskKeys(teachingClassId); if (publishedTaskKeys == null) { return tasks.stream() .filter(task -> !requiresExplicitPublication(task.getTaskKey())) .collect(Collectors.toList()); } return tasks.stream() .filter(task -> publishedTaskKeys.contains(task.getTaskKey())) .filter(task -> !requiresExplicitPublication(task.getTaskKey()) || Boolean.TRUE.equals(task.getEnabled())) .collect(Collectors.toList()); } private boolean requiresExplicitPublication(String taskKey) { return COMPREHENSIVE_CASE_TRAINING_TASK_KEY.equals(StringUtils.trimToEmpty(taskKey)); } /** * @return null when this teaching class has never been configured (default: all tasks), * otherwise the explicitly published task keys, which may be empty. */ private Set getPublishedTaskKeys(String teachingClassId) { if (taskAllocationMapper == null || StringUtils.isBlank(teachingClassId)) { return null; } TaskAllocationExample example = new TaskAllocationExample(); example.createCriteria().andClassIdEqualTo(teachingClassId); List rows = taskAllocationMapper.selectByExample(example); boolean configured = false; Set taskKeys = new LinkedHashSet<>(); if (rows != null) { for (TaskAllocation row : rows) { if (TaskAllocation.PUBLICATION_MARKER.equals(row.getModule())) { configured = true; } else if (row.getDisabledStatus() == null || row.getDisabledStatus() == 0) { taskKeys.add(row.getModule()); } } } return configured ? taskKeys : null; } private String requireStudentTeachingClass(JwtUser student) { if (studentTeachingClassResolver == null) { return resolveStudentTeachingClassId(student == null ? null : student.getUserId()); } return studentTeachingClassResolver.resolveRequired(student); } private SchoolClass requireOwnedTeachingClass(String teachingClassId, String operatorId) { SchoolClass schoolClass = schoolClassMapper.selectByPrimaryKey(StringUtils.trimToEmpty(teachingClassId)); if (schoolClass == null || !"TEACHING".equals(schoolClass.getClassType())) { throw new IllegalArgumentException("Teaching class not found"); } if (!StringUtils.equals(schoolClass.getCreatedBy(), operatorId)) { throw new IllegalArgumentException("Only owned teaching classes can be configured"); } return schoolClass; } SchoolClass requireReadableTeachingClass(String teachingClassId, JwtUser operator) { SchoolClass schoolClass = schoolClassMapper.selectByPrimaryKey(StringUtils.trimToEmpty(teachingClassId)); if (schoolClass == null || !"TEACHING".equals(schoolClass.getClassType())) { throw new IllegalArgumentException("Teaching class not found"); } if (operator == null || StringUtils.isBlank(operator.getUserId()) || StringUtils.isBlank(operator.getSchoolId()) || StringUtils.isBlank(schoolClass.getSchoolId()) || !StringUtils.equals(operator.getSchoolId(), schoolClass.getSchoolId())) { throw new IllegalArgumentException("Teaching class is unavailable"); } return schoolClass; } private List readClassTasks(String teachingClassId, List defaults) { Map tasksByKey = new LinkedHashMap<>(); List sourceTasks = defaults == null || defaults.isEmpty() ? defaultTasks() : defaults; for (TrainingTask task : sourceTasks) { if (task != null && StringUtils.isNotBlank(task.getTaskKey())) { tasksByKey.put(task.getTaskKey(), task); } } if (trainingTaskClassConfigMapper != null) { List classTasks = trainingTaskClassConfigMapper.selectListByTeachingClass(teachingClassId, null); if (classTasks != null) { for (TrainingTaskClassConfig classTask : classTasks) { if (classTask != null && StringUtils.isNotBlank(classTask.getTaskKey())) { tasksByKey.put(classTask.getTaskKey(), toTask(classTask)); } } } } return sortTasks(new ArrayList<>(tasksByKey.values())); } private String resolveStudentTeachingClassId(String studentUserId) { if (StringUtils.isBlank(studentUserId) || teachingClassStudentMapper == null) { return null; } TeachingClassStudent membership = teachingClassStudentMapper.selectActiveByStudentUserId(StringUtils.trimToEmpty(studentUserId)); return membership == null ? null : membership.getTeachingClassId(); } private List ensureClassTasksInitialized(String teachingClassId, List defaults, String operatorId) { if (StringUtils.isBlank(teachingClassId) || trainingTaskClassConfigMapper == null) { return new ArrayList<>(); } ensureTrainingTaskClassConfigTable(); List sourceTasks = defaults == null || defaults.isEmpty() ? defaultTasks() : defaults; List existing = trainingTaskClassConfigMapper.selectListByTeachingClass(teachingClassId, null); Map existingByKey = new LinkedHashMap<>(); if (existing != null) { for (TrainingTaskClassConfig config : existing) { if (config != null && StringUtils.isNotBlank(config.getTaskKey())) { existingByKey.put(config.getTaskKey(), config); } } } String creator = resolveClassTaskCreator(teachingClassId, operatorId); boolean inserted = false; for (TrainingTask task : sourceTasks) { if (task == null || StringUtils.isBlank(task.getTaskKey()) || existingByKey.containsKey(task.getTaskKey())) { continue; } TrainingTaskClassConfig config = toConfig(teachingClassId, task, creator); Date now = new Date(); config.setId(UUID.randomUUID().toString()); config.setCreateTime(now); config.setUpdateTime(now); trainingTaskClassConfigMapper.insertSelective(config); existingByKey.put(config.getTaskKey(), config); inserted = true; } if (inserted) { List refreshed = trainingTaskClassConfigMapper.selectListByTeachingClass(teachingClassId, null); if (refreshed != null) { return refreshed; } } return new ArrayList<>(existingByKey.values()); } private void ensureAllTeachingClassTasks(List defaults, String operatorId) { if (schoolClassMapper == null || trainingTaskClassConfigMapper == null) { return; } SchoolClassExample example = new SchoolClassExample(); example.createCriteria().andClassTypeEqualTo("TEACHING"); List teachingClasses = schoolClassMapper.selectByExample(example); if (teachingClasses == null || teachingClasses.isEmpty()) { return; } for (SchoolClass teachingClass : teachingClasses) { if (teachingClass == null || StringUtils.isBlank(teachingClass.getSchoolClassId())) { continue; } String creator = StringUtils.defaultIfBlank(operatorId, teachingClass.getCreatedBy()); ensureClassTasksInitialized(teachingClass.getSchoolClassId(), defaults, creator); } } private String resolveClassTaskCreator(String teachingClassId, String operatorId) { if (StringUtils.isNotBlank(operatorId)) { return operatorId; } if (schoolClassMapper == null) { return null; } SchoolClass schoolClass = schoolClassMapper.selectByPrimaryKey(StringUtils.trimToEmpty(teachingClassId)); return schoolClass == null ? null : schoolClass.getCreatedBy(); } private List toTasks(List configs) { if (configs == null) { return new ArrayList<>(); } return sortTasks(configs.stream() .map(this::toTask) .filter(task -> task != null) .collect(Collectors.toList())); } private List sortTasks(List tasks) { if (tasks == null) { return new ArrayList<>(); } return tasks.stream() .sorted(Comparator.comparing(task -> task.getSort() == null ? 0 : task.getSort())) .collect(Collectors.toList()); } private List filterEnabled(List tasks, Boolean enabledOnly) { if (!Boolean.TRUE.equals(enabledOnly)) { return tasks; } return tasks.stream() .filter(task -> Boolean.TRUE.equals(task.getEnabled())) .collect(Collectors.toList()); } private TrainingTask toTask(TrainingTaskClassConfig config) { if (config == null) { return null; } TrainingTask task = new TrainingTask(); task.setId(config.getId()); task.setTaskKey(config.getTaskKey()); task.setProjectName(config.getProjectName()); task.setTaskName(config.getTaskName()); task.setKnowledge(config.getKnowledge()); task.setBackground(config.getBackground()); task.setObjectives(config.getObjectives()); task.setRequirements(config.getRequirements()); task.setSteps(config.getSteps()); task.setMaterialName(config.getMaterialName()); task.setMaterialUrl(config.getMaterialUrl()); task.setSort(config.getSort()); task.setEnabled(config.getEnabled()); task.setCreatedBy(config.getCreatedBy()); task.setCreateTime(config.getCreateTime()); task.setUpdateTime(config.getUpdateTime()); return task; } private TrainingTaskClassConfig toConfig(String teachingClassId, TrainingTask task, String operatorId) { TrainingTaskClassConfig config = new TrainingTaskClassConfig(); config.setTeachingClassId(teachingClassId); config.setTaskKey(task.getTaskKey()); config.setProjectName(task.getProjectName()); config.setTaskName(task.getTaskName()); config.setKnowledge(task.getKnowledge()); config.setBackground(task.getBackground()); config.setObjectives(task.getObjectives()); config.setRequirements(task.getRequirements()); config.setSteps(task.getSteps()); config.setMaterialName(task.getMaterialName()); config.setMaterialUrl(task.getMaterialUrl()); config.setSort(task.getSort()); config.setEnabled(task.getEnabled()); config.setCreatedBy(operatorId); return config; } private void ensureTrainingTaskTable() { if (trainingTaskTableChecked || jdbcTemplate == null) { return; } jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS training_task (" + "id varchar(64) NOT NULL COMMENT '主键ID'," + "task_key varchar(128) NOT NULL COMMENT '学生端页面任务标识'," + "project_name varchar(128) NOT NULL COMMENT '所属项目'," + "task_name varchar(128) NOT NULL COMMENT '实训任务名称'," + "knowledge text NULL COMMENT '任务分析'," + "background text NULL COMMENT '实训背景'," + "objectives text NULL COMMENT '实训目标JSON数组'," + "requirements text NULL COMMENT '实训要求'," + "steps text NULL COMMENT '步骤名称JSON数组'," + "material_name varchar(255) NULL COMMENT '资料名称'," + "material_url varchar(500) NULL COMMENT '资料地址'," + "sort int DEFAULT 0 COMMENT '排序'," + "enabled bit(1) DEFAULT b'1' COMMENT '是否启用'," + "created_by varchar(64) NULL COMMENT '创建人'," + "create_time datetime NULL COMMENT '创建时间'," + "update_time datetime NULL COMMENT '更新时间'," + "PRIMARY KEY (id)," + "UNIQUE KEY uk_training_task_key (task_key)," + "KEY idx_training_task_project (project_name)," + "KEY idx_training_task_sort (sort)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='实训任务配置'"); jdbcTemplate.execute("ALTER TABLE training_task CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci"); trainingTaskTableChecked = true; } private void ensureTrainingTaskClassConfigTable() { if (trainingTaskClassConfigTableChecked || jdbcTemplate == null) { return; } jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS training_task_class_config (" + "id varchar(64) NOT NULL," + "teaching_class_id varchar(64) NOT NULL," + "task_key varchar(128) NOT NULL," + "project_name varchar(128) NOT NULL," + "task_name varchar(128) NOT NULL," + "knowledge text NULL," + "background text NULL," + "objectives text NULL," + "requirements text NULL," + "steps text NULL," + "material_name varchar(255) NULL," + "material_url varchar(500) NULL," + "sort int DEFAULT 0," + "enabled bit(1) DEFAULT b'1'," + "created_by varchar(64) NULL," + "create_time datetime NULL," + "update_time datetime NULL," + "PRIMARY KEY (id)," + "UNIQUE KEY uk_training_task_class_key (teaching_class_id, task_key)," + "KEY idx_training_task_class_enabled_sort (teaching_class_id, enabled, sort)," + "KEY idx_training_task_class_created_by (created_by)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Class scoped training task config'"); trainingTaskClassConfigTableChecked = true; } TrainingTask buildTask(TrainingTaskImportDTO row, int rowNumber, List errors) { String projectName = StringUtils.trimToEmpty(row.getProjectName()); String taskKey = StringUtils.trimToEmpty(row.getTaskKey()); String taskName = StringUtils.trimToEmpty(row.getTaskName()); if (StringUtils.isBlank(projectName)) { errors.add("第 " + rowNumber + " 行:所属项目不能为空"); } if (StringUtils.isBlank(taskKey)) { errors.add("第 " + rowNumber + " 行:任务标识不能为空"); } if (StringUtils.isBlank(taskName)) { errors.add("第 " + rowNumber + " 行:实训任务名称不能为空"); } TrainingTask builtInTask = getBuiltInTask(taskKey); if (StringUtils.isNotBlank(taskKey) && builtInTask == null) { errors.add("第 " + rowNumber + " 行:任务标识不是系统内置实训任务"); } Boolean enabled = parseEnabled(row.getEnabled()); if (StringUtils.isNotBlank(row.getEnabled()) && enabled == null) { errors.add("第 " + rowNumber + " 行:是否启用请填写是或否"); } if (hasRowError(errors, rowNumber)) { return null; } TrainingTask task = new TrainingTask(); task.setProjectName(projectName); task.setTaskKey(taskKey); task.setTaskName(taskName); task.setKnowledge(StringUtils.trimToEmpty(row.getKnowledge())); task.setBackground(StringUtils.trimToEmpty(row.getBackground())); task.setObjectives(toJsonArray(splitList(row.getObjectives()))); task.setRequirements(StringUtils.trimToEmpty(row.getRequirements())); task.setSteps(normalizeSteps(row.getSteps(), builtInTask)); task.setMaterialName(StringUtils.trimToEmpty(row.getMaterialName())); task.setMaterialUrl(StringUtils.trimToEmpty(row.getMaterialUrl())); task.setSort(row.getSort()); task.setEnabled(enabled == null ? Boolean.TRUE : enabled); return task; } public List splitList(String value) { if (StringUtils.isBlank(value)) { return new ArrayList<>(); } return Arrays.stream(SPLIT_PATTERN.split(value)) .map(StringUtils::trimToEmpty) .filter(StringUtils::isNotBlank) .collect(Collectors.toList()); } private void normalizeTask(TrainingTask task) { if (task == null) { throw new IllegalArgumentException("实训任务不能为空"); } if (StringUtils.isBlank(task.getProjectName())) { throw new IllegalArgumentException("所属项目不能为空"); } if (StringUtils.isBlank(task.getTaskKey())) { throw new IllegalArgumentException("任务标识不能为空"); } if (StringUtils.isBlank(task.getTaskName())) { throw new IllegalArgumentException("实训任务名称不能为空"); } task.setProjectName(StringUtils.trimToEmpty(task.getProjectName())); task.setTaskKey(StringUtils.trimToEmpty(task.getTaskKey())); task.setTaskName(StringUtils.trimToEmpty(task.getTaskName())); task.setKnowledge(StringUtils.trimToEmpty(task.getKnowledge())); task.setBackground(StringUtils.trimToEmpty(task.getBackground())); task.setObjectives(normalizeJsonArray(task.getObjectives())); task.setRequirements(StringUtils.trimToEmpty(task.getRequirements())); task.setSteps(normalizeJsonArray(task.getSteps())); task.setMaterialName(StringUtils.trimToEmpty(task.getMaterialName())); task.setMaterialUrl(StringUtils.trimToEmpty(task.getMaterialUrl())); if (task.getEnabled() == null) { task.setEnabled(Boolean.TRUE); } } private TrainingTask requireBuiltInTask(String taskKey) { TrainingTask builtInTask = getBuiltInTask(taskKey); if (builtInTask == null) { throw new IllegalArgumentException("任务标识不是系统内置实训任务"); } return builtInTask; } private TrainingTask getBuiltInTask(String taskKey) { String normalizedTaskKey = StringUtils.trimToEmpty(taskKey); for (TrainingTask task : defaultTasks()) { if (normalizedTaskKey.equals(task.getTaskKey())) { return task; } } return null; } private String normalizeSteps(String value, TrainingTask builtInTask) { List steps = parseSteps(value, builtInTask); if (steps.isEmpty()) { throw new IllegalArgumentException("至少保留一个步骤"); } return JSON.toJSONString(steps); } 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) { if (StringUtils.isBlank(value)) { return new ArrayList<>(); } String trimmed = StringUtils.trimToEmpty(value); if (trimmed.startsWith("[") && trimmed.endsWith("]")) { try { List parsed = JSON.parseArray(trimmed, String.class); return parsed == null ? new ArrayList<>() : parsed; } catch (Exception ignored) { return splitList(trimmed); } } return splitList(trimmed); } private String normalizeJsonArray(String value) { if (StringUtils.isBlank(value)) { return "[]"; } String trimmed = StringUtils.trimToEmpty(value); if (trimmed.startsWith("[") && trimmed.endsWith("]")) { return trimmed; } return toJsonArray(splitList(trimmed)); } private String toJsonArray(List list) { return JSON.toJSONString(list == null ? Collections.emptyList() : list); } private Boolean parseEnabled(String value) { String normalized = StringUtils.trimToEmpty(value); if (StringUtils.isBlank(normalized)) { return Boolean.TRUE; } if ("是".equals(normalized) || "启用".equals(normalized) || "true".equalsIgnoreCase(normalized) || "1".equals(normalized)) { return Boolean.TRUE; } if ("否".equals(normalized) || "禁用".equals(normalized) || "false".equalsIgnoreCase(normalized) || "0".equals(normalized)) { return Boolean.FALSE; } return null; } private boolean isBlankRow(TrainingTaskImportDTO row) { if (row == null) { return true; } return StringUtils.isBlank(row.getProjectName()) && StringUtils.isBlank(row.getTaskKey()) && StringUtils.isBlank(row.getTaskName()) && StringUtils.isBlank(row.getKnowledge()) && StringUtils.isBlank(row.getBackground()) && StringUtils.isBlank(row.getObjectives()) && StringUtils.isBlank(row.getRequirements()) && StringUtils.isBlank(row.getSteps()) && StringUtils.isBlank(row.getMaterialName()) && StringUtils.isBlank(row.getMaterialUrl()) && row.getSort() == null && StringUtils.isBlank(row.getEnabled()); } private boolean hasRowError(List errors, int rowNumber) { String prefix = "第 " + rowNumber + " 行:"; for (String error : errors) { if (error.startsWith(prefix)) { return true; } } return false; } private List defaultTasks() { List tasks = new ArrayList<>(); addDefault(tasks, "综合实训", COMPREHENSIVE_CASE_TRAINING_TASK_KEY, "综合案例实训"); addDefault(tasks, "互联网产品开发基础认知", "new-product-survey", "新产品调查与分析"); addDefault(tasks, "互联网产品开发基础认知", "product-development-factors", "产品开发关键因素"); addDefault(tasks, "互联网产品开发基础认知", "product-development-process", "产品开发主要流程"); addDefault(tasks, "市场洞察与需求分析", "industry-demand-analysis", "行业需求分析"); addDefault(tasks, "市场洞察与需求分析", "competitive-environment-analysis", "竞争环境分析"); addDefault(tasks, "市场洞察与需求分析", "market-opportunity-selection", "市场机会识别与选品"); addDefault(tasks, "市场洞察与需求分析", "business-feasibility", "商业可行性论证"); addDefault(tasks, "市场洞察与需求分析", "target-user-profile", "目标用户画像"); addDefault(tasks, "市场洞察与需求分析", "consumer-behavior", "消费者需求与行为"); addDefault(tasks, "市场洞察与需求分析", "consumer-scenario", "消费场景解构"); addDefault(tasks, "市场洞察与需求分析", "feature-conversion", "需求转化为功能"); addDefault(tasks, "市场洞察与需求分析", "product-value-positioning", "产品价值主张及定位"); addDefault(tasks, "产品规划与设计", "product-structure", "产品架构设计"); addDefault(tasks, "产品规划与设计", "after-sales-service", "售后服务流程设计"); addDefault(tasks, "产品规划与设计", "requirements-doc", "产品需求说明书"); addDefault(tasks, "产品开发与测试验证", "hardware-cost", "智能硬件成本估算"); addDefault(tasks, "产品开发与测试验证", "supplier-evaluation", "供应渠道评估"); addDefault(tasks, "产品开发与测试验证", "purchase-demand-forecast", "采购需求预测"); addDefault(tasks, "产品开发与测试验证", "product-pricing", "产品定价"); addDefault(tasks, "产品上线与运营推广", "category-optimization", "品类结构优化"); addDefault(tasks, "产品上线与运营推广", "release-channel-evaluation", "产品发布渠道评估"); addDefault(tasks, "产品上线与运营推广", "promotion-effect-optimization", "投放效果评估与优化"); addDefault(tasks, "产品上线与运营推广", "product-diagnosis-iteration", "产品问题诊断与迭代"); return tasks; } private void addDefault(List tasks, String projectName, String taskKey, String taskName) { TrainingTask task = new TrainingTask(); task.setProjectName(projectName); task.setTaskKey(taskKey); task.setTaskName(taskName); task.setKnowledge(taskName + "任务分析由教师在实训任务管理中维护。"); task.setBackground(taskName + "围绕真实业务场景展开,要求学生完成调研、分析与成果输出。"); task.setObjectives(toJsonArray(Arrays.asList("理解" + taskName + "的核心方法", "能够按步骤完成实训成果"))); task.setRequirements("按任务步骤完成信息收集、分析记录和结论输出,提交内容需要结构清晰、依据充分。"); task.setSteps(toJsonArray(Arrays.asList("任务理解", "资料收集", "分析整理", "成果提交"))); task.setMaterialName(""); task.setMaterialUrl(""); task.setEnabled(Boolean.TRUE); tasks.add(task); } }