|
|
@@ -0,0 +1,155 @@
|
|
|
+package com.train.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.train.entity.*;
|
|
|
+import com.train.mapper.*;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.security.MessageDigest;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 完课证书服务:校验离场验收 6 项(课纲 V4 口径,映射到系统内数据),
|
|
|
+ * 通过后返回确定性证书号(CERT- 前缀,不落库,幂等)。领取动作写 audit_log。
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+public class CertService {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private TrainUserMapper trainUserMapper;
|
|
|
+ @Resource
|
|
|
+ private TrainCheckinMapper trainCheckinMapper;
|
|
|
+ @Resource
|
|
|
+ private TrainPrepCheckMapper trainPrepCheckMapper;
|
|
|
+ @Resource
|
|
|
+ private TrainAssignmentMapper trainAssignmentMapper;
|
|
|
+ @Resource
|
|
|
+ private TrainSubmissionMapper trainSubmissionMapper;
|
|
|
+ @Resource
|
|
|
+ private TrainTeachingCardMapper trainTeachingCardMapper;
|
|
|
+ @Resource
|
|
|
+ private TrainRoadmapMapper trainRoadmapMapper;
|
|
|
+ @Resource
|
|
|
+ private TrainGroupMemberMapper trainGroupMemberMapper;
|
|
|
+ @Resource
|
|
|
+ private TrainPlanMapper trainPlanMapper;
|
|
|
+ @Resource
|
|
|
+ private TrainAuditLogMapper trainAuditLogMapper;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 校验离场验收 6 项并生成证书号。
|
|
|
+ * 返回结构:{eligible, certNo, issuedAt, items:[{key,label,ok}]}
|
|
|
+ */
|
|
|
+ public Map<String, Object> issue(Long uid) {
|
|
|
+ List<Map<String, Object>> items = new ArrayList<>();
|
|
|
+ TrainUser user = trainUserMapper.selectById(uid);
|
|
|
+
|
|
|
+ // 1 装机打卡:可独立打开 WorkBuddy 完成一次有效提问
|
|
|
+ boolean checkinOk = trainCheckinMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<TrainCheckin>()
|
|
|
+ .eq(TrainCheckin::getUid, uid)
|
|
|
+ .eq(TrainCheckin::getStatus, "done")) > 0;
|
|
|
+ items.add(item("checkin", "装机打卡(有效提问)", checkinOk));
|
|
|
+
|
|
|
+ // 2 数据自检:三本账数据自查且数据就绪
|
|
|
+ boolean prepOk = trainPrepCheckMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<TrainPrepCheck>().eq(TrainPrepCheck::getUid, uid)) > 0;
|
|
|
+ items.add(item("prep", "数据自检(三本账就绪)", prepOk));
|
|
|
+
|
|
|
+ // 3 三本账分工确认
|
|
|
+ boolean assignOk = trainAssignmentMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<TrainAssignment>()
|
|
|
+ .eq(TrainAssignment::getUid, uid)
|
|
|
+ .eq(TrainAssignment::getStatus, "confirmed")) > 0;
|
|
|
+ items.add(item("assignment", "三本账分工确认", assignOk));
|
|
|
+
|
|
|
+ // 4 成果核验通过(深度产出 / 互教跑通)
|
|
|
+ boolean submitOk = trainSubmissionMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<TrainSubmission>()
|
|
|
+ .eq(TrainSubmission::getUid, uid)
|
|
|
+ .eq(TrainSubmission::getStatus, "approved")) > 0;
|
|
|
+ items.add(item("submission", "成果上传并核验通过", submitOk));
|
|
|
+
|
|
|
+ // 5 教学卡 + 路演完成
|
|
|
+ boolean cardOk = trainTeachingCardMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<TrainTeachingCard>().eq(TrainTeachingCard::getUid, uid)) > 0;
|
|
|
+ boolean roadmapOk = false;
|
|
|
+ List<TrainGroupMember> members = trainGroupMemberMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<TrainGroupMember>().eq(TrainGroupMember::getUid, uid));
|
|
|
+ for (TrainGroupMember m : members) {
|
|
|
+ if (trainRoadmapMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<TrainRoadmap>().eq(TrainRoadmap::getGroupId, m.getGroupId())) > 0) {
|
|
|
+ roadmapOk = true;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ items.add(item("roadmap", "教学卡与路演完成", cardOk && roadmapOk));
|
|
|
+
|
|
|
+ // 6 7 天行动计划已提交
|
|
|
+ boolean planOk = trainPlanMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<TrainPlan>().eq(TrainPlan::getUid, uid)) > 0;
|
|
|
+ items.add(item("plan", "7天行动计划已提交", planOk));
|
|
|
+
|
|
|
+ boolean eligible = checkinOk && prepOk && assignOk && submitOk && cardOk && roadmapOk && planOk;
|
|
|
+
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
+ result.put("eligible", eligible);
|
|
|
+ result.put("items", items);
|
|
|
+ if (eligible) {
|
|
|
+ Long classId = user != null ? user.getClassId() : null;
|
|
|
+ String certNo = genCertNo(uid, classId);
|
|
|
+ result.put("certNo", certNo);
|
|
|
+ result.put("issuedAt", new Date());
|
|
|
+ ensureAuditLog(uid, "cert_issued", "领取完课证书 " + certNo);
|
|
|
+ } else {
|
|
|
+ result.put("certNo", null);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Map<String, Object> item(String key, String label, boolean ok) {
|
|
|
+ Map<String, Object> m = new HashMap<>();
|
|
|
+ m.put("key", key);
|
|
|
+ m.put("label", label);
|
|
|
+ m.put("ok", ok);
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 确定性证书号:CERT-UC{classId}-{uid}-{MD5 前 8 位大写},同一学员重复调用结果一致 */
|
|
|
+ private String genCertNo(Long uid, Long classId) {
|
|
|
+ try {
|
|
|
+ MessageDigest md = MessageDigest.getInstance("MD5");
|
|
|
+ byte[] digest = md.digest(("cert:" + uid + ":" + classId).getBytes(StandardCharsets.UTF_8));
|
|
|
+ StringBuilder sb = new StringBuilder();
|
|
|
+ for (int i = 0; i < 4; i++) {
|
|
|
+ sb.append(String.format("%02X", digest[i]));
|
|
|
+ }
|
|
|
+ return "CERT-UC" + (classId == null ? "0" : classId) + "-" + uid + "-" + sb;
|
|
|
+ } catch (Exception e) {
|
|
|
+ return "CERT-" + uid + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 审计落账(幂等:同 action + targetId 已存在则跳过) */
|
|
|
+ private void ensureAuditLog(Long uid, String action, String detail) {
|
|
|
+ Long exists = trainAuditLogMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<TrainAuditLog>()
|
|
|
+ .eq(TrainAuditLog::getAction, action)
|
|
|
+ .eq(TrainAuditLog::getTargetId, uid));
|
|
|
+ if (exists != null && exists > 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ TrainAuditLog entry = new TrainAuditLog();
|
|
|
+ entry.setAction(action);
|
|
|
+ entry.setActorUid(uid);
|
|
|
+ entry.setTargetType("user");
|
|
|
+ entry.setTargetId(uid);
|
|
|
+ entry.setDetail(detail);
|
|
|
+ entry.setTs(new Date());
|
|
|
+ trainAuditLogMapper.insert(entry);
|
|
|
+ }
|
|
|
+}
|