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;
|
|
/**
|
* 培训任务核心实现。
|
*
|
* <p>编号约定:草稿 {@code TD…},发布后正式号 {@code TT…}。
|
* 计划生成幂等键:{@code source_item_id}(同一计划明细不重复建任务)。</p>
|
*
|
* @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<TmsTaskForm> getList(TmsTaskQuery query) {
|
LambdaQueryWrapper<TmsTaskEntity> 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<TmsTaskEntity> page = taskMapper.selectPage(new Page<>(current, size), qw);
|
|
List<TmsTaskForm> 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<TmsPersonTaskEntity> persons = personTaskMapper.selectList(
|
new LambdaQueryWrapper<TmsPersonTaskEntity>()
|
.eq(TmsPersonTaskEntity::getTaskId, id)
|
.orderByAsc(TmsPersonTaskEntity::getCreatorTime));
|
List<TmsPersonTaskForm> 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<TmsPersonTaskEntity>().eq(TmsPersonTaskEntity::getTaskId, id));
|
if (cnt != null && cnt > 0) {
|
throw new DataException("已生成个人任务,不可删除");
|
}
|
taskMapper.deleteById(id);
|
}
|
|
/**
|
* 发布流水线(顺序固定,勿拆事务):
|
* <ol>
|
* <li>校验草稿 + 发布必填项</li>
|
* <li>正式编号 TT(若仍为 TD)</li>
|
* <li>按 trainees 插入个人任务 todo</li>
|
* <li>插入或复活培训记录 open</li>
|
* <li>讲义快照到记录;考试模式预生成待考壳</li>
|
* <li>任务置 published</li>
|
* </ol>
|
*/
|
@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<String> userIds = splitTrainees(entity.getTrainees());
|
if (userIds.isEmpty()) {
|
throw new DataException("培训对象不能为空");
|
}
|
|
Date now = new Date();
|
BigDecimal hours = resolveTaskHours(entity);
|
List<TmsPersonTaskEntity> 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<TmsRecordEntity> existingRecords = recordMapper.selectList(
|
new LambdaQueryWrapper<TmsRecordEntity>().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<TmsRecordEntity>()
|
.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<TmsPersonTaskEntity>()
|
.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<TmsPersonTaskEntity>()
|
.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<String> 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<TmsPersonTaskEntity> persons = personTaskMapper.selectList(
|
new LambdaQueryWrapper<TmsPersonTaskEntity>().eq(TmsPersonTaskEntity::getTaskId, id));
|
for (TmsPersonTaskEntity p : persons) {
|
personTaskMapper.deleteById(p.getId());
|
}
|
List<TmsRecordEntity> records = recordMapper.selectList(
|
new LambdaQueryWrapper<TmsRecordEntity>().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<TmsTaskEntity>()
|
.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<TmsAnnualPlanItemEntity> items = annualPlanItemMapper.selectList(
|
new LambdaQueryWrapper<TmsAnnualPlanItemEntity>()
|
.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<TmsPostPlanItemEntity> items = postPlanItemMapper.selectList(
|
new LambdaQueryWrapper<TmsPostPlanItemEntity>()
|
.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<TmsTaskEntity>()
|
.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<String> 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<String> 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<TmsTrainModeEntity>()
|
.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<TmsTaskEntity> placeQw = new LambdaQueryWrapper<TmsTaskEntity>()
|
.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<TmsTaskEntity> 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<TmsTaskEntity> teacherQw = new LambdaQueryWrapper<TmsTaskEntity>()
|
.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<TmsTaskEntity> 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<TmsTaskFileForm> files) {
|
taskFileMapper.delete(new LambdaQueryWrapper<TmsTaskFileEntity>()
|
.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<TmsTaskFileForm> listTaskFiles(String taskId) {
|
List<TmsTaskFileEntity> rows = taskFileMapper.selectList(
|
new LambdaQueryWrapper<TmsTaskFileEntity>()
|
.eq(TmsTaskFileEntity::getForeignId, taskId)
|
.orderByAsc(TmsTaskFileEntity::getCreatorTime));
|
List<TmsTaskFileForm> 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<TmsTaskQuizForm> quizzes) {
|
taskQuizMapper.delete(new LambdaQueryWrapper<TmsTaskQuizEntity>()
|
.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<TmsTaskQuizForm> listTaskQuizzes(String taskId) {
|
List<TmsTaskQuizEntity> rows = taskQuizMapper.selectList(
|
new LambdaQueryWrapper<TmsTaskQuizEntity>()
|
.eq(TmsTaskQuizEntity::getForeignId, taskId)
|
.orderByAsc(TmsTaskQuizEntity::getSortNo)
|
.orderByAsc(TmsTaskQuizEntity::getCreatorTime));
|
List<TmsTaskQuizForm> 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<TmsTaskQuizForm> 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<TmsTaskQuizEntity>()
|
.eq(TmsTaskQuizEntity::getForeignId, taskId));
|
return cnt == null ? 0 : cnt.intValue();
|
}
|
|
/**
|
* 取逗号分隔的第一个 ID
|
*/
|
private static String firstId(String raw) {
|
List<String> 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<String> resolveTraineeIds(TmsTaskForm form) {
|
Set<String> 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<String> 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<String> 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<TmsTaskFileEntity> files = taskFileMapper.selectList(
|
new LambdaQueryWrapper<TmsTaskFileEntity>()
|
.eq(TmsTaskFileEntity::getForeignId, taskId)
|
.orderByAsc(TmsTaskFileEntity::getCreatorTime));
|
List<Map<String, Object>> snaps = new ArrayList<>();
|
for (TmsTaskFileEntity f : files) {
|
if (f == null) {
|
continue;
|
}
|
Map<String, Object> 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<String, Object> 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<TmsRecordEntity>()
|
.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);
|
}
|
}
|