You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

907 lines
42 KiB
Java

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<TrainingTask> list(String projectName, String taskName, Boolean enabledOnly) {
return listDefaults(projectName, taskName, enabledOnly);
}
@Override
public List<TrainingTask> 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<TrainingTaskImportDTO> rows, String operatorId, List<String> 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<TrainingTaskImportDTO> 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<TrainingTask> listForTeachingClass(String teachingClassId, Boolean enabledOnly, JwtUser operator) {
requireReadableTeachingClass(teachingClassId, operator);
List<TrainingTask> 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<TrainingTask> 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<String> 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<TrainingTask> listForStudent(String studentUserId, Boolean enabledOnly) {
return listForStudentTeachingClass(resolveStudentTeachingClassId(studentUserId), enabledOnly);
}
@Override
public List<TrainingTask> listForStudent(JwtUser student, Boolean enabledOnly) {
return listForStudentTeachingClass(requireStudentTeachingClass(student), enabledOnly);
}
private List<TrainingTask> listForStudentTeachingClass(String teachingClassId, Boolean enabledOnly) {
ensureDefaultTasks(null);
List<TrainingTask> 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<TrainingTaskClassConfig> classTasks = ensureClassTasksInitialized(teachingClassId, defaults, null);
List<TrainingTask> tasks = filterEnabled(toTasks(classTasks), enabledOnly);
Set<String> 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<String> getPublishedTaskKeys(String teachingClassId) {
if (taskAllocationMapper == null || StringUtils.isBlank(teachingClassId)) {
return null;
}
TaskAllocationExample example = new TaskAllocationExample();
example.createCriteria().andClassIdEqualTo(teachingClassId);
List<TaskAllocation> rows = taskAllocationMapper.selectByExample(example);
boolean configured = false;
Set<String> 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<TrainingTask> readClassTasks(String teachingClassId, List<TrainingTask> defaults) {
Map<String, TrainingTask> tasksByKey = new LinkedHashMap<>();
List<TrainingTask> 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<TrainingTaskClassConfig> 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<TrainingTaskClassConfig> ensureClassTasksInitialized(String teachingClassId, List<TrainingTask> defaults, String operatorId) {
if (StringUtils.isBlank(teachingClassId) || trainingTaskClassConfigMapper == null) {
return new ArrayList<>();
}
ensureTrainingTaskClassConfigTable();
List<TrainingTask> sourceTasks = defaults == null || defaults.isEmpty() ? defaultTasks() : defaults;
List<TrainingTaskClassConfig> existing = trainingTaskClassConfigMapper.selectListByTeachingClass(teachingClassId, null);
Map<String, TrainingTaskClassConfig> 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<TrainingTaskClassConfig> refreshed = trainingTaskClassConfigMapper.selectListByTeachingClass(teachingClassId, null);
if (refreshed != null) {
return refreshed;
}
}
return new ArrayList<>(existingByKey.values());
}
private void ensureAllTeachingClassTasks(List<TrainingTask> defaults, String operatorId) {
if (schoolClassMapper == null || trainingTaskClassConfigMapper == null) {
return;
}
SchoolClassExample example = new SchoolClassExample();
example.createCriteria().andClassTypeEqualTo("TEACHING");
List<SchoolClass> 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<TrainingTask> toTasks(List<TrainingTaskClassConfig> configs) {
if (configs == null) {
return new ArrayList<>();
}
return sortTasks(configs.stream()
.map(this::toTask)
.filter(task -> task != null)
.collect(Collectors.toList()));
}
private List<TrainingTask> sortTasks(List<TrainingTask> 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<TrainingTask> filterEnabled(List<TrainingTask> 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<String> 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<String> 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<TrainingTaskStep> steps = parseSteps(value, builtInTask);
if (steps.isEmpty()) {
throw new IllegalArgumentException("至少保留一个步骤");
}
return JSON.toJSONString(steps);
}
private List<TrainingTaskStep> parseSteps(String value, TrainingTask builtInTask) {
List<TrainingTaskStep> 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<TrainingTaskStep> 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<String> parseJsonOrDelimitedList(String value) {
if (StringUtils.isBlank(value)) {
return new ArrayList<>();
}
String trimmed = StringUtils.trimToEmpty(value);
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
try {
List<String> 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<String> 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<String> errors, int rowNumber) {
String prefix = "第 " + rowNumber + " 行:";
for (String error : errors) {
if (error.startsWith(prefix)) {
return true;
}
}
return false;
}
private List<TrainingTask> defaultTasks() {
List<TrainingTask> 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<TrainingTask> 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);
}
}