package jnpf.tmsService.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import jnpf.audit.sdk.annotation.AuditLog; import jnpf.exception.DataException; import jnpf.tmsEntity.TmsAnnualPlanEntity; import jnpf.tmsEntity.TmsAnnualPlanItemEntity; import jnpf.tmsEntity.TmsExamEntity; import jnpf.tmsEntity.TmsMaterialEntity; import jnpf.tmsEntity.TmsPaperEntity; import jnpf.tmsEntity.TmsPersonTaskEntity; import jnpf.tmsEntity.TmsPlaceEntity; import jnpf.tmsEntity.TmsPostPlanEntity; import jnpf.tmsEntity.TmsPostPlanItemEntity; import jnpf.tmsEntity.TmsRecordEntity; import jnpf.tmsEntity.TmsTaskEntity; import jnpf.tmsEntity.TmsTaskFileEntity; import jnpf.tmsEntity.TmsTaskQuizEntity; import jnpf.tmsEntity.TmsTrainModeEntity; import jnpf.tmsEntity.TmsTrainerEntity; import jnpf.tmsEntity.task.TmsFromPlanResult; import jnpf.tmsEntity.task.TmsPersonTaskForm; import jnpf.tmsEntity.task.TmsTaskFileForm; import jnpf.tmsEntity.task.TmsTaskQuizForm; import jnpf.tmsEntity.task.TmsTaskForm; import jnpf.tmsEntity.task.TmsTaskQuery; import jnpf.tmsEntity.enums.TmsTaskPublishStatus; import jnpf.tmsEntity.enums.TmsPersonTaskStatus; import jnpf.tmsEntity.enums.TmsTaskSourceType; import jnpf.tmsEntity.enums.TmsTaskCategory; import jnpf.tmsEntity.enums.TmsTaskKind; import jnpf.tmsEntity.enums.TmsArchiveStatus; import jnpf.tmsEntity.enums.TmsExamStatus; import jnpf.tmsEntity.enums.TmsEvalMode; import jnpf.tmsEntity.enums.TmsOpenClosedInvalid; import jnpf.tmsEntity.enums.TmsPlanLineStatus; import jnpf.tmsEntity.enums.TmsTrainMode; import jnpf.tmsMapper.TmsAnnualPlanItemMapper; import jnpf.tmsMapper.TmsAnnualPlanMapper; import jnpf.tmsMapper.TmsExamMapper; import jnpf.tmsMapper.TmsMaterialMapper; import jnpf.tmsMapper.TmsPaperMapper; import jnpf.tmsMapper.TmsPersonTaskMapper; import jnpf.tmsMapper.TmsPlaceMapper; import jnpf.tmsMapper.TmsPostPlanItemMapper; import jnpf.tmsMapper.TmsPostPlanMapper; import jnpf.tmsMapper.TmsRecordMapper; import jnpf.tmsMapper.TmsTaskFileMapper; import jnpf.tmsMapper.TmsTaskQuizMapper; import jnpf.tmsMapper.TmsTaskMapper; import jnpf.tmsMapper.TmsTrainModeMapper; import jnpf.tmsMapper.TmsTrainerMapper; import jnpf.tmsService.TmsTaskService; import jnpf.util.DateUtil; import jnpf.util.JsonUtil; import jnpf.util.RandomUtil; import jnpf.util.StringUtil; import jnpf.util.UserProvider; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.time.LocalDate; import java.time.YearMonth; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; /** * 培训任务核心实现。 * *

编号约定:草稿 {@code TD…},发布后正式号 {@code TT…}。 * 计划生成幂等键:{@code source_item_id}(同一计划明细不重复建任务)。

* * @see TmsTaskService#publish(String) * @see TmsTaskService#createFromAnnual(String) * @see TmsTaskService#createFromPost(String) */ @Service @RequiredArgsConstructor public class TmsTaskServiceImpl implements TmsTaskService { /** 草稿任务编号前缀 */ private static final String PREFIX_DRAFT = "TD"; /** 已发布任务编号前缀 */ private static final String PREFIX_PUBLISH = "TT"; private static final String FMT = "yyyy-MM-dd HH:mm:ss"; private final TmsTaskMapper taskMapper; private final TmsTrainerMapper trainerMapper; private final TmsPersonTaskMapper personTaskMapper; private final TmsRecordMapper recordMapper; private final TmsPaperMapper paperMapper; private final TmsExamMapper examMapper; private final TmsMaterialMapper materialMapper; private final TmsTaskFileMapper taskFileMapper; private final TmsTaskQuizMapper taskQuizMapper; private final TmsPlaceMapper placeMapper; private final TmsTrainModeMapper trainModeMapper; private final TmsAnnualPlanMapper annualPlanMapper; private final TmsAnnualPlanItemMapper annualPlanItemMapper; private final TmsPostPlanMapper postPlanMapper; private final TmsPostPlanItemMapper postPlanItemMapper; /** * 培训任务:条件分页查询列表 */ @Override public List getList(TmsTaskQuery query) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); if (StringUtil.isNotEmpty(query.getPublishStatus())) { qw.eq(TmsTaskEntity::getPublishStatus, query.getPublishStatus()); } if (StringUtil.isNotEmpty(query.getTrainMode())) { qw.eq(TmsTaskEntity::getTrainMode, query.getTrainMode()); } if (StringUtil.isNotEmpty(query.getSourceType())) { qw.eq(TmsTaskEntity::getSourceType, query.getSourceType()); } if (StringUtil.isNotEmpty(query.getKeyword()) && !"null".equalsIgnoreCase(query.getKeyword())) { String kw = query.getKeyword().trim(); qw.and(w -> w.like(TmsTaskEntity::getTaskNo, kw).or().like(TmsTaskEntity::getSubject, kw)); } Date from = parseDayStart(query.getStartTimeFrom()); Date to = parseDayEnd(query.getStartTimeTo()); if (from != null) { qw.ge(TmsTaskEntity::getStartTime, from); } if (to != null) { qw.le(TmsTaskEntity::getStartTime, to); } qw.orderByDesc(TmsTaskEntity::getStartTime).orderByDesc(TmsTaskEntity::getCreatorTime); long current = query.getCurrentPage() > 0 ? query.getCurrentPage() : 1; long size = query.getPageSize() > 0 ? query.getPageSize() : 20; Page page = taskMapper.selectPage(new Page<>(current, size), qw); List list = new ArrayList<>(page.getRecords().size()); for (TmsTaskEntity row : page.getRecords()) { list.add(toForm(row, false)); } query.setData(list, page.getTotal()); return list; } /** * 培训任务:按主键查询详情 */ @Override public TmsTaskForm getInfo(String id) { TmsTaskEntity entity = requireOne(id); TmsTaskForm form = toForm(entity, true); List persons = personTaskMapper.selectList( new LambdaQueryWrapper() .eq(TmsPersonTaskEntity::getTaskId, id) .orderByAsc(TmsPersonTaskEntity::getCreatorTime)); List personForms = new ArrayList<>(persons.size()); for (TmsPersonTaskEntity p : persons) { TmsPersonTaskForm pf = new TmsPersonTaskForm(); pf.setId(p.getId()); pf.setUserId(p.getUserId()); pf.setDeptId(p.getDeptId()); pf.setBizStatus(p.getBizStatus()); pf.setSignTime(formatDate(p.getSignTime())); pf.setExamScore(p.getExamScore()); pf.setPassFlag(p.getPassFlag()); pf.setGuestFlag(p.getGuestFlag()); personForms.add(pf); } form.setPersonTasks(personForms); form.setPersonCount(personForms.size()); form.setFiles(listTaskFiles(id)); form.setQuizzes(listTaskQuizzes(id)); return form; } /** * 培训任务:新增 */ @Override @Transactional(rollbackFor = Exception.class) public String create(TmsTaskForm form) { // 空白新增仅允许临时培训;年/岗计划任务应由计划侧生成并带 source_id/item_id boolean hasSourceLink = StringUtil.isNotEmpty(form.getSourceId()) || StringUtil.isNotEmpty(form.getSourceItemId()); if (!hasSourceLink) { form.setSourceType(TmsTaskSourceType.TEMP.getCode()); form.setCategory(TmsTaskCategory.TEMP.getCode()); } validateForm(form, true); TmsTaskEntity entity = new TmsTaskEntity(); entity.setId(RandomUtil.uuId()); entity.setTaskNo(nextTaskNo(PREFIX_DRAFT)); entity.setPublishStatus(TmsTaskPublishStatus.DRAFT.getCode()); entity.setTaskKind(StringUtil.isNotEmpty(form.getTaskKind()) ? form.getTaskKind() : "new"); fillEntity(entity, form, false); applyDefaults(entity); fillRefs(entity); taskMapper.insert(entity); saveTaskFiles(entity.getId(), form.getFiles()); saveTaskQuizzes(entity.getId(), form.getQuizzes()); return entity.getId(); } /** * 培训任务:更新 */ @Override @Transactional(rollbackFor = Exception.class) public void update(String id, TmsTaskForm form) { TmsTaskEntity entity = requireOne(id); if (!TmsTaskPublishStatus.DRAFT.equalsCode(entity.getPublishStatus())) { throw new DataException("仅未发布任务可编辑"); } boolean fromPlan = isPlanSourced(entity); if (fromPlan) { // 计划带出:来源链路只读,业务内容允许维护补全 form.setCategory(entity.getCategory()); form.setSourceType(entity.getSourceType()); form.setSourcePlanNo(entity.getSourcePlanNo()); form.setSourceId(entity.getSourceId()); form.setSourceItemId(entity.getSourceItemId()); } validateForm(form, false); fillEntity(entity, form, false); applyDefaults(entity); fillRefs(entity); entity.setLastModifyUserId(UserProvider.getLoginUserId()); entity.setLastModifyTime(new Date()); taskMapper.updateById(entity); if (form.getFiles() != null) { saveTaskFiles(entity.getId(), form.getFiles()); } if (form.getQuizzes() != null) { saveTaskQuizzes(entity.getId(), form.getQuizzes()); } } /** * 培训任务:删除 */ @Override @Transactional(rollbackFor = Exception.class) public void delete(String id) { TmsTaskEntity entity = requireOne(id); if (!TmsTaskPublishStatus.DRAFT.equalsCode(entity.getPublishStatus())) { throw new DataException("仅未发布任务可删除"); } Long cnt = personTaskMapper.selectCount( new LambdaQueryWrapper().eq(TmsPersonTaskEntity::getTaskId, id)); if (cnt != null && cnt > 0) { throw new DataException("已生成个人任务,不可删除"); } taskMapper.deleteById(id); } /** * 发布流水线(顺序固定,勿拆事务): *
    *
  1. 校验草稿 + 发布必填项
  2. *
  3. 正式编号 TT(若仍为 TD)
  4. *
  5. 按 trainees 插入个人任务 todo
  6. *
  7. 插入或复活培训记录 open
  8. *
  9. 讲义快照到记录;考试模式预生成待考壳
  10. *
  11. 任务置 published
  12. *
*/ @Override @Transactional(rollbackFor = Exception.class) @AuditLog(action = "TASK_PUBLISH", label = "发布培训任务", bizType = "TMS_TASK", bizCodeExpr = "#id", targetTable = "tms_task", targetIdExpr = "#id") /** * 培训任务:发布 */ public void publish(String id) { TmsTaskEntity entity = requireOne(id); if (!TmsTaskPublishStatus.DRAFT.equalsCode(entity.getPublishStatus())) { throw new DataException("仅未发布任务可发布"); } validateForPublish(entity); String taskNo = entity.getTaskNo(); if (StringUtil.isEmpty(taskNo) || taskNo.startsWith(PREFIX_DRAFT)) { taskNo = nextTaskNo(PREFIX_PUBLISH); entity.setTaskNo(taskNo); } List userIds = splitTrainees(entity.getTrainees()); if (userIds.isEmpty()) { throw new DataException("培训对象不能为空"); } Date now = new Date(); BigDecimal hours = resolveTaskHours(entity); List createdPersons = new ArrayList<>(); for (String userId : userIds) { TmsPersonTaskEntity pt = new TmsPersonTaskEntity(); pt.setId(RandomUtil.uuId()); pt.setTaskId(entity.getId()); pt.setTaskNo(taskNo); pt.setUserId(userId); pt.setHours(hours); pt.setBizStatus(TmsPersonTaskStatus.TODO.getCode()); pt.setGuestFlag("0"); pt.setCreatorUserId(UserProvider.getLoginUserId()); pt.setCreatorTime(now); personTaskMapper.insert(pt); createdPersons.add(pt); } String recordId = null; List existingRecords = recordMapper.selectList( new LambdaQueryWrapper().eq(TmsRecordEntity::getTaskId, entity.getId())); if (existingRecords == null || existingRecords.isEmpty()) { recordId = RandomUtil.uuId(); TmsRecordEntity record = new TmsRecordEntity(); record.setId(recordId); record.setRecordNo(taskNo); record.setTaskId(entity.getId()); record.setTaskNo(taskNo); record.setCategory(entity.getCategory()); record.setSubject(entity.getSubject()); record.setTeacherId(entity.getTeacherId()); record.setTrainType(entity.getTrainType()); record.setTrainMode(entity.getTrainMode()); record.setEvalMode(entity.getEvalMode()); record.setStartTime(entity.getStartTime()); record.setEndTime(entity.getEndTime()); record.setCloseTime(entity.getCloseTime()); // FDS:发布后为进行中 open;关闭时间到才置 pending record.setArchiveStatus(TmsArchiveStatus.OPEN.getCode()); record.setCreatorUserId(UserProvider.getLoginUserId()); record.setCreatorTime(now); recordMapper.insert(record); } else { recordId = existingRecords.get(0).getId(); recordMapper.update(null, new LambdaUpdateWrapper() .eq(TmsRecordEntity::getId, recordId) .set(TmsRecordEntity::getArchiveStatus, TmsArchiveStatus.OPEN.getCode()) .set(TmsRecordEntity::getLastModifyUserId, UserProvider.getLoginUserId()) .set(TmsRecordEntity::getLastModifyTime, now)); } // FDS:同步教材到记录(讲义快照) syncLectureFileToRecord(recordId, entity.getId()); // FDS:考核方式=在线考试时预生成 tms_exam 待考壳 if (TmsEvalMode.EXAM.equalsCode(entity.getEvalMode()) && StringUtil.isNotEmpty(entity.getPaperId())) { TmsPaperEntity paper = paperMapper.selectById(entity.getPaperId()); BigDecimal passScore = entity.getPassScore(); if (passScore == null && paper != null) { passScore = paper.getPassScore(); } Integer durationMin = paper == null ? null : paper.getDurationMin(); String paperName = StringUtil.isNotEmpty(entity.getPaperName()) ? entity.getPaperName() : (paper == null ? null : paper.getPaperName()); for (TmsPersonTaskEntity pt : createdPersons) { TmsExamEntity exam = new TmsExamEntity(); exam.setId(RandomUtil.uuId()); exam.setTaskId(entity.getId()); exam.setPersonTaskId(pt.getId()); exam.setRecordId(recordId); exam.setPaperId(entity.getPaperId()); exam.setPaperName(paperName); exam.setUserId(pt.getUserId()); exam.setAttemptNo(1); exam.setExamStatus(TmsExamStatus.NOT_STARTED.getCode()); exam.setPassScore(passScore); exam.setDurationMin(durationMin); exam.setCreatorUserId(UserProvider.getLoginUserId()); exam.setCreatorTime(now); examMapper.insert(exam); } } entity.setPublishStatus(TmsTaskPublishStatus.PUBLISHED.getCode()); entity.setPublishTime(now); entity.setLastModifyUserId(UserProvider.getLoginUserId()); entity.setLastModifyTime(now); taskMapper.updateById(entity); } @Override @Transactional(rollbackFor = Exception.class) @AuditLog(action = "TASK_CANCEL", label = "取消培训任务", bizType = "TMS_TASK", bizCodeExpr = "#id", targetTable = "tms_task", targetIdExpr = "#id") /** * 培训任务:取消 */ public void cancel(String id) { TmsTaskEntity entity = requireOne(id); if (!TmsTaskPublishStatus.PUBLISHED.equalsCode(entity.getPublishStatus())) { throw new DataException("仅已发布任务可取消"); } Long signed = personTaskMapper.selectCount( new LambdaQueryWrapper() .eq(TmsPersonTaskEntity::getTaskId, id) .and(w -> w.isNotNull(TmsPersonTaskEntity::getSignTime) .or().in(TmsPersonTaskEntity::getBizStatus, TmsPersonTaskStatus.SIGNED.getCode(), TmsPersonTaskStatus.DONE.getCode()))); if (signed != null && signed > 0) { throw new DataException("已有人员签到,不可取消"); } Date now = new Date(); entity.setPublishStatus(TmsTaskPublishStatus.CANCELLED.getCode()); entity.setLastModifyUserId(UserProvider.getLoginUserId()); entity.setLastModifyTime(now); taskMapper.updateById(entity); personTaskMapper.update(null, new LambdaUpdateWrapper() .eq(TmsPersonTaskEntity::getTaskId, id) .set(TmsPersonTaskEntity::getBizStatus, TmsPersonTaskStatus.CANCELLED.getCode()) .set(TmsPersonTaskEntity::getLastModifyUserId, UserProvider.getLoginUserId()) .set(TmsPersonTaskEntity::getLastModifyTime, now)); } @Override @Transactional(rollbackFor = Exception.class) @AuditLog(action = "TASK_CANCEL", label = "批量取消培训任务", bizType = "TMS_TASK", bizCodeExpr = "#ids != null && !#ids.isEmpty() ? #ids[0] : 'BATCH'", targetTable = "tms_task", targetIdExpr = "#ids != null && !#ids.isEmpty() ? #ids[0] : ''", targetIdsExpr = "#ids", bizCodeRequired = false) /** * 培训任务:批量取消 */ public void cancelBatch(List ids) { if (ids == null || ids.isEmpty()) { throw new DataException("请选择要取消的任务"); } for (String id : ids) { if (StringUtil.isNotEmpty(id)) { cancel(id); } } } @Override @Transactional(rollbackFor = Exception.class) @AuditLog(action = "TASK_RESTORE", label = "恢复培训任务", bizType = "TMS_TASK", bizCodeExpr = "#id", targetTable = "tms_task", targetIdExpr = "#id") /** * 培训任务:恢复 */ public void restore(String id) { TmsTaskEntity entity = requireOne(id); if (!TmsTaskPublishStatus.CANCELLED.equalsCode(entity.getPublishStatus())) { throw new DataException("仅已取消任务可恢复"); } Date now = new Date(); String userId = UserProvider.getLoginUserId(); // 口径:误取消仅恢复为草稿(不直接回 published)。清个人任务与记录壳,需重新编辑后发布。 List persons = personTaskMapper.selectList( new LambdaQueryWrapper().eq(TmsPersonTaskEntity::getTaskId, id)); for (TmsPersonTaskEntity p : persons) { personTaskMapper.deleteById(p.getId()); } List records = recordMapper.selectList( new LambdaQueryWrapper().eq(TmsRecordEntity::getTaskId, id)); for (TmsRecordEntity r : records) { recordMapper.deleteById(r.getId()); } entity.setPublishStatus(TmsTaskPublishStatus.DRAFT.getCode()); entity.setPublishTime(null); entity.setLastModifyUserId(userId); entity.setLastModifyTime(now); taskMapper.updateById(entity); // 清空发布时间字段(updateById 可能跳过 null) taskMapper.update(null, new LambdaUpdateWrapper() .eq(TmsTaskEntity::getId, id) .set(TmsTaskEntity::getPublishTime, null)); } /** * 年计划审批通过:每条明细生成一条草稿任务(source_type=annual)。 * 已存在 {@code source_item_id} 则跳过并回写明细 task_id(可重复调用)。 */ @Override @Transactional(rollbackFor = Exception.class) public TmsFromPlanResult createFromAnnual(String planId) { if (StringUtil.isEmpty(planId)) { throw new DataException("年计划ID不能为空"); } String id = planId.trim(); TmsAnnualPlanEntity plan = annualPlanMapper.selectById(id); if (plan == null) { throw new DataException("年度培训计划不存在: " + id); } List items = annualPlanItemMapper.selectList( new LambdaQueryWrapper() .eq(TmsAnnualPlanItemEntity::getForeignId, plan.getId()) .orderByAsc(TmsAnnualPlanItemEntity::getTrainMonth)); if (items.isEmpty()) { throw new DataException("年度计划没有明细,无法生成任务"); } TmsFromPlanResult result = new TmsFromPlanResult(); result.setPlanId(plan.getId()); Date now = new Date(); String userId = UserProvider.getLoginUserId(); for (TmsAnnualPlanItemEntity item : items) { TmsTaskEntity existed = findTaskBySourceItem(item.getId()); if (existed != null) { result.setSkipped(result.getSkipped() + 1); if (StringUtil.isEmpty(item.getTaskId())) { item.setTaskId(existed.getId()); item.setLineStatus(TmsPlanLineStatus.TASKED.getCode()); annualPlanItemMapper.updateById(item); } continue; } Date[] window = monthWindow(parseYear(plan.getPlanYear()), item.getTrainMonth()); TmsTaskEntity task = new TmsTaskEntity(); task.setId(RandomUtil.uuId()); task.setTaskNo(nextTaskNo(PREFIX_DRAFT)); task.setPublishStatus(TmsTaskPublishStatus.DRAFT.getCode()); task.setTaskKind(TmsTaskKind.NEW.getCode()); task.setCategory(TmsTaskCategory.ANNUAL_PLAN.getCode()); task.setSourceType(TmsTaskSourceType.ANNUAL.getCode()); task.setSourcePlanNo(plan.getPlanNo()); task.setSourceId(plan.getId()); task.setSourceItemId(item.getId()); String subject = StringUtil.isNotEmpty(item.getTrainSubject()) ? item.getTrainSubject().trim() : "年度培训"; task.setSubject(subject); task.setTrainMode(item.getTrainMode()); task.setEvalMode(item.getEvalMode()); task.setTeacherId(resolveTeacherUserId(item.getTeacherId())); task.setTrainees(normalizeTrainees(item.getTrainees())); if (window != null) { task.setStartTime(window[0]); task.setEndTime(window[1]); task.setCloseTime(window[1]); } task.setRetakeLimit(1); task.setOverdueRemind("0"); task.setInviteFlag("0"); task.setCreatorUserId(userId); task.setCreatorTime(now); fillRefs(task); taskMapper.insert(task); item.setTaskId(task.getId()); item.setLineStatus(TmsPlanLineStatus.TASKED.getCode()); annualPlanItemMapper.updateById(item); result.setCreated(result.getCreated() + 1); result.getTaskIds().add(task.getId()); } return result; } /** * 岗计划审批通过:每条明细生成草稿任务(source_type=post), * 并将主表 {@code publish_status} 置为 {@code published}(列表「发布情况」)。 * 培训对象取计划主表 trainees;幂等同 {@link #createFromAnnual}。 */ @Override @Transactional(rollbackFor = Exception.class) public TmsFromPlanResult createFromPost(String planId) { if (StringUtil.isEmpty(planId)) { throw new DataException("岗位培训计划ID不能为空"); } String id = planId.trim(); TmsPostPlanEntity plan = postPlanMapper.selectById(id); if (plan == null) { throw new DataException("岗位培训计划不存在: " + id); } List items = postPlanItemMapper.selectList( new LambdaQueryWrapper() .eq(TmsPostPlanItemEntity::getForeignId, plan.getId()) .orderByAsc(TmsPostPlanItemEntity::getPlanTime)); if (items.isEmpty()) { throw new DataException("岗位培训计划没有明细,无法生成任务"); } TmsFromPlanResult result = new TmsFromPlanResult(); result.setPlanId(plan.getId()); Date now = new Date(); String userId = UserProvider.getLoginUserId(); String trainees = normalizeTrainees(plan.getTrainees()); for (TmsPostPlanItemEntity item : items) { TmsTaskEntity existed = findTaskBySourceItem(item.getId()); if (existed != null) { result.setSkipped(result.getSkipped() + 1); if (StringUtil.isEmpty(item.getTaskId())) { item.setTaskId(existed.getId()); item.setLineStatus(TmsPlanLineStatus.TASKED.getCode()); postPlanItemMapper.updateById(item); } continue; } Date[] window = dayWindow(item.getPlanTime() != null ? item.getPlanTime() : plan.getPlanEndTime()); TmsTaskEntity task = new TmsTaskEntity(); task.setId(RandomUtil.uuId()); task.setTaskNo(nextTaskNo(PREFIX_DRAFT)); task.setPublishStatus(TmsTaskPublishStatus.DRAFT.getCode()); task.setTaskKind(TmsTaskKind.NEW.getCode()); task.setCategory(TmsTaskCategory.POST_PLAN.getCode()); task.setSourceType(TmsTaskSourceType.POST.getCode()); task.setSourcePlanNo(plan.getPlanNo()); task.setSourceId(plan.getId()); task.setSourceItemId(item.getId()); String subject = StringUtil.isNotEmpty(item.getTrainSubject()) ? item.getTrainSubject().trim() : null; if (StringUtil.isEmpty(subject)) { subject = StringUtil.isNotEmpty(plan.getPostName()) ? plan.getPostName() + "岗位培训" : "岗位培训"; } task.setSubject(subject); task.setTrainMode(item.getTrainMode()); task.setEvalMode(item.getEvalMode()); task.setTeacherId(resolveTeacherUserId(item.getTeacherId())); task.setTrainees(trainees); task.setMaterialId(item.getMaterialId()); if (window != null) { task.setStartTime(window[0]); task.setEndTime(window[1]); task.setCloseTime(window[1]); } task.setRetakeLimit(1); task.setOverdueRemind("0"); task.setInviteFlag("0"); task.setCreatorUserId(userId); task.setCreatorTime(now); fillRefs(task); taskMapper.insert(task); item.setTaskId(task.getId()); item.setLineStatus(TmsPlanLineStatus.TASKED.getCode()); postPlanItemMapper.updateById(item); result.setCreated(result.getCreated() + 1); result.getTaskIds().add(task.getId()); } // 列表「发布情况」读主表 改为已发布 if (!"published".equalsIgnoreCase(plan.getPublishStatus())) { plan.setPublishStatus("published"); postPlanMapper.updateById(plan); } return result; } /** * 按来源计划明细查找已生成任务 */ private TmsTaskEntity findTaskBySourceItem(String itemId) { if (StringUtil.isEmpty(itemId)) { return null; } return taskMapper.selectOne(new LambdaQueryWrapper() .eq(TmsTaskEntity::getSourceItemId, itemId) .last("limit 1")); } /** * 年份:2026、2026-01-01…、或 13 位毫秒时间戳。 * 在线表单日期控件会把年份存成日期字符串。 */ private static Integer parseYear(String raw) { if (StringUtil.isEmpty(raw)) { return null; } String text = raw.trim(); try { if (text.matches("\\d{4}")) { return Integer.parseInt(text); } if (text.matches("\\d{13}")) { return java.time.Instant.ofEpochMilli(Long.parseLong(text)) .atZone(ZoneId.systemDefault()).getYear(); } if (text.length() >= 4 && text.substring(0, 4).matches("\\d{4}")) { return Integer.parseInt(text.substring(0, 4)); } } catch (Exception ignored) { return null; } return null; } /** 月份占位:精确时间在任务「执行安排」再改。支持 yyyyMM、1-12、yyyy-MM-dd、13 位毫秒时间戳。 */ private static Date[] monthWindow(Integer planYear, String trainMonth) { if (StringUtil.isEmpty(trainMonth)) { return null; } String raw = trainMonth.trim(); int year; int month; try { if (raw.matches("\\d{13}")) { java.time.LocalDate day = java.time.Instant.ofEpochMilli(Long.parseLong(raw)) .atZone(ZoneId.systemDefault()).toLocalDate(); year = day.getYear(); month = day.getMonthValue(); } else if (raw.matches("\\d{4}-\\d{2}-\\d{2}.*")) { year = Integer.parseInt(raw.substring(0, 4)); month = Integer.parseInt(raw.substring(5, 7)); } else if (raw.matches("\\d{6}")) { year = Integer.parseInt(raw.substring(0, 4)); month = Integer.parseInt(raw.substring(4, 6)); } else if (raw.matches("\\d{1,2}")) { if (planYear == null) { return null; } year = planYear; month = Integer.parseInt(raw); } else { return null; } if (month < 1 || month > 12) { return null; } YearMonth ym = YearMonth.of(year, month); ZoneId zone = ZoneId.systemDefault(); Date start = Date.from(ym.atDay(1).atStartOfDay(zone).toInstant()); Date end = Date.from(ym.atEndOfMonth().atTime(23, 59, 59).atZone(zone).toInstant()); return new Date[]{start, end}; } catch (Exception ex) { return null; } } /** 岗位计划明细是具体日期;没有明细日期时用计划结束日。 */ private static Date[] dayWindow(Date day) { if (day == null) { return null; } try { java.time.LocalDate date = day.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); ZoneId zone = ZoneId.systemDefault(); Date start = Date.from(date.atStartOfDay(zone).toInstant()); Date end = Date.from(date.atTime(23, 59, 59).atZone(zone).toInstant()); return new Date[]{start, end}; } catch (Exception ex) { return null; } } /** * 规范化学员 ID 字符串 */ private static String normalizeTrainees(String raw) { if (StringUtil.isEmpty(raw)) { return null; } String text = raw.trim(); if (text.startsWith("[") && text.endsWith("]")) { text = text.substring(1, text.length() - 1).replace("\"", ""); } List ids = splitTrainees(text); return ids.isEmpty() ? text : joinTrainees(ids); } /** * 校验表单 */ private void validateForm(TmsTaskForm form, boolean creating) { if (form == null) { throw new DataException("表单不能为空"); } if (StringUtil.isEmpty(form.getSubject())) { throw new DataException("培训主题不能为空"); } if (StringUtil.isEmpty(form.getCategory())) { throw new DataException("培训分类不能为空"); } if (StringUtil.isEmpty(form.getTrainMode())) { throw new DataException("培训方式不能为空"); } if (StringUtil.isEmpty(form.getSourceType())) { throw new DataException("来源类型不能为空"); } if (StringUtil.isEmpty(form.getEvalMode())) { throw new DataException("考核方式不能为空"); } Date start = parseDate(form.getStartTime()); Date end = parseDate(form.getEndTime()); Date close = parseDate(form.getCloseTime()); if (start == null || end == null || close == null) { throw new DataException("开始/结束/关闭时间不能为空"); } if (start.after(end)) { throw new DataException("开始时间不能晚于结束时间"); } if (end.after(close)) { throw new DataException("结束时间不能晚于关闭时间"); } if (creating && start.before(new Date())) { // 允许当天稍早几分钟;严格:开始不得早于当前日 00:00 Calendar cal = Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, 0); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); cal.set(Calendar.MILLISECOND, 0); if (start.before(cal.getTime())) { throw new DataException("开始时间不得早于当天"); } } String mode = form.getTrainMode(); TrainModeRules rules = resolveTrainModeRules(mode); if (rules.sameDayRequired) { if (!sameDay(start, end)) { throw new DataException("该培训方式的开始与结束须为同一天"); } } if (rules.placeRequired) { if (StringUtil.isEmpty(form.getPlaceId()) && StringUtil.isEmpty(form.getPlaceName())) { throw new DataException("该培训方式须填写地点"); } } if ("online".equals(mode) && splitTrainees(form.getMaterialId()).isEmpty()) { throw new DataException("在线学习须选择教材"); } if (TmsEvalMode.EXAM.equalsCode(form.getEvalMode())) { if (StringUtil.isEmpty(form.getPaperId())) { throw new DataException("在线考试须选择试卷"); } Date examStart = parseDate(form.getExamStart()); Date examEnd = parseDate(form.getExamEnd()); if (examStart != null && examStart.before(start)) { throw new DataException("考试开始不得早于培训开始"); } if (examStart != null && examEnd != null && examStart.after(examEnd)) { throw new DataException("考试开始不能晚于考试结束"); } } if (TmsEvalMode.QUIZ.equalsCode(form.getEvalMode())) { int quizSize = countValidQuizzes(form.getQuizzes()); if (quizSize <= 0 && StringUtil.isNotEmpty(form.getId())) { quizSize = countTaskQuizzes(form.getId()); } if (quizSize <= 0) { throw new DataException("提问考核须至少配置一条提问预设"); } Integer ratio = form.getQuizRatio(); if (ratio != null && (ratio < 0 || ratio > 100)) { throw new DataException("提问占比须在 0~100 之间"); } } List trainees = resolveTraineeIds(form); if (trainees.isEmpty()) { throw new DataException("培训对象不能为空"); } Integer retake = form.getRetakeLimit(); if (retake != null && retake < 0) { throw new DataException("补考次数不能为负数"); } assertNoScheduleConflicts(form.getId(), form.getPlaceId(), form.getTeacherId(), start, end, mode, rules); } /** 发布前校验:对象、培训师、方式相关必填(地点/教材/试卷等)。 */ private void validateForPublish(TmsTaskEntity entity) { if (StringUtil.isEmpty(entity.getSubject()) || StringUtil.isEmpty(entity.getTrainMode()) || StringUtil.isEmpty(entity.getEvalMode()) || entity.getStartTime() == null || entity.getEndTime() == null || entity.getCloseTime() == null) { throw new DataException("任务信息不完整,请先完善后发布"); } if (splitTrainees(entity.getTrainees()).isEmpty()) { throw new DataException("培训对象不能为空"); } TrainModeRules rules = resolveTrainModeRules(entity.getTrainMode()); if (rules.sameDayRequired && !sameDay(entity.getStartTime(), entity.getEndTime())) { throw new DataException("该培训方式的开始与结束须为同一天"); } if (rules.placeRequired && StringUtil.isEmpty(entity.getPlaceId()) && StringUtil.isEmpty(entity.getPlaceName())) { throw new DataException("该培训方式须填写地点"); } if (TmsEvalMode.EXAM.equalsCode(entity.getEvalMode())) { if (StringUtil.isEmpty(entity.getPaperId())) { throw new DataException("在线考试须选择试卷"); } TmsPaperEntity paper = paperMapper.selectById(entity.getPaperId()); if (paper == null) { throw new DataException("所选试卷不存在"); } if (TmsOpenClosedInvalid.INVALID.equalsCode(paper.getBizStatus())) { throw new DataException("失效试卷不可用于发布"); } } if (TmsEvalMode.QUIZ.equalsCode(entity.getEvalMode()) && countTaskQuizzes(entity.getId()) <= 0) { throw new DataException("提问考核须至少配置一条提问预设"); } assertNoScheduleConflicts(entity.getId(), entity.getPlaceId(), entity.getTeacherId(), entity.getStartTime(), entity.getEndTime(), entity.getTrainMode(), rules); } /** * 解析培训方式规则 */ private TrainModeRules resolveTrainModeRules(String modeCode) { TrainModeRules rules = new TrainModeRules(); boolean fallbackOnsite = TmsTrainMode.ONSITE.equalsCode(modeCode) || TmsTrainMode.PRACTICE.equalsCode(modeCode); if (StringUtil.isEmpty(modeCode)) { rules.sameDayRequired = fallbackOnsite; rules.placeRequired = fallbackOnsite; return rules; } TmsTrainModeEntity cfg = trainModeMapper.selectOne( new LambdaQueryWrapper() .eq(TmsTrainModeEntity::getModeCode, modeCode) .last("limit 1")); if (cfg == null) { rules.sameDayRequired = fallbackOnsite; rules.placeRequired = fallbackOnsite; return rules; } rules.sameDayRequired = "1".equals(cfg.getSameDayRequired()); rules.placeRequired = "1".equals(cfg.getPlaceRequired()); return rules; } /** 联调暂时关闭地点/培训师时段冲突校验;恢复时改为 true。 */ private static final boolean SCHEDULE_CONFLICT_CHECK_ENABLED = false; /** * 地点 / 培训师时段冲突:[start, end) 半开区间重叠判定 start1 < end2 && start2 < end1。 * 培训师冲突只与「占用档期」的任务比对(现场/实操/须同日),不与在线学习长窗口互斥。 */ private void assertNoScheduleConflicts(String excludeTaskId, String placeId, String teacherId, Date start, Date end, String trainMode, TrainModeRules rules) { if (!SCHEDULE_CONFLICT_CHECK_ENABLED) { return; } if (start == null || end == null) { return; } if (StringUtil.isNotEmpty(placeId)) { LambdaQueryWrapper placeQw = new LambdaQueryWrapper() .eq(TmsTaskEntity::getPlaceId, placeId) .ne(TmsTaskEntity::getPublishStatus, TmsTaskPublishStatus.CANCELLED.getCode()) .lt(TmsTaskEntity::getStartTime, end) .gt(TmsTaskEntity::getEndTime, start); if (StringUtil.isNotEmpty(excludeTaskId)) { placeQw.ne(TmsTaskEntity::getId, excludeTaskId); } List placeHits = taskMapper.selectList(placeQw.last("limit 1")); if (placeHits != null && !placeHits.isEmpty()) { TmsTaskEntity hit = placeHits.get(0); throw new DataException("地点时段与任务 " + displayTaskNo(hit) + " 冲突"); } } boolean checkTrainer = StringUtil.isNotEmpty(teacherId) && occupiesTrainerSchedule(trainMode, rules); if (checkTrainer) { LambdaQueryWrapper teacherQw = new LambdaQueryWrapper() .eq(TmsTaskEntity::getTeacherId, teacherId) .ne(TmsTaskEntity::getPublishStatus, TmsTaskPublishStatus.CANCELLED.getCode()) .and(w -> w.in(TmsTaskEntity::getTrainMode, "onsite", "practice") .or().inSql(TmsTaskEntity::getTrainMode, "SELECT mode_code FROM tms_train_mode WHERE same_day_required = '1'" + " AND COALESCE(f_delete_mark, 0) = 0")) .lt(TmsTaskEntity::getStartTime, end) .gt(TmsTaskEntity::getEndTime, start); if (StringUtil.isNotEmpty(excludeTaskId)) { teacherQw.ne(TmsTaskEntity::getId, excludeTaskId); } List teacherHits = taskMapper.selectList(teacherQw.last("limit 1")); if (teacherHits != null && !teacherHits.isEmpty()) { TmsTaskEntity hit = teacherHits.get(0); throw new DataException("培训师时段与任务 " + displayTaskNo(hit) + " 冲突"); } } } /** 现场/实操,或培训方式配置「须同日」——视为占用培训师档期。在线学习长窗口不算。 */ private static boolean occupiesTrainerSchedule(String trainMode, TrainModeRules rules) { return TmsTrainMode.ONSITE.equalsCode(trainMode) || TmsTrainMode.PRACTICE.equalsCode(trainMode) || (rules != null && rules.sameDayRequired); } /** * 获取展示用任务编号 */ private static String displayTaskNo(TmsTaskEntity hit) { return hit == null || StringUtil.isEmpty(hit.getTaskNo()) ? "(未知编号)" : hit.getTaskNo(); } private static final class TrainModeRules { private boolean sameDayRequired; private boolean placeRequired; } /** * 表单写入实体字段 */ private void fillEntity(TmsTaskEntity entity, TmsTaskForm form, boolean keepPlanContent) { if (!keepPlanContent) { entity.setCategory(form.getCategory()); entity.setTrainType(form.getTrainType()); entity.setSourceType(form.getSourceType()); entity.setSourcePlanNo(form.getSourcePlanNo()); entity.setSourceId(form.getSourceId()); entity.setSourceItemId(form.getSourceItemId()); entity.setSubject(form.getSubject() == null ? null : form.getSubject().trim()); entity.setKeyPoints(form.getKeyPoints()); entity.setMaterialId(firstId(form.getMaterialId())); entity.setMaterialNo(firstId(form.getMaterialNo())); entity.setTeacherId(resolveTeacherUserId(form.getTeacherId())); entity.setTrainMode(form.getTrainMode()); entity.setEvalMode(form.getEvalMode()); entity.setTrainees(joinTrainees(resolveTraineeIds(form))); if (StringUtil.isNotEmpty(form.getTaskKind())) { entity.setTaskKind(form.getTaskKind()); } entity.setPrevTaskNo(form.getPrevTaskNo()); } // 执行信息:计划/临时均可维护 entity.setPlaceId(form.getPlaceId()); entity.setPlaceNo(form.getPlaceNo()); entity.setPlaceName(form.getPlaceName()); entity.setStartTime(parseDate(form.getStartTime())); entity.setEndTime(parseDate(form.getEndTime())); entity.setCloseTime(parseDate(form.getCloseTime())); entity.setPaperId(form.getPaperId()); entity.setPaperName(form.getPaperName()); entity.setExamStart(parseDate(form.getExamStart())); entity.setExamEnd(parseDate(form.getExamEnd())); entity.setRetakeLimit(form.getRetakeLimit() == null ? 1 : form.getRetakeLimit()); entity.setPassScore(form.getPassScore()); entity.setQuizRatio(form.getQuizRatio()); entity.setOverdueRemind(StringUtil.isEmpty(form.getOverdueRemind()) ? "0" : form.getOverdueRemind()); entity.setInviteFlag(StringUtil.isEmpty(form.getInviteFlag()) ? "0" : form.getInviteFlag()); entity.setRemark(form.getRemark()); } /** * 是否由计划生成 */ private static boolean isPlanSourced(TmsTaskEntity entity) { if (StringUtil.isNotEmpty(entity.getSourceId()) || StringUtil.isNotEmpty(entity.getSourceItemId()) || StringUtil.isNotEmpty(entity.getSourcePlanNo())) { return true; } String st = entity.getSourceType(); return StringUtil.isNotEmpty(st) && !TmsTaskSourceType.TEMP.equalsCode(st); } /** * 写入默认字段 */ private void applyDefaults(TmsTaskEntity entity) { if (entity.getRetakeLimit() == null) { entity.setRetakeLimit(1); } if (StringUtil.isEmpty(entity.getOverdueRemind())) { entity.setOverdueRemind("0"); } if (StringUtil.isEmpty(entity.getInviteFlag())) { entity.setInviteFlag("0"); } if (StringUtil.isEmpty(entity.getTaskKind())) { entity.setTaskKind(TmsTaskKind.NEW.getCode()); } } /** * 回填关联名称等冗余字段 */ private void fillRefs(TmsTaskEntity entity) { if (StringUtil.isNotEmpty(entity.getPaperId())) { TmsPaperEntity paper = paperMapper.selectById(entity.getPaperId()); if (paper != null) { entity.setPaperName(paper.getPaperName()); if (entity.getPassScore() == null) { entity.setPassScore(paper.getPassScore()); } } } else { entity.setPaperName(null); } if (StringUtil.isNotEmpty(entity.getMaterialId())) { TmsMaterialEntity material = materialMapper.selectById(entity.getMaterialId()); if (material != null && StringUtil.isNotEmpty(material.getMaterialNo())) { entity.setMaterialNo(material.getMaterialNo()); } } else { entity.setMaterialNo(null); } if (StringUtil.isNotEmpty(entity.getPlaceId())) { TmsPlaceEntity place = placeMapper.selectById(entity.getPlaceId()); if (place != null) { entity.setPlaceNo(place.getPlaceNo()); entity.setPlaceName(place.getPlaceName()); } } } /** * 保存任务附件 */ private void saveTaskFiles(String taskId, List files) { taskFileMapper.delete(new LambdaQueryWrapper() .eq(TmsTaskFileEntity::getForeignId, taskId)); if (files == null || files.isEmpty()) { return; } Date now = new Date(); String userId = UserProvider.getLoginUserId(); for (TmsTaskFileForm row : files) { if (row == null || StringUtil.isEmpty(row.getFileName())) { continue; } TmsTaskFileEntity entity = new TmsTaskFileEntity(); entity.setId(RandomUtil.uuId()); entity.setForeignId(taskId); entity.setFileNo(row.getFileNo()); entity.setFileName(row.getFileName().trim()); entity.setMaterialId(row.getMaterialId()); entity.setFileJson(row.getFileJson()); entity.setCanDownload(StringUtil.isEmpty(row.getCanDownload()) ? "0" : row.getCanDownload()); entity.setCreatorUserId(userId); entity.setCreatorTime(now); taskFileMapper.insert(entity); } } /** * 查询任务附件列表 */ private List listTaskFiles(String taskId) { List rows = taskFileMapper.selectList( new LambdaQueryWrapper() .eq(TmsTaskFileEntity::getForeignId, taskId) .orderByAsc(TmsTaskFileEntity::getCreatorTime)); List list = new ArrayList<>(rows.size()); for (TmsTaskFileEntity row : rows) { TmsTaskFileForm form = new TmsTaskFileForm(); form.setId(row.getId()); form.setFileNo(row.getFileNo()); form.setFileName(row.getFileName()); form.setMaterialId(row.getMaterialId()); form.setFileJson(row.getFileJson()); form.setCanDownload(row.getCanDownload()); list.add(form); } return list; } private void saveTaskQuizzes(String taskId, List quizzes) { taskQuizMapper.delete(new LambdaQueryWrapper() .eq(TmsTaskQuizEntity::getForeignId, taskId)); if (quizzes == null || quizzes.isEmpty()) { return; } Date now = new Date(); String userId = UserProvider.getLoginUserId(); int sort = 1; for (TmsTaskQuizForm row : quizzes) { if (row == null || StringUtil.isEmpty(row.getQuestion())) { continue; } TmsTaskQuizEntity entity = new TmsTaskQuizEntity(); entity.setId(RandomUtil.uuId()); entity.setForeignId(taskId); entity.setSortNo(row.getSortNo() != null ? row.getSortNo() : sort); entity.setQuestion(row.getQuestion().trim()); entity.setAnswer(row.getAnswer() == null ? "" : row.getAnswer().trim()); entity.setCreatorUserId(userId); entity.setCreatorTime(now); taskQuizMapper.insert(entity); sort++; } } private List listTaskQuizzes(String taskId) { List rows = taskQuizMapper.selectList( new LambdaQueryWrapper() .eq(TmsTaskQuizEntity::getForeignId, taskId) .orderByAsc(TmsTaskQuizEntity::getSortNo) .orderByAsc(TmsTaskQuizEntity::getCreatorTime)); List list = new ArrayList<>(rows.size()); for (TmsTaskQuizEntity row : rows) { TmsTaskQuizForm form = new TmsTaskQuizForm(); form.setId(row.getId()); form.setSortNo(row.getSortNo()); form.setQuestion(row.getQuestion()); form.setAnswer(row.getAnswer()); list.add(form); } return list; } private int countValidQuizzes(List quizzes) { if (quizzes == null || quizzes.isEmpty()) { return 0; } int n = 0; for (TmsTaskQuizForm q : quizzes) { if (q != null && StringUtil.isNotEmpty(q.getQuestion())) { n++; } } return n; } private int countTaskQuizzes(String taskId) { if (StringUtil.isEmpty(taskId)) { return 0; } Long cnt = taskQuizMapper.selectCount(new LambdaQueryWrapper() .eq(TmsTaskQuizEntity::getForeignId, taskId)); return cnt == null ? 0 : cnt.intValue(); } /** * 取逗号分隔的第一个 ID */ private static String firstId(String raw) { List ids = splitTrainees(raw); return ids.isEmpty() ? null : ids.get(0); } /** * 生成下一任务编号 */ private synchronized String nextTaskNo(String typePrefix) { String ym = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM")); String prefix = typePrefix + ym; String max = taskMapper.selectMaxTaskNo(prefix); int seq = 1; if (StringUtil.isNotEmpty(max) && max.length() == 12) { try { seq = Integer.parseInt(max.substring(8)) + 1; } catch (NumberFormatException ignored) { seq = 1; } } if (seq > 9999) { throw new DataException("当月任务编号已用尽"); } return prefix + String.format("%04d", seq); } /** * 解析讲师用户 ID */ private String resolveTeacherUserId(String teacherRef) { if (StringUtil.isEmpty(teacherRef)) { return null; } String raw = teacherRef.trim(); TmsTrainerEntity trainer = trainerMapper.selectById(raw); if (trainer != null && StringUtil.isNotEmpty(trainer.getUserId())) { return trainer.getUserId(); } // 已是系统用户,或档案尚未关联用户 return raw; } /** * 按 ID 查询,不存在则抛错 */ private TmsTaskEntity requireOne(String id) { if (StringUtil.isEmpty(id)) { throw new DataException("任务ID不能为空"); } TmsTaskEntity entity = taskMapper.selectById(id); if (entity == null) { throw new DataException("培训任务不存在"); } return entity; } /** * 实体转为表单/VO */ private TmsTaskForm toForm(TmsTaskEntity entity, boolean detail) { TmsTaskForm form = new TmsTaskForm(); form.setId(entity.getId()); form.setTaskNo(entity.getTaskNo()); form.setCategory(entity.getCategory()); form.setTrainType(entity.getTrainType()); form.setSourceType(entity.getSourceType()); form.setSourcePlanNo(entity.getSourcePlanNo()); form.setSourceId(entity.getSourceId()); form.setSourceItemId(entity.getSourceItemId()); form.setSubject(entity.getSubject()); form.setKeyPoints(entity.getKeyPoints()); form.setMaterialId(entity.getMaterialId()); form.setMaterialNo(entity.getMaterialNo()); form.setPlaceId(entity.getPlaceId()); form.setPlaceNo(entity.getPlaceNo()); form.setPlaceName(entity.getPlaceName()); form.setStartTime(formatDate(entity.getStartTime())); form.setEndTime(formatDate(entity.getEndTime())); form.setCloseTime(formatDate(entity.getCloseTime())); form.setPaperId(entity.getPaperId()); form.setPaperName(entity.getPaperName()); form.setExamStart(formatDate(entity.getExamStart())); form.setExamEnd(formatDate(entity.getExamEnd())); form.setTeacherId(resolveTeacherUserId(entity.getTeacherId())); form.setTrainMode(entity.getTrainMode()); form.setEvalMode(entity.getEvalMode()); form.setTrainees(entity.getTrainees()); form.setTraineeIds(splitTrainees(entity.getTrainees())); form.setTaskKind(entity.getTaskKind()); form.setPrevTaskNo(entity.getPrevTaskNo()); form.setPublishStatus(entity.getPublishStatus()); form.setPublishTime(formatDate(entity.getPublishTime())); form.setFlowId(entity.getFlowId()); form.setFlowTaskId(entity.getFlowTaskId()); form.setFlowState(entity.getFlowState()); form.setRetakeLimit(entity.getRetakeLimit()); form.setPassScore(entity.getPassScore()); form.setQuizRatio(entity.getQuizRatio()); form.setOverdueRemind(entity.getOverdueRemind()); form.setInviteFlag(entity.getInviteFlag()); form.setRemark(entity.getRemark()); form.setCreatorTime(formatDate(entity.getCreatorTime())); form.setCreatorUserId(entity.getCreatorUserId()); if (detail) { // person tasks filled by caller } return form; } /** * 解析学员 ID 列表 */ private static List resolveTraineeIds(TmsTaskForm form) { Set set = new LinkedHashSet<>(); if (form.getTraineeIds() != null) { for (String id : form.getTraineeIds()) { if (StringUtil.isNotEmpty(id)) { set.add(id.trim()); } } } if (set.isEmpty() && StringUtil.isNotEmpty(form.getTrainees())) { set.addAll(splitTrainees(form.getTrainees())); } return new ArrayList<>(set); } /** * 拆分学员 ID 字符串 */ private static List splitTrainees(String raw) { if (StringUtil.isEmpty(raw)) { return new ArrayList<>(); } return Arrays.stream(raw.split("[,;,;\\s]+")) .map(String::trim) .filter(s -> !s.isEmpty()) .distinct() .collect(Collectors.toList()); } /** * 拼接学员 ID 列表 */ private static String joinTrainees(List ids) { if (ids == null || ids.isEmpty()) { return null; } return ids.stream().filter(Objects::nonNull).map(String::trim).filter(s -> !s.isEmpty()) .distinct().collect(Collectors.joining(",")); } /** * 是否同一自然日 */ private static boolean sameDay(Date a, Date b) { LocalDate da = a.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); LocalDate db = b.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); return da.equals(db); } /** * 解析日期时间 */ private static Date parseDate(String text) { if (StringUtil.isEmpty(text)) { return null; } String raw = text.trim(); try { if (raw.matches("^\\d{10,13}$")) { long ts = Long.parseLong(raw); if (raw.length() == 10) { ts = ts * 1000L; } return new Date(ts); } return DateUtil.stringToDate(raw); } catch (Exception ex) { throw new DataException("时间格式无效:" + text); } } /** * 解析日开始时间 */ private static Date parseDayStart(String day) { if (StringUtil.isEmpty(day) || "null".equalsIgnoreCase(day)) { return null; } String raw = day.trim(); if (raw.length() == 10) { raw = raw + " 00:00:00"; } return parseDate(raw); } /** * 解析日结束时间 */ private static Date parseDayEnd(String day) { if (StringUtil.isEmpty(day) || "null".equalsIgnoreCase(day)) { return null; } String raw = day.trim(); if (raw.length() == 10) { raw = raw + " 23:59:59"; } return parseDate(raw); } /** * 解析任务学时 */ private BigDecimal resolveTaskHours(TmsTaskEntity task) { if (task == null || StringUtil.isEmpty(task.getSourceItemId())) { return null; } if (TmsTaskSourceType.ANNUAL.equalsCode(task.getSourceType())) { TmsAnnualPlanItemEntity item = annualPlanItemMapper.selectById(task.getSourceItemId()); return item == null ? null : item.getHours(); } if (TmsTaskSourceType.POST.equalsCode(task.getSourceType())) { TmsPostPlanItemEntity item = postPlanItemMapper.selectById(task.getSourceItemId()); return item == null ? null : item.getHours(); } return null; } /** 发布时把任务教材附件快照写入记录 lecture_file */ /** 将任务关联教材/附件快照到培训记录讲义字段,归档后仍可追溯。 */ private void syncLectureFileToRecord(String recordId, String taskId) { if (StringUtil.isEmpty(recordId) || StringUtil.isEmpty(taskId)) { return; } List files = taskFileMapper.selectList( new LambdaQueryWrapper() .eq(TmsTaskFileEntity::getForeignId, taskId) .orderByAsc(TmsTaskFileEntity::getCreatorTime)); List> snaps = new ArrayList<>(); for (TmsTaskFileEntity f : files) { if (f == null) { continue; } Map item = new LinkedHashMap<>(); item.put("id", f.getId()); item.put("name", f.getFileName()); item.put("fileNo", f.getFileNo()); item.put("materialId", f.getMaterialId()); String fileJson = f.getFileJson(); // 任务附件只挂了教材、未落 fileJson 时,快照补上教材文件,详情才能预览 if (StringUtil.isEmpty(fileJson) && StringUtil.isNotEmpty(f.getMaterialId())) { TmsMaterialEntity m = materialMapper.selectById(f.getMaterialId()); if (m != null && StringUtil.isNotEmpty(m.getFileJson())) { fileJson = m.getFileJson(); if (StringUtil.isEmpty(f.getFileName())) { item.put("name", m.getMaterialName()); } } } item.put("fileJson", fileJson); item.put("canDownload", f.getCanDownload()); snaps.add(item); } // 无任务附件时,尝试把任务主教材写入快照 if (snaps.isEmpty()) { TmsTaskEntity task = taskMapper.selectById(taskId); if (task != null && StringUtil.isNotEmpty(task.getMaterialId())) { for (String mid : task.getMaterialId().split(",")) { if (StringUtil.isEmpty(mid)) { continue; } TmsMaterialEntity m = materialMapper.selectById(mid.trim()); if (m == null) { continue; } Map item = new LinkedHashMap<>(); item.put("id", m.getId()); item.put("name", m.getMaterialName()); item.put("fileNo", m.getMaterialNo()); item.put("materialId", m.getId()); item.put("fileJson", m.getFileJson()); snaps.add(item); } } } String json = snaps.isEmpty() ? null : JsonUtil.getObjectToString(snaps); recordMapper.update(null, new LambdaUpdateWrapper() .eq(TmsRecordEntity::getId, recordId) .set(TmsRecordEntity::getLectureFile, json) .set(TmsRecordEntity::getLastModifyUserId, UserProvider.getLoginUserId()) .set(TmsRecordEntity::getLastModifyTime, new Date())); } /** * 格式化日期时间 */ private static String formatDate(Date date) { if (date == null) { return null; } return DateUtil.dateToString(date, FMT); } }