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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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);
}
}