package jnpf.tmsService.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import jnpf.audit.sdk.annotation.AuditLog;
import jnpf.exception.DataException;
import jnpf.permission.OrganizeApi;
import jnpf.permission.UserApi;
import jnpf.permission.entity.OrganizeEntity;
import jnpf.permission.entity.UserEntity;
import jnpf.tmsEntity.TmsAnnualPlanItemEntity;
import jnpf.tmsEntity.TmsMaterialEntity;
import jnpf.tmsEntity.TmsPersonFileEntity;
import jnpf.tmsEntity.TmsPersonTaskEntity;
import jnpf.tmsEntity.TmsPersonTrainEntity;
import jnpf.tmsEntity.TmsPostPlanItemEntity;
import jnpf.tmsEntity.TmsQuizScoreEntity;
import jnpf.tmsEntity.TmsRecordEntity;
import jnpf.tmsEntity.TmsRecordSignEntity;
import jnpf.tmsEntity.TmsSignModeEntity;
import jnpf.tmsEntity.TmsTaskEntity;
import jnpf.tmsEntity.TmsTaskFileEntity;
import jnpf.tmsEntity.TmsTaskQuizEntity;
import jnpf.tmsEntity.TmsTrainModeEntity;
import jnpf.tmsEntity.person.TmsPersonTaskSignResult;
import jnpf.tmsEntity.record.TmsQuizAnswerPaperVO;
import jnpf.tmsEntity.record.TmsRecordAttachmentForm;
import jnpf.tmsEntity.record.TmsRecordForm;
import jnpf.tmsEntity.record.TmsRecordIncompleteVO;
import jnpf.tmsEntity.record.TmsRecordParticipantForm;
import jnpf.tmsEntity.record.TmsRecordProgressVO;
import jnpf.tmsEntity.record.TmsRecordQuery;
import jnpf.tmsEntity.record.TmsRecordScanSignForm;
import jnpf.tmsEntity.record.TmsRecordSignForm;
import jnpf.tmsEntity.record.TmsRecordSignQrModeVO;
import jnpf.tmsEntity.record.TmsRecordSignQrVO;
import jnpf.tmsMapper.TmsAnnualPlanItemMapper;
import jnpf.tmsMapper.TmsMaterialMapper;
import jnpf.tmsMapper.TmsPersonFileMapper;
import jnpf.tmsMapper.TmsPersonTaskMapper;
import jnpf.tmsMapper.TmsPersonTrainMapper;
import jnpf.tmsMapper.TmsPostPlanItemMapper;
import jnpf.tmsMapper.TmsQuizScoreMapper;
import jnpf.tmsMapper.TmsRecordMapper;
import jnpf.tmsMapper.TmsRecordSignMapper;
import jnpf.tmsMapper.TmsSignModeMapper;
import jnpf.tmsMapper.TmsTaskFileMapper;
import jnpf.tmsMapper.TmsTaskMapper;
import jnpf.tmsMapper.TmsTaskQuizMapper;
import jnpf.tmsMapper.TmsTrainModeMapper;
import jnpf.tmsService.TmsPersonTaskService;
import jnpf.tmsService.TmsRecordService;
import jnpf.tmsEntity.enums.TmsPassFlag;
import jnpf.tmsEntity.enums.TmsTaskKind;
import jnpf.tmsEntity.enums.TmsTaskCategory;
import jnpf.tmsEntity.enums.TmsTaskPublishStatus;
import jnpf.tmsEntity.enums.TmsArchiveStatus;
import jnpf.tmsEntity.enums.TmsTaskSourceType;
import jnpf.tmsEntity.enums.TmsPlanLineStatus;
import jnpf.tmsEntity.enums.TmsTrainMode;
import jnpf.tmsEntity.enums.TmsSignRule;
import jnpf.tmsEntity.enums.TmsSignMode;
import jnpf.tmsEntity.enums.TmsYesNo;
import jnpf.tmsEntity.enums.TmsEvalMode;
import jnpf.tmsEntity.enums.TmsValidRevoked;
import jnpf.tmsUtil.TmsQuizHelper;
import jnpf.util.DateUtil;
import jnpf.util.RandomUtil;
import jnpf.util.StringUtil;
import jnpf.util.UserProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 培训记录:签到二维码、归档状态机、归档写个人培训目录。
*
*
归档状态({@link jnpf.tmsEntity.enums.TmsArchiveStatus}):
* open → pending(关闭时间到)→ ready(可选)→ archived;或 invalid。
* 归档成功后才写入 {@code tms_person_train}(个人档案「培训目录」);
* 学员学完/考过不会直接写目录。
*
* @see #archive(java.util.List)
* @see #archiveOne(jnpf.tmsEntity.TmsRecordEntity, java.util.Date, String)
*/
@Service
@RequiredArgsConstructor
public class TmsRecordServiceImpl implements TmsRecordService {
private static final String FMT = "yyyy-MM-dd HH:mm:ss";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final long SIGN_QR_TTL_MS = 5L * 60L * 1000L;
private static final String PREFIX_DRAFT = "TD";
/** 归档/作废产生的再培训草稿:category 固定 retrain(字典 tmsTaskCategory) */
private final TmsRecordMapper recordMapper;
private final TmsRecordSignMapper recordSignMapper;
private final TmsTaskMapper taskMapper;
private final TmsTaskFileMapper taskFileMapper;
private final TmsMaterialMapper materialMapper;
private final TmsPersonTaskMapper personTaskMapper;
private final TmsPersonTrainMapper personTrainMapper;
private final TmsPersonFileMapper personFileMapper;
private final TmsTrainModeMapper trainModeMapper;
private final TmsSignModeMapper signModeMapper;
private final TmsAnnualPlanItemMapper annualPlanItemMapper;
private final TmsPostPlanItemMapper postPlanItemMapper;
private final TmsPersonTaskService personTaskService;
private final TmsQuizScoreMapper quizScoreMapper;
private final TmsTaskQuizMapper taskQuizMapper;
private final UserApi userApi;
private final OrganizeApi organizeApi;
/**
* 培训记录:条件分页查询列表
*/
@Override
public List getList(TmsRecordQuery query) {
LambdaQueryWrapper qw = new LambdaQueryWrapper<>();
applyListMode(qw, query);
if (StringUtil.isNotEmpty(query.getRecordNo())) {
qw.like(TmsRecordEntity::getRecordNo, query.getRecordNo().trim());
}
if (StringUtil.isNotEmpty(query.getSubject())) {
qw.like(TmsRecordEntity::getSubject, query.getSubject().trim());
}
if (StringUtil.isNotEmpty(query.getCategory())) {
qw.eq(TmsRecordEntity::getCategory, query.getCategory().trim());
}
if (StringUtil.isNotEmpty(query.getTrainMode())) {
qw.eq(TmsRecordEntity::getTrainMode, query.getTrainMode().trim());
}
if (StringUtil.isNotEmpty(query.getEvalMode())) {
qw.eq(TmsRecordEntity::getEvalMode, query.getEvalMode().trim());
}
if (StringUtil.isNotEmpty(query.getKeyword()) && !"null".equalsIgnoreCase(query.getKeyword())) {
String kw = query.getKeyword().trim();
qw.and(w -> w.like(TmsRecordEntity::getRecordNo, kw)
.or().like(TmsRecordEntity::getSubject, kw));
}
Date from = parseDayStart(query.getStartTimeFrom());
Date to = parseDayEnd(query.getStartTimeTo());
if (from != null) {
qw.ge(TmsRecordEntity::getStartTime, from);
}
if (to != null) {
qw.le(TmsRecordEntity::getStartTime, to);
}
Date endFrom = parseDayStart(query.getEndTimeFrom());
Date endTo = parseDayEnd(query.getEndTimeTo());
if (endFrom != null) {
qw.ge(TmsRecordEntity::getEndTime, endFrom);
}
if (endTo != null) {
qw.le(TmsRecordEntity::getEndTime, endTo);
}
qw.orderByDesc(TmsRecordEntity::getStartTime).orderByDesc(TmsRecordEntity::getCreatorTime);
long current = query.getCurrentPage() > 0 ? query.getCurrentPage() : 1;
long size = query.getPageSize() > 0 ? query.getPageSize() : 20;
Page page = recordMapper.selectPage(new Page<>(current, size), qw);
List rows = page.getRecords();
Map names = loadUserNames(rows.stream()
.map(TmsRecordEntity::getTeacherId)
.filter(StringUtil::isNotEmpty)
.distinct()
.collect(Collectors.toList()));
List list = new ArrayList<>(rows.size());
String trainerFilter = StringUtil.isNotEmpty(query.getTrainerName()) ? query.getTrainerName().trim() : null;
for (TmsRecordEntity row : rows) {
TmsRecordForm form = toListForm(row, names);
if (trainerFilter != null) {
String name = form.getTrainerName() == null ? "" : form.getTrainerName();
if (!name.contains(trainerFilter)) {
continue;
}
}
list.add(form);
}
query.setData(list, page.getTotal());
return list;
}
/**
* 培训记录:按主键查询详情
*/
@Override
public TmsRecordForm getInfo(String id) {
TmsRecordEntity entity = requireOne(id);
Map teacherNames = loadUserNames(
StringUtil.isEmpty(entity.getTeacherId())
? Collections.emptyList()
: Collections.singletonList(entity.getTeacherId()));
TmsRecordForm form = toListForm(entity, teacherNames);
TmsTaskEntity task = StringUtil.isEmpty(entity.getTaskId()) ? null : taskMapper.selectById(entity.getTaskId());
if (task != null) {
form.setKeyPoints(task.getKeyPoints());
form.setPlaceName(task.getPlaceName());
if (StringUtil.isEmpty(form.getTrainType()) && StringUtil.isNotEmpty(task.getTrainType())) {
form.setTrainType(task.getTrainType());
}
if (StringUtil.isEmpty(form.getTrainMode()) && StringUtil.isNotEmpty(task.getTrainMode())) {
form.setTrainMode(task.getTrainMode());
}
}
String trainMode = StringUtil.isNotEmpty(form.getTrainMode())
? form.getTrainMode()
: (task != null ? task.getTrainMode() : null);
String signRule = resolveSignRule(trainMode);
form.setSignRule(signRule);
form.setCanScanSign(canScanByRule(signRule));
List persons = StringUtil.isEmpty(entity.getTaskId())
? Collections.emptyList()
: personTaskMapper.selectList(new LambdaQueryWrapper()
.eq(TmsPersonTaskEntity::getTaskId, entity.getTaskId())
.orderByAsc(TmsPersonTaskEntity::getCreatorTime));
Map userNames = loadUserNames(persons.stream()
.map(TmsPersonTaskEntity::getUserId)
.filter(StringUtil::isNotEmpty)
.distinct()
.collect(Collectors.toList()));
Set deptIds = new HashSet<>();
for (TmsPersonTaskEntity p : persons) {
if (p != null && StringUtil.isNotEmpty(p.getDeptId())) {
deptIds.add(p.getDeptId());
}
}
Map deptNames = loadDeptNames(deptIds);
List participants = new ArrayList<>(persons.size());
List signs = new ArrayList<>();
List traineeNames = new ArrayList<>();
// 优先读签到明细表;无数据时回退 person_task 投影
List signRows = recordSignMapper.selectList(
new LambdaQueryWrapper()
.eq(TmsRecordSignEntity::getForeignId, entity.getId())
.orderByAsc(TmsRecordSignEntity::getSignTime));
Map signByUser = new HashMap<>();
if (signRows != null) {
for (TmsRecordSignEntity s : signRows) {
if (s != null && StringUtil.isNotEmpty(s.getUserId())) {
signByUser.putIfAbsent(s.getUserId(), s);
}
}
}
Set quizAnswerUsers = loadQuizAnswerUserIds(entity.getTaskId(), entity.getId());
for (TmsPersonTaskEntity p : persons) {
String userName = userNames.getOrDefault(p.getUserId(), p.getUserId());
traineeNames.add(userName);
TmsRecordParticipantForm pf = new TmsRecordParticipantForm();
pf.setId(p.getId());
pf.setUserId(p.getUserId());
pf.setUserName(userName);
pf.setDeptName(deptNames.getOrDefault(p.getDeptId(), ""));
pf.setSignDate(formatDate(p.getSignTime()));
pf.setExamScore(p.getExamScore());
pf.setPassFlag(p.getPassFlag());
TmsRecordSignEntity sign = signByUser.get(p.getUserId());
if (sign != null && sign.getQuizScore() != null) {
pf.setQuizScore(sign.getQuizScore());
}
pf.setHasQuizAnswer(quizAnswerUsers.contains(p.getUserId()));
participants.add(pf);
}
if (signRows != null && !signRows.isEmpty()) {
Map signNames = loadUserNames(signRows.stream()
.map(TmsRecordSignEntity::getUserId)
.filter(StringUtil::isNotEmpty)
.distinct()
.collect(Collectors.toList()));
for (TmsRecordSignEntity s : signRows) {
TmsRecordSignForm sf = new TmsRecordSignForm();
sf.setId(s.getId());
sf.setUserName(StringUtil.isNotEmpty(s.getUserName())
? s.getUserName()
: signNames.getOrDefault(s.getUserId(), s.getUserId()));
sf.setSignTime(formatDate(s.getSignTime()));
sf.setSignMode(s.getSignMode());
sf.setGuestFlag(s.getGuestFlag());
signs.add(sf);
}
} else {
for (TmsPersonTaskEntity p : persons) {
if (p.getSignTime() == null) {
continue;
}
String userName = userNames.getOrDefault(p.getUserId(), p.getUserId());
TmsRecordSignForm sf = new TmsRecordSignForm();
sf.setId(p.getId());
sf.setUserName(userName);
sf.setSignTime(formatDate(p.getSignTime()));
sf.setSignMode(p.getSignMode());
sf.setGuestFlag(p.getGuestFlag());
signs.add(sf);
}
}
form.setParticipants(participants);
form.setSignList(signs);
form.setTrainees(String.join(",", traineeNames));
form.setAttachments(loadAttachments(entity));
form.setRemark(entity.getRemark());
return form;
}
/**
* 获取签到二维码信息
*/
@Override
public TmsRecordSignQrVO getSignQr(String id) {
TmsRecordEntity entity = requireOne(id);
if (TmsArchiveStatus.ARCHIVED.equalsCode(entity.getArchiveStatus())) {
throw new DataException("已存档记录不可生成签到码");
}
TmsTaskEntity task = StringUtil.isEmpty(entity.getTaskId()) ? null : taskMapper.selectById(entity.getTaskId());
String trainMode = StringUtil.isNotEmpty(entity.getTrainMode())
? entity.getTrainMode()
: (task != null ? task.getTrainMode() : null);
String signRule = resolveSignRule(trainMode);
TmsRecordSignQrVO vo = new TmsRecordSignQrVO();
vo.setRecordId(entity.getId());
vo.setTaskId(entity.getTaskId());
vo.setSubject(entity.getSubject());
vo.setSignRule(signRule);
vo.setModes(Collections.emptyList());
if (!canScanByRule(signRule)) {
vo.setCanScanSign(false);
vo.setTip("当前培训方式不支持扫码签到,请学员在「个人培训任务」中在线签到");
return vo;
}
List scanModes = signModeMapper.selectList(
new LambdaQueryWrapper()
.eq(TmsSignModeEntity::getEnabled, TmsYesNo.YES.getCode())
.in(TmsSignModeEntity::getModeCode, Arrays.asList(
TmsSignMode.APP_SCAN.getCode(), TmsSignMode.WX_SCAN.getCode()))
.orderByAsc(TmsSignModeEntity::getModeCode));
List modes = new ArrayList<>();
for (TmsSignModeEntity sm : scanModes) {
TmsRecordSignQrModeVO m = new TmsRecordSignQrModeVO();
m.setModeCode(sm.getModeCode());
m.setModeName(sm.getModeName());
modes.add(m);
}
vo.setModes(modes);
if (modes.isEmpty()) {
vo.setCanScanSign(false);
vo.setTip("暂无启用的扫码签到方式(APP扫码/微信扫码),请先在签到方式中启用");
return vo;
}
long expMs = System.currentTimeMillis() + SIGN_QR_TTL_MS;
Date expireAt = new Date(expMs);
Map payload = new LinkedHashMap<>();
payload.put("t", "tms_sign");
payload.put("recordId", entity.getId());
payload.put("taskId", entity.getTaskId());
payload.put("exp", expMs);
try {
vo.setQrContent(OBJECT_MAPPER.writeValueAsString(payload));
} catch (Exception e) {
throw new DataException("生成签到码失败");
}
vo.setExpireAt(DateUtil.dateToString(expireAt, FMT));
vo.setCanScanSign(true);
vo.setTip("");
return vo;
}
/**
* 扫码签到
*/
@Override
@Transactional(rollbackFor = Exception.class)
public TmsPersonTaskSignResult scanSign(TmsRecordScanSignForm form) {
if (form == null) {
throw new DataException("签到参数不能为空");
}
return personTaskService.scanSign(form.getQrContent(), form.getSignMode());
}
/**
* 解析签到规则
*/
private String resolveSignRule(String trainMode) {
if (StringUtil.isEmpty(trainMode)) {
return "";
}
TmsTrainModeEntity mode = trainModeMapper.selectOne(
new LambdaQueryWrapper()
.eq(TmsTrainModeEntity::getModeCode, trainMode)
.last("LIMIT 1"));
if (mode != null && StringUtil.isNotEmpty(mode.getSignRule())) {
return mode.getSignRule().trim();
}
return TmsTrainMode.ONLINE.equalsCodeIgnoreCase(trainMode) ? TmsSignRule.BOTH.getCode() : TmsSignRule.SCAN.getCode();
}
/**
* 按规则判断是否允许扫码签到
*/
private boolean canScanByRule(String signRule) {
return TmsSignRule.SCAN.equalsCodeIgnoreCase(signRule) || TmsSignRule.BOTH.equalsCodeIgnoreCase(signRule);
}
/**
* 培训记录:更新
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void update(String id, TmsRecordForm form) {
TmsRecordEntity entity = requireOne(id);
if (form == null) {
throw new DataException("表单不能为空");
}
if (StringUtil.isNotEmpty(form.getSubject())) {
entity.setSubject(form.getSubject().trim());
}
if (form.getCategory() != null) {
entity.setCategory(form.getCategory());
}
if (form.getTrainType() != null) {
entity.setTrainType(form.getTrainType());
}
if (form.getTrainMode() != null) {
entity.setTrainMode(form.getTrainMode());
}
if (form.getEvalMode() != null) {
entity.setEvalMode(form.getEvalMode());
}
entity.setRemark(form.getRemark());
entity.setLastModifyUserId(UserProvider.getLoginUserId());
entity.setLastModifyTime(new Date());
recordMapper.updateById(entity);
}
/**
* 按结束时间将记录置为可归档
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int markReadyByCloseTime() {
Date now = new Date();
// FDS:关闭时间到 → pending;进行中为 open
List rows = recordMapper.selectList(
new LambdaQueryWrapper()
.eq(TmsRecordEntity::getArchiveStatus, TmsArchiveStatus.OPEN.getCode())
.isNotNull(TmsRecordEntity::getCloseTime)
.le(TmsRecordEntity::getCloseTime, now));
if (rows == null || rows.isEmpty()) {
return 0;
}
String userId = UserProvider.getLoginUserId();
int count = 0;
for (TmsRecordEntity entity : rows) {
if (entity == null || StringUtil.isEmpty(entity.getId())) {
continue;
}
entity.setArchiveStatus(TmsArchiveStatus.PENDING.getCode());
entity.setLastModifyUserId(userId);
entity.setLastModifyTime(now);
recordMapper.updateById(entity);
count++;
}
return count;
}
/**
* 推进为可归档
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int promoteToReady(List ids) {
if (ids == null || ids.isEmpty()) {
throw new DataException("请选择要转入可归档的记录");
}
int count = 0;
Date now = new Date();
String userId = UserProvider.getLoginUserId();
for (String id : ids) {
if (StringUtil.isEmpty(id)) {
continue;
}
TmsRecordEntity entity = recordMapper.selectById(id);
if (entity == null) {
continue;
}
if (!TmsArchiveStatus.PENDING.equalsCode(entity.getArchiveStatus())) {
throw new DataException("仅待归档状态可转入可归档:" + entity.getRecordNo());
}
entity.setArchiveStatus(TmsArchiveStatus.READY.getCode());
entity.setLastModifyUserId(userId);
entity.setLastModifyTime(now);
recordMapper.updateById(entity);
count++;
}
return count;
}
/**
* 恢复为待处理
*/
@Override
@Transactional(rollbackFor = Exception.class)
public int restoreToPending(List ids) {
if (ids == null || ids.isEmpty()) {
throw new DataException("请选择要恢复的记录");
}
int count = 0;
Date now = new Date();
String userId = UserProvider.getLoginUserId();
for (String id : ids) {
if (StringUtil.isEmpty(id)) {
continue;
}
TmsRecordEntity entity = recordMapper.selectById(id);
if (entity == null) {
continue;
}
if (!TmsArchiveStatus.READY.equalsCode(entity.getArchiveStatus())) {
throw new DataException("仅可归档状态可恢复为待归档:" + entity.getRecordNo());
}
entity.setArchiveStatus(TmsArchiveStatus.PENDING.getCode());
entity.setLastModifyUserId(userId);
entity.setLastModifyTime(now);
recordMapper.updateById(entity);
count++;
}
return count;
}
/**
* 批量归档:仅 pending / ready 可归档。
* 每条走 {@link #archiveOne}(写 person_train、任务 done、未完成人再培训草稿)。
*/
@Override
@Transactional(rollbackFor = Exception.class)
@AuditLog(action = "RECORD_ARCHIVE", label = "归档培训记录", bizType = "TMS_RECORD",
bizCodeExpr = "#ids != null && !#ids.isEmpty() ? #ids[0] : 'BATCH'",
targetTable = "tms_record", targetIdExpr = "#ids != null && !#ids.isEmpty() ? #ids[0] : ''",
targetIdsExpr = "#ids", bizCodeRequired = false)
/**
* 归档
*/
public int archive(List ids) {
if (ids == null || ids.isEmpty()) {
throw new DataException("请选择要归档的记录");
}
int count = 0;
Date now = new Date();
String userId = UserProvider.getLoginUserId();
for (String id : ids) {
if (StringUtil.isEmpty(id)) {
continue;
}
TmsRecordEntity entity = recordMapper.selectById(id);
if (entity == null) {
continue;
}
String st = entity.getArchiveStatus();
if (!TmsArchiveStatus.PENDING.equalsCode(st) && !TmsArchiveStatus.READY.equalsCode(st)) {
throw new DataException("仅待归档/可归档状态下的记录可归档:" + entity.getRecordNo());
}
archiveOne(entity, now, userId);
count++;
}
return count;
}
@Override
@Transactional(rollbackFor = Exception.class)
@AuditLog(action = "RECORD_INVALID", label = "作废培训记录", bizType = "TMS_RECORD",
bizCodeExpr = "#ids != null && !#ids.isEmpty() ? #ids[0] : 'BATCH'",
targetTable = "tms_record", targetIdExpr = "#ids != null && !#ids.isEmpty() ? #ids[0] : ''",
targetIdsExpr = "#ids", bizCodeRequired = false)
/**
* 标记作废
*/
public int markInvalid(List ids) {
if (ids == null || ids.isEmpty()) {
throw new DataException("请选择要作废的记录");
}
int count = 0;
Date now = new Date();
String operatorId = UserProvider.getLoginUserId();
for (String id : ids) {
if (StringUtil.isEmpty(id)) {
continue;
}
TmsRecordEntity entity = recordMapper.selectById(id);
if (entity == null) {
continue;
}
String st = entity.getArchiveStatus();
if (TmsArchiveStatus.ARCHIVED.equalsCode(st) || TmsArchiveStatus.INVALID.equalsCode(st)) {
throw new DataException("已归档/已作废记录不可再作废:" + entity.getRecordNo());
}
List persons = StringUtil.isEmpty(entity.getTaskId())
? Collections.emptyList()
: personTaskMapper.selectList(new LambdaQueryWrapper()
.eq(TmsPersonTaskEntity::getTaskId, entity.getTaskId()));
boolean anyoneDone = false;
List incomplete = new ArrayList<>();
for (TmsPersonTaskEntity p : persons) {
if (p == null || "1".equals(p.getGuestFlag())) {
continue;
}
if (isCompletedForInvalid(entity.getEvalMode(), p)) {
anyoneDone = true;
break;
}
incomplete.add(p);
}
if (anyoneDone) {
throw new DataException("已有人员完成,不可作废:" + entity.getRecordNo());
}
entity.setArchiveStatus(TmsArchiveStatus.INVALID.getCode());
entity.setLastModifyUserId(operatorId);
entity.setLastModifyTime(now);
recordMapper.updateById(entity);
if (StringUtil.isNotEmpty(entity.getTaskId())) {
TmsTaskEntity task = taskMapper.selectById(entity.getTaskId());
if (task != null && !TmsTaskPublishStatus.CANCELLED.equalsCode(task.getPublishStatus())
&& !TmsTaskPublishStatus.DONE.equalsCode(task.getPublishStatus())) {
task.setPublishStatus(TmsTaskPublishStatus.CANCELLED.getCode());
task.setLastModifyUserId(operatorId);
task.setLastModifyTime(now);
taskMapper.updateById(task);
}
}
if (!incomplete.isEmpty()) {
createContinueDraftTask(entity, incomplete, now, operatorId);
} else if (StringUtil.isNotEmpty(entity.getTaskId())) {
// 无人完成也无人名册时,仍按原任务对象生成空壳再维护草稿(复制原 trainees)
TmsTaskEntity src = taskMapper.selectById(entity.getTaskId());
if (src != null && StringUtil.isNotEmpty(src.getTrainees())) {
List roster = new ArrayList<>();
for (String uid : src.getTrainees().split(",")) {
if (StringUtil.isEmpty(uid)) {
continue;
}
TmsPersonTaskEntity stub = new TmsPersonTaskEntity();
stub.setUserId(uid.trim());
roster.add(stub);
}
if (!roster.isEmpty()) {
createContinueDraftTask(entity, roster, now, operatorId);
}
}
}
count++;
}
return count;
}
/** 作废判定:在线考试须合格;其它方式已签到视为完成 */
private boolean isCompletedForInvalid(String evalMode, TmsPersonTaskEntity person) {
if (person.getSignTime() == null) {
return false;
}
if (TmsEvalMode.EXAM.equalsCodeIgnoreCase(evalMode)) {
return TmsPassFlag.isPass(person.getPassFlag());
}
return true;
}
/**
* 统计测验/考试次数
*/
@Override
public int calcQuizCount(String recordId) {
if (StringUtil.isEmpty(recordId)) {
throw new DataException("记录ID不能为空");
}
TmsRecordEntity record = recordMapper.selectById(recordId);
if (record == null) {
throw new DataException("培训记录不存在");
}
if (StringUtil.isEmpty(record.getTaskId())) {
return 0;
}
TmsTaskEntity task = taskMapper.selectById(record.getTaskId());
if (task == null) {
return 0;
}
Long signed = personTaskMapper.selectCount(
new LambdaQueryWrapper()
.eq(TmsPersonTaskEntity::getTaskId, record.getTaskId())
.isNotNull(TmsPersonTaskEntity::getSignTime)
.and(w -> w.isNull(TmsPersonTaskEntity::getGuestFlag)
.or().ne(TmsPersonTaskEntity::getGuestFlag, "1")));
int signedCount = signed == null ? 0 : signed.intValue();
return TmsQuizHelper.quizCount(signedCount, task.getQuizRatio());
}
@Override
public TmsQuizAnswerPaperVO getQuizAnswers(String recordId, String userId) {
TmsRecordEntity record = requireOne(recordId);
if (StringUtil.isEmpty(userId)) {
throw new DataException("用户ID不能为空");
}
if (StringUtil.isEmpty(record.getTaskId())) {
throw new DataException("培训记录未关联任务");
}
List scores = quizScoreMapper.selectList(new LambdaQueryWrapper()
.eq(TmsQuizScoreEntity::getTaskId, record.getTaskId())
.eq(TmsQuizScoreEntity::getUserId, userId.trim())
.and(w -> w.eq(TmsQuizScoreEntity::getRecordId, record.getId())
.or().isNull(TmsQuizScoreEntity::getRecordId))
.orderByAsc(TmsQuizScoreEntity::getCreatorTime)
.orderByAsc(TmsQuizScoreEntity::getId));
if (scores == null || scores.isEmpty()) {
throw new DataException("该学员尚未提交提问答卷");
}
List presets = taskQuizMapper.selectList(new LambdaQueryWrapper()
.eq(TmsTaskQuizEntity::getForeignId, record.getTaskId())
.orderByAsc(TmsTaskQuizEntity::getSortNo)
.orderByAsc(TmsTaskQuizEntity::getCreatorTime));
Map names = loadUserNames(Collections.singletonList(userId.trim()));
String userName = names.getOrDefault(userId.trim(), userId.trim());
String passFlag = null;
BigDecimal quizScore = null;
TmsPersonTaskEntity person = personTaskMapper.selectOne(new LambdaQueryWrapper()
.eq(TmsPersonTaskEntity::getTaskId, record.getTaskId())
.eq(TmsPersonTaskEntity::getUserId, userId.trim())
.last("LIMIT 1"));
if (person != null) {
passFlag = person.getPassFlag();
}
TmsRecordSignEntity sign = recordSignMapper.selectOne(new LambdaQueryWrapper()
.eq(TmsRecordSignEntity::getForeignId, record.getId())
.eq(TmsRecordSignEntity::getUserId, userId.trim())
.last("LIMIT 1"));
if (sign != null) {
quizScore = sign.getQuizScore();
if (StringUtil.isEmpty(passFlag)) {
passFlag = sign.getResultFlag();
}
}
String quizTime = formatDate(scores.get(0).getQuizTime() != null
? scores.get(0).getQuizTime()
: scores.get(0).getCreatorTime());
TmsQuizAnswerPaperVO paper = TmsQuizHelper.buildAnswerPaper(
record.getId(),
record.getTaskId(),
userId.trim(),
userName,
passFlag,
quizTime,
scores,
presets);
if (quizScore != null) {
paper.setTotalScore(quizScore);
}
return paper;
}
private Set loadQuizAnswerUserIds(String taskId, String recordId) {
if (StringUtil.isEmpty(taskId)) {
return Collections.emptySet();
}
List rows = quizScoreMapper.selectList(new LambdaQueryWrapper()
.select(TmsQuizScoreEntity::getUserId)
.eq(TmsQuizScoreEntity::getTaskId, taskId)
.and(w -> w.eq(TmsQuizScoreEntity::getRecordId, recordId)
.or().isNull(TmsQuizScoreEntity::getRecordId)));
if (rows == null || rows.isEmpty()) {
return Collections.emptySet();
}
return rows.stream()
.map(TmsQuizScoreEntity::getUserId)
.filter(StringUtil::isNotEmpty)
.collect(Collectors.toSet());
}
/**
* 归档单条核心:
*
* - 统计签到率 / 合格率(访客不计)
* - 记录 → archived;任务 → done;计划明细 → done
* - 每位非访客学员 {@link #upsertPersonTrain} → 个人档案培训目录
* - 未完成人 {@link #createContinueDraftTask}(不自动发布)
*
*/
private void archiveOne(TmsRecordEntity entity, Date now, String operatorId) {
String taskId = entity.getTaskId();
List persons = Collections.emptyList();
if (StringUtil.isNotEmpty(taskId)) {
persons = personTaskMapper.selectList(
new LambdaQueryWrapper()
.eq(TmsPersonTaskEntity::getTaskId, taskId)
.orderByAsc(TmsPersonTaskEntity::getCreatorTime));
}
int total = 0;
int signed = 0;
int passed = 0;
List incomplete = new ArrayList<>();
for (TmsPersonTaskEntity p : persons) {
if (p == null || "1".equals(p.getGuestFlag())) {
continue;
}
total++;
boolean hasSign = p.getSignTime() != null;
if (hasSign) {
signed++;
}
boolean pass = TmsPassFlag.isPass(p.getPassFlag());
if (pass) {
passed++;
}
if (isIncomplete(entity.getEvalMode(), p)) {
incomplete.add(p);
}
}
entity.setSignRate(rateFloor(signed, total));
entity.setPassRate(rateFloor(passed, total));
entity.setArchiveStatus(TmsArchiveStatus.ARCHIVED.getCode());
entity.setArchiveTime(now);
entity.setArchiverId(operatorId);
entity.setLastModifyUserId(operatorId);
entity.setLastModifyTime(now);
recordMapper.updateById(entity);
if (StringUtil.isNotEmpty(taskId)) {
TmsTaskEntity task = taskMapper.selectById(taskId);
if (task != null) {
task.setSignRate(entity.getSignRate());
task.setPassRate(entity.getPassRate());
task.setPublishStatus(TmsTaskPublishStatus.DONE.getCode());
task.setLastModifyUserId(operatorId);
task.setLastModifyTime(now);
taskMapper.updateById(task);
markPlanItemDone(task);
}
}
for (TmsPersonTaskEntity p : persons) {
if (p == null || "1".equals(p.getGuestFlag()) || StringUtil.isEmpty(p.getUserId())) {
continue;
}
upsertPersonTrain(entity, p, now, operatorId);
}
if (!incomplete.isEmpty()) {
createContinueDraftTask(entity, incomplete, now, operatorId);
}
}
/**
* 实时参与进度
*/
@Override
public TmsRecordProgressVO getProgress(String recordId) {
TmsRecordEntity record = requireOne(recordId);
List persons = listNonGuestPersons(record.getTaskId());
return buildProgress(record, persons);
}
/**
* 未完成人员清单(按部门名称、姓名排序)
*/
@Override
public List listIncomplete(String recordId) {
TmsRecordEntity record = requireOne(recordId);
String evalMode = record.getEvalMode();
List persons = listNonGuestPersons(record.getTaskId());
List incomplete = new ArrayList<>();
for (TmsPersonTaskEntity p : persons) {
if (isIncomplete(evalMode, p)) {
incomplete.add(p);
}
}
Map userNames = loadUserNames(incomplete.stream()
.map(TmsPersonTaskEntity::getUserId)
.filter(StringUtil::isNotEmpty)
.distinct()
.collect(Collectors.toList()));
Set deptIds = new HashSet<>();
for (TmsPersonTaskEntity p : incomplete) {
if (StringUtil.isNotEmpty(p.getDeptId())) {
deptIds.add(p.getDeptId());
}
}
Map deptNames = loadDeptNames(deptIds);
List list = new ArrayList<>(incomplete.size());
for (TmsPersonTaskEntity p : incomplete) {
TmsRecordIncompleteVO vo = new TmsRecordIncompleteVO();
vo.setPersonTaskId(p.getId());
vo.setUserId(p.getUserId());
vo.setUserName(userNames.getOrDefault(p.getUserId(), p.getUserId()));
vo.setDeptId(p.getDeptId());
vo.setDeptName(deptNames.getOrDefault(p.getDeptId(), ""));
vo.setBizStatus(p.getBizStatus());
vo.setSignTime(formatDate(p.getSignTime()));
vo.setExamScore(p.getExamScore());
vo.setPassFlag(p.getPassFlag());
vo.setReason(incompleteReason(evalMode, p));
list.add(vo);
}
list.sort(Comparator
.comparing((TmsRecordIncompleteVO v) -> StringUtil.isEmpty(v.getDeptName()) ? "" : v.getDeptName())
.thenComparing(v -> StringUtil.isEmpty(v.getUserName()) ? "" : v.getUserName()));
return list;
}
/**
* 按任务回写记录签到率/合格率
*/
@Override
public void refreshRatesByTaskId(String taskId) {
if (StringUtil.isEmpty(taskId)) {
return;
}
TmsRecordEntity record = recordMapper.selectOne(new LambdaQueryWrapper()
.eq(TmsRecordEntity::getTaskId, taskId)
.last("LIMIT 1"));
if (record == null) {
return;
}
List persons = listNonGuestPersons(taskId);
TmsRecordProgressVO progress = buildProgress(record, persons);
record.setSignRate(progress.getSignRate());
record.setPassRate(progress.getPassRate());
record.setLastModifyTime(new Date());
recordMapper.updateById(record);
TmsTaskEntity task = taskMapper.selectById(taskId);
if (task != null) {
task.setSignRate(progress.getSignRate());
task.setPassRate(progress.getPassRate());
task.setLastModifyTime(new Date());
taskMapper.updateById(task);
}
}
private TmsRecordProgressVO buildProgress(TmsRecordEntity record, List persons) {
int total = 0;
int signed = 0;
int passed = 0;
int incomplete = 0;
String evalMode = record.getEvalMode();
for (TmsPersonTaskEntity p : persons) {
if (p == null) {
continue;
}
total++;
if (p.getSignTime() != null) {
signed++;
}
if (TmsPassFlag.isPass(p.getPassFlag())) {
passed++;
}
if (isIncomplete(evalMode, p)) {
incomplete++;
}
}
TmsRecordProgressVO vo = new TmsRecordProgressVO();
vo.setRecordId(record.getId());
vo.setTaskId(record.getTaskId());
vo.setTotalCount(total);
vo.setSignedCount(signed);
vo.setPassedCount(passed);
vo.setIncompleteCount(incomplete);
vo.setSignRate(rateFloor(signed, total));
vo.setPassRate(rateFloor(passed, total));
vo.setIncompleteRate(rateFloor(incomplete, total));
vo.setEvalMode(evalMode);
return vo;
}
private List listNonGuestPersons(String taskId) {
if (StringUtil.isEmpty(taskId)) {
return Collections.emptyList();
}
List rows = personTaskMapper.selectList(new LambdaQueryWrapper()
.eq(TmsPersonTaskEntity::getTaskId, taskId)
.orderByAsc(TmsPersonTaskEntity::getCreatorTime));
if (rows == null || rows.isEmpty()) {
return Collections.emptyList();
}
List list = new ArrayList<>();
for (TmsPersonTaskEntity p : rows) {
if (p != null && !"1".equals(p.getGuestFlag())) {
list.add(p);
}
}
return list;
}
private Map loadDeptNames(Set deptIds) {
Map map = new HashMap<>();
if (deptIds == null || deptIds.isEmpty()) {
return map;
}
for (String id : deptIds) {
try {
OrganizeEntity org = organizeApi.getInfoById(id);
if (org != null && StringUtil.isNotEmpty(org.getFullName())) {
map.put(id, org.getFullName());
}
} catch (Exception ignored) {
// ignore
}
}
return map;
}
private String incompleteReason(String evalMode, TmsPersonTaskEntity person) {
if (person.getSignTime() == null) {
return "未签到";
}
if (TmsEvalMode.EXAM.equalsCodeIgnoreCase(evalMode) && !TmsPassFlag.isPass(person.getPassFlag())) {
return "考试未合格";
}
if (TmsEvalMode.QUIZ.equalsCodeIgnoreCase(evalMode) && !TmsPassFlag.isPass(person.getPassFlag())) {
return "提问未合格";
}
if (TmsEvalMode.PRACTICE.equalsCodeIgnoreCase(evalMode) && !TmsPassFlag.isPass(person.getPassFlag())) {
return "实操未合格";
}
return "未完成";
}
/** 未完成:未签到;考试/提问/实操另要求未合格 */
private boolean isIncomplete(String evalMode, TmsPersonTaskEntity person) {
if (person.getSignTime() == null) {
return true;
}
if (TmsEvalMode.EXAM.equalsCodeIgnoreCase(evalMode)
|| TmsEvalMode.QUIZ.equalsCodeIgnoreCase(evalMode)
|| TmsEvalMode.PRACTICE.equalsCodeIgnoreCase(evalMode)) {
return !TmsPassFlag.isPass(person.getPassFlag());
}
return false;
}
/**
* 回写计划明细已完成
*/
private void markPlanItemDone(TmsTaskEntity task) {
if (task == null || StringUtil.isEmpty(task.getSourceItemId())) {
return;
}
if (TmsTaskSourceType.ANNUAL.equalsCode(task.getSourceType())) {
TmsAnnualPlanItemEntity item = annualPlanItemMapper.selectById(task.getSourceItemId());
if (item != null) {
item.setLineStatus(TmsPlanLineStatus.DONE.getCode());
annualPlanItemMapper.updateById(item);
}
return;
}
if (TmsTaskSourceType.POST.equalsCode(task.getSourceType())) {
TmsPostPlanItemEntity item = postPlanItemMapper.selectById(task.getSourceItemId());
if (item != null) {
item.setLineStatus(TmsPlanLineStatus.DONE.getCode());
postPlanItemMapper.updateById(item);
}
}
}
/**
* 完成率向下取整
*/
private static int rateFloor(int part, int total) {
if (total <= 0) {
return 0;
}
return (int) Math.floor(part * 100.0 / total);
}
/**
* 写入/更新个人培训目录 {@code tms_person_train}(档案页 catalogList 的数据源)。
* 无个人档案主档时先 {@link #ensurePersonFile}。
* 同一 recordId+userId 幂等更新。
*/
private void upsertPersonTrain(TmsRecordEntity record, TmsPersonTaskEntity person, Date now, String operatorId) {
TmsPersonTrainEntity exists = personTrainMapper.selectOne(
new LambdaQueryWrapper()
.eq(TmsPersonTrainEntity::getRecordId, record.getId())
.eq(TmsPersonTrainEntity::getUserId, person.getUserId())
.last("LIMIT 1"));
if (exists != null) {
exists.setPassFlag(TmsPassFlag.normalize(person.getPassFlag()));
exists.setHours(person.getHours());
exists.setSubject(record.getSubject());
exists.setCategory(record.getCategory());
exists.setTrainMode(record.getTrainMode());
exists.setEvalMode(record.getEvalMode());
exists.setTeacherId(record.getTeacherId());
exists.setStartTime(record.getStartTime());
exists.setEndTime(record.getEndTime());
exists.setLastModifyUserId(operatorId);
exists.setLastModifyTime(now);
personTrainMapper.updateById(exists);
return;
}
TmsPersonFileEntity file = ensurePersonFile(person.getUserId(), now, operatorId);
TmsPersonTrainEntity row = new TmsPersonTrainEntity();
row.setId(RandomUtil.uuId());
row.setArchiveId(file.getId());
row.setUserId(person.getUserId());
row.setRecordId(record.getId());
row.setRecordNo(record.getRecordNo());
row.setCategory(record.getCategory());
row.setSubject(record.getSubject());
row.setTrainMode(record.getTrainMode());
row.setTeacherId(record.getTeacherId());
row.setEvalMode(record.getEvalMode());
row.setPassFlag(TmsPassFlag.normalize(person.getPassFlag()));
row.setStartTime(record.getStartTime());
row.setEndTime(record.getEndTime());
row.setHours(person.getHours());
row.setCreatorUserId(operatorId);
row.setCreatorTime(now);
personTrainMapper.insert(row);
}
/** 确保存在个人档案主档 tms_person_file(归档写目录前调用)。 */
private TmsPersonFileEntity ensurePersonFile(String userId, Date now, String operatorId) {
TmsPersonFileEntity file = personFileMapper.selectOne(
new LambdaQueryWrapper()
.eq(TmsPersonFileEntity::getUserId, userId)
.last("LIMIT 1"));
if (file != null) {
return file;
}
UserEntity user = null;
try {
user = userApi.getInfoById(userId);
} catch (Exception ignored) {
// ignore remote failures; still create a stub archive
}
file = new TmsPersonFileEntity();
file.setId(RandomUtil.uuId());
file.setArchiveNo(nextArchiveNo());
file.setArchiveDate(now);
file.setUserId(userId);
if (user != null) {
file.setUserName(user.getRealName());
file.setDeptId(user.getOrganizeId());
}
file.setBizStatus(TmsValidRevoked.VALID.getCode());
file.setCreatorUserId(operatorId);
file.setCreatorTime(now);
personFileMapper.insert(file);
return file;
}
/**
* 生成档案编号
*/
private String nextArchiveNo() {
int year = Calendar.getInstance().get(Calendar.YEAR);
String prefix = "AR" + year;
TmsPersonFileEntity last = personFileMapper.selectOne(
new LambdaQueryWrapper()
.likeRight(TmsPersonFileEntity::getArchiveNo, prefix)
.orderByDesc(TmsPersonFileEntity::getArchiveNo)
.last("LIMIT 1"));
int seq = 1;
if (last != null && StringUtil.isNotEmpty(last.getArchiveNo())
&& last.getArchiveNo().length() >= prefix.length() + 4) {
try {
seq = Integer.parseInt(last.getArchiveNo().substring(prefix.length())) + 1;
} catch (NumberFormatException ignored) {
seq = 1;
}
}
return prefix + String.format("%04d", Math.min(seq, 9999));
}
/** 未完成人员 → 草稿再维护任务(不自动发布) */
/**
* 为未完成学员生成「再培训」草稿任务(TD…,category=retrain,不自动发布),
* 便于维护后再次送审/发布。
*/
private void createContinueDraftTask(TmsRecordEntity record, List incomplete,
Date now, String operatorId) {
if (StringUtil.isEmpty(record.getTaskId())) {
return;
}
TmsTaskEntity src = taskMapper.selectById(record.getTaskId());
if (src == null) {
return;
}
// 同一原任务若已有未发布 continue,则合并培训对象,避免重复草稿
TmsTaskEntity existing = taskMapper.selectOne(
new LambdaQueryWrapper()
.eq(TmsTaskEntity::getPrevTaskNo, src.getTaskNo())
.eq(TmsTaskEntity::getTaskKind, TmsTaskKind.CONTINUE.getCode())
.eq(TmsTaskEntity::getPublishStatus, TmsTaskPublishStatus.DRAFT.getCode())
.last("LIMIT 1"));
List userIds = incomplete.stream()
.map(TmsPersonTaskEntity::getUserId)
.filter(StringUtil::isNotEmpty)
.distinct()
.collect(Collectors.toList());
if (userIds.isEmpty()) {
return;
}
if (existing != null) {
List merged = new ArrayList<>();
if (StringUtil.isNotEmpty(existing.getTrainees())) {
for (String id : existing.getTrainees().split(",")) {
if (StringUtil.isNotEmpty(id) && !merged.contains(id.trim())) {
merged.add(id.trim());
}
}
}
for (String id : userIds) {
if (!merged.contains(id)) {
merged.add(id);
}
}
existing.setTrainees(String.join(",", merged));
existing.setCategory(TmsTaskCategory.RETRAIN.getCode());
existing.setTaskKind(TmsTaskKind.CONTINUE.getCode());
existing.setLastModifyUserId(operatorId);
existing.setLastModifyTime(now);
taskMapper.updateById(existing);
return;
}
TmsTaskEntity draft = new TmsTaskEntity();
draft.setId(RandomUtil.uuId());
draft.setTaskNo(nextDraftTaskNo());
draft.setCategory(TmsTaskCategory.RETRAIN.getCode());
draft.setTrainType(src.getTrainType());
draft.setSourceType(src.getSourceType());
draft.setSourcePlanNo(src.getSourcePlanNo());
draft.setSourceId(src.getSourceId());
draft.setSourceItemId(src.getSourceItemId());
draft.setSubject(src.getSubject());
draft.setKeyPoints(src.getKeyPoints());
draft.setMaterialId(src.getMaterialId());
draft.setMaterialNo(src.getMaterialNo());
draft.setPlaceId(src.getPlaceId());
draft.setPlaceNo(src.getPlaceNo());
draft.setPlaceName(src.getPlaceName());
draft.setPaperId(src.getPaperId());
draft.setPaperName(src.getPaperName());
draft.setTeacherId(src.getTeacherId());
draft.setTrainMode(src.getTrainMode());
draft.setEvalMode(src.getEvalMode());
draft.setTrainees(String.join(",", userIds));
draft.setTaskKind(TmsTaskKind.CONTINUE.getCode());
draft.setPrevTaskNo(src.getTaskNo());
draft.setPublishStatus(TmsTaskPublishStatus.DRAFT.getCode());
draft.setRetakeLimit(src.getRetakeLimit());
draft.setPassScore(src.getPassScore());
draft.setQuizRatio(src.getQuizRatio());
draft.setOverdueRemind(src.getOverdueRemind());
draft.setInviteFlag(src.getInviteFlag());
draft.setRemark("由培训记录 " + record.getRecordNo() + " 归档自动生成(未完成人员再维护)");
draft.setCreatorUserId(operatorId);
draft.setCreatorTime(now);
taskMapper.insert(draft);
// 复制任务附件引用
List files = taskFileMapper.selectList(
new LambdaQueryWrapper()
.eq(TmsTaskFileEntity::getForeignId, src.getId()));
for (TmsTaskFileEntity f : files) {
TmsTaskFileEntity copy = new TmsTaskFileEntity();
copy.setId(RandomUtil.uuId());
copy.setForeignId(draft.getId());
copy.setFileName(f.getFileName());
copy.setFileNo(f.getFileNo());
copy.setMaterialId(f.getMaterialId());
copy.setFileJson(f.getFileJson());
copy.setCanDownload(f.getCanDownload());
copy.setCreatorUserId(operatorId);
copy.setCreatorTime(now);
taskFileMapper.insert(copy);
}
}
/**
* 生成草稿任务编号
*/
private synchronized String nextDraftTaskNo() {
String ym = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMM"));
String prefix = PREFIX_DRAFT + 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);
}
/**
* 培训记录:删除
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void delete(List ids) {
if (ids == null || ids.isEmpty()) {
throw new DataException("请选择要删除的记录");
}
for (String id : ids) {
if (StringUtil.isEmpty(id)) {
continue;
}
recordMapper.deleteById(id);
}
}
/**
* 应用列表查询模式过滤
*/
private void applyListMode(LambdaQueryWrapper qw, TmsRecordQuery query) {
String archiveStatus = query.getArchiveStatus();
if (StringUtil.isNotEmpty(archiveStatus)) {
qw.eq(TmsRecordEntity::getArchiveStatus, archiveStatus);
return;
}
String mode = query.getListMode();
// 口径:open进行中 / pending待归档 / ready可归档 / archived已归档
if (TmsArchiveStatus.READY.equalsCode(mode)) {
qw.in(TmsRecordEntity::getArchiveStatus,
TmsArchiveStatus.PENDING.getCode(), TmsArchiveStatus.READY.getCode());
} else if (TmsArchiveStatus.ARCHIVED.equalsCode(mode)) {
qw.eq(TmsRecordEntity::getArchiveStatus, TmsArchiveStatus.ARCHIVED.getCode());
} else {
qw.eq(TmsRecordEntity::getArchiveStatus, TmsArchiveStatus.OPEN.getCode());
}
}
/**
* 转为列表表单
*/
private TmsRecordForm toListForm(TmsRecordEntity entity, Map names) {
TmsRecordForm form = new TmsRecordForm();
form.setId(entity.getId());
form.setRecordNo(entity.getRecordNo());
form.setTaskId(entity.getTaskId());
form.setTaskNo(entity.getTaskNo());
form.setCategory(entity.getCategory());
form.setSubject(entity.getSubject());
form.setTeacherId(entity.getTeacherId());
form.setTrainerName(names.getOrDefault(entity.getTeacherId(),
StringUtil.isEmpty(entity.getTeacherId()) ? "-" : entity.getTeacherId()));
form.setTrainType(StringUtil.isNotEmpty(entity.getTrainType())
? entity.getTrainType()
: entity.getTrainMode());
form.setTrainMode(entity.getTrainMode());
form.setEvalMode(entity.getEvalMode());
form.setStartTime(formatDate(entity.getStartTime()));
form.setEndTime(formatDate(entity.getEndTime()));
form.setCloseTime(formatDate(entity.getCloseTime()));
form.setArchiveStatus(entity.getArchiveStatus());
form.setSignRate(entity.getSignRate());
form.setPassRate(entity.getPassRate());
form.setRemark(entity.getRemark());
return form;
}
/**
* 加载培训记录附件
*/
private List loadAttachments(TmsRecordEntity entity) {
if (entity == null) {
return Collections.emptyList();
}
// 优先用发布时同步的 lecture_file 快照
List fromSnap = parseLectureFile(entity.getLectureFile());
if (!fromSnap.isEmpty()) {
return fromSnap;
}
return loadAttachmentsFromTask(entity.getTaskId());
}
/**
* 解析课件 JSON
*/
private List parseLectureFile(String lectureFile) {
if (StringUtil.isEmpty(lectureFile)) {
return Collections.emptyList();
}
try {
List