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);
}
/**
* 发布流水线(顺序固定,勿拆事务):
*
* - 校验草稿 + 发布必填项
* - 正式编号 TT(若仍为 TD)
* - 按 trainees 插入个人任务 todo
* - 插入或复活培训记录 open
* - 讲义快照到记录;考试模式预生成待考壳
* - 任务置 published
*
*/
@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