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.exception.DataException; import jnpf.tmsEntity.TmsAnnualPlanEntity; import jnpf.tmsEntity.TmsAnnualPlanItemEntity; 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.TmsTrainerEntity; import jnpf.tmsEntity.task.TmsFromPlanResult; import jnpf.tmsEntity.task.TmsPersonTaskForm; import jnpf.tmsEntity.task.TmsTaskFileForm; import jnpf.tmsEntity.task.TmsTaskForm; import jnpf.tmsEntity.task.TmsTaskQuery; import jnpf.tmsMapper.TmsAnnualPlanItemMapper; import jnpf.tmsMapper.TmsAnnualPlanMapper; 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.TmsTaskMapper; import jnpf.tmsMapper.TmsTrainerMapper; import jnpf.tmsService.TmsTaskService; import jnpf.util.DateUtil; 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.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class TmsTaskServiceImpl implements TmsTaskService { private static final String STATUS_DRAFT = "draft"; private static final String STATUS_PUBLISHED = "published"; private static final String STATUS_CANCELLED = "cancelled"; private static final String PERSON_TODO = "todo"; private static final String PERSON_SIGNED = "signed"; private static final String PERSON_DONE = "done"; private static final String PERSON_CANCELLED = "cancelled"; 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 TmsMaterialMapper materialMapper; private final TmsTaskFileMapper taskFileMapper; private final TmsPlaceMapper placeMapper; 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)); 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("temp"); form.setCategory("temp"); } validateForm(form, true); TmsTaskEntity entity = new TmsTaskEntity(); entity.setId(RandomUtil.uuId()); entity.setTaskNo(nextTaskNo(PREFIX_DRAFT)); entity.setPublishStatus(STATUS_DRAFT); 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()); return entity.getId(); } @Override @Transactional(rollbackFor = Exception.class) public void update(String id, TmsTaskForm form) { TmsTaskEntity entity = requireOne(id); if (!STATUS_DRAFT.equals(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()); } } @Override @Transactional(rollbackFor = Exception.class) public void delete(String id) { TmsTaskEntity entity = requireOne(id); if (!STATUS_DRAFT.equals(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); } @Override @Transactional(rollbackFor = Exception.class) public void publish(String id) { TmsTaskEntity entity = requireOne(id); if (!STATUS_DRAFT.equals(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); 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(PERSON_TODO); pt.setGuestFlag("0"); pt.setCreatorUserId(UserProvider.getLoginUserId()); pt.setCreatorTime(now); personTaskMapper.insert(pt); } Long recordCnt = recordMapper.selectCount( new LambdaQueryWrapper().eq(TmsRecordEntity::getTaskId, entity.getId())); if (recordCnt == null || recordCnt == 0) { TmsRecordEntity record = new TmsRecordEntity(); record.setId(RandomUtil.uuId()); 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()); record.setArchiveStatus("pending"); record.setCreatorUserId(UserProvider.getLoginUserId()); record.setCreatorTime(now); recordMapper.insert(record); } entity.setPublishStatus(STATUS_PUBLISHED); entity.setPublishTime(now); entity.setLastModifyUserId(UserProvider.getLoginUserId()); entity.setLastModifyTime(now); taskMapper.updateById(entity); } @Override @Transactional(rollbackFor = Exception.class) public void cancel(String id) { TmsTaskEntity entity = requireOne(id); if (!STATUS_PUBLISHED.equals(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, PERSON_SIGNED, PERSON_DONE))); if (signed != null && signed > 0) { throw new DataException("已有人员签到,不可取消"); } Date now = new Date(); entity.setPublishStatus(STATUS_CANCELLED); entity.setLastModifyUserId(UserProvider.getLoginUserId()); entity.setLastModifyTime(now); taskMapper.updateById(entity); personTaskMapper.update(null, new LambdaUpdateWrapper() .eq(TmsPersonTaskEntity::getTaskId, id) .set(TmsPersonTaskEntity::getBizStatus, PERSON_CANCELLED) .set(TmsPersonTaskEntity::getLastModifyUserId, UserProvider.getLoginUserId()) .set(TmsPersonTaskEntity::getLastModifyTime, now)); } @Override @Transactional(rollbackFor = Exception.class) 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) public void restore(String id) { TmsTaskEntity entity = requireOne(id); if (!STATUS_CANCELLED.equals(entity.getPublishStatus())) { throw new DataException("仅已取消任务可恢复"); } Date now = new Date(); String userId = UserProvider.getLoginUserId(); // 逻辑删除已生成的个人任务与记录,便于再次编辑后发布 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(STATUS_DRAFT); 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)); } @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("tasked"); 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(STATUS_DRAFT); task.setTaskKind("new"); task.setCategory("annual_plan"); task.setSourceType("annual"); 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("tasked"); annualPlanItemMapper.updateById(item); result.setCreated(result.getCreated() + 1); result.getTaskIds().add(task.getId()); } return result; } @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("tasked"); 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(STATUS_DRAFT); task.setTaskKind("new"); task.setCategory("post_plan"); task.setSourceType("post"); task.setSourcePlanNo(plan.getPlanNo()); task.setSourceId(plan.getId()); task.setSourceItemId(item.getId()); String subject = StringUtil.isNotEmpty(item.getSubject()) ? item.getSubject().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("tasked"); postPlanItemMapper.updateById(item); result.setCreated(result.getCreated() + 1); result.getTaskIds().add(task.getId()); } 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; } } 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(); if ("onsite".equals(mode) || "practice".equals(mode)) { if (!sameDay(start, end)) { throw new DataException("集中/操作授课的开始与结束须为同一天"); } if (StringUtil.isEmpty(form.getPlaceId()) && StringUtil.isEmpty(form.getPlaceName())) { throw new DataException("现场类培训须填写地点"); } } if ("online".equals(mode) && splitTrainees(form.getMaterialId()).isEmpty()) { throw new DataException("在线学习须选择教材"); } if ("exam".equals(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("考试开始不能晚于考试结束"); } } List trainees = resolveTraineeIds(form); if (trainees.isEmpty()) { throw new DataException("培训对象不能为空"); } Integer retake = form.getRetakeLimit(); if (retake != null && retake < 0) { throw new DataException("补考次数不能为负数"); } } 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("培训对象不能为空"); } if ("exam".equals(entity.getEvalMode())) { if (StringUtil.isEmpty(entity.getPaperId())) { throw new DataException("在线考试须选择试卷"); } TmsPaperEntity paper = paperMapper.selectById(entity.getPaperId()); if (paper == null) { throw new DataException("所选试卷不存在"); } if ("invalid".equals(paper.getBizStatus())) { throw new DataException("失效试卷不可用于发布"); } } } 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(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) && !"temp".equals(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("new"); } } 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 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); } /** * 年计划子表 teacher_id 多为培训师档案 f_id;任务表单用 UserSelect,需落到系统用户。 */ 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; } 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; } 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.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; } 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); } 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()); } 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 ("annual".equals(task.getSourceType())) { TmsAnnualPlanItemEntity item = annualPlanItemMapper.selectById(task.getSourceItemId()); return item == null ? null : item.getHours(); } if ("post".equals(task.getSourceType())) { TmsPostPlanItemEntity item = postPlanItemMapper.selectById(task.getSourceItemId()); return item == null ? null : item.getHours(); } return null; } private static String formatDate(Date date) { if (date == null) { return null; } return DateUtil.dateToString(date, FMT); } }