|
|
@@ -7,13 +7,12 @@ 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。
|
|
|
+ * 通过后签发证书编号(CM-{课程码}-{年份}-{序号},落库,幂等)。
|
|
|
+ * 同一学员同一课程(uid + level 维度)重复领取返回同一编号,不重复发号。
|
|
|
*/
|
|
|
@Slf4j
|
|
|
@Service
|
|
|
@@ -39,10 +38,16 @@ public class CertService {
|
|
|
private TrainPlanMapper trainPlanMapper;
|
|
|
@Resource
|
|
|
private TrainAuditLogMapper trainAuditLogMapper;
|
|
|
+ @Resource
|
|
|
+ private TrainCertificateMapper trainCertificateMapper;
|
|
|
+ @Resource
|
|
|
+ private TrainClassMapper trainClassMapper;
|
|
|
+ @Resource
|
|
|
+ private TrainCourseMapper trainCourseMapper;
|
|
|
|
|
|
/**
|
|
|
- * 校验离场验收 6 项并生成证书号。
|
|
|
- * 返回结构:{eligible, certNo, issuedAt, items:[{key,label,ok}]}
|
|
|
+ * 校验离场验收 6 项并签发证书编号。
|
|
|
+ * 返回结构:{eligible, certNo, issuedAt, items:[{key,label,ok}], level, year, seq}
|
|
|
*/
|
|
|
public Map<String, Object> issue(Long uid) {
|
|
|
List<Map<String, Object>> items = checkItemsOf(uid);
|
|
|
@@ -54,17 +59,93 @@ public class CertService {
|
|
|
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);
|
|
|
+ // 解析课程等级:user.classId → train_class.courseId → train_course.level(任一为空则默认 L0)
|
|
|
+ String level = resolveLevel(user);
|
|
|
+
|
|
|
+ // 幂等:同一学员同一课程已发过则直接返回
|
|
|
+ TrainCertificate existing = trainCertificateMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<TrainCertificate>()
|
|
|
+ .eq(TrainCertificate::getUid, uid)
|
|
|
+ .eq(TrainCertificate::getLevel, level)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ if (existing != null) {
|
|
|
+ result.put("certNo", existing.getCertNo());
|
|
|
+ result.put("issuedAt", existing.getIssuedAt());
|
|
|
+ result.put("level", existing.getLevel());
|
|
|
+ result.put("year", existing.getYear());
|
|
|
+ result.put("seq", existing.getSeq());
|
|
|
+ } else {
|
|
|
+ // 发号:当年该 level 最大 seq + 1
|
|
|
+ int year = Calendar.getInstance().get(Calendar.YEAR);
|
|
|
+ int maxSeq = trainCertificateMapper.selectCount(
|
|
|
+ new LambdaQueryWrapper<TrainCertificate>()
|
|
|
+ .eq(TrainCertificate::getLevel, level)
|
|
|
+ .eq(TrainCertificate::getYear, year));
|
|
|
+ int seq = maxSeq + 1;
|
|
|
+ String certNo = String.format("CM-%s-%d-%03d", level, year, seq);
|
|
|
+
|
|
|
+ Date now = new Date();
|
|
|
+ TrainCertificate cert = new TrainCertificate();
|
|
|
+ cert.setCertNo(certNo);
|
|
|
+ cert.setUid(uid);
|
|
|
+ cert.setClassId(user != null ? user.getClassId() : null);
|
|
|
+ cert.setLevel(level);
|
|
|
+ cert.setYear(year);
|
|
|
+ cert.setSeq(seq);
|
|
|
+ cert.setHolderName(user != null ? user.getName() : null);
|
|
|
+ cert.setIssuedAt(now);
|
|
|
+ trainCertificateMapper.insert(cert);
|
|
|
+
|
|
|
+ result.put("certNo", certNo);
|
|
|
+ result.put("issuedAt", now);
|
|
|
+ result.put("level", level);
|
|
|
+ result.put("year", year);
|
|
|
+ result.put("seq", seq);
|
|
|
+
|
|
|
+ ensureAuditLog(uid, "cert_issued", "领取完课证书 " + certNo);
|
|
|
+ }
|
|
|
} else {
|
|
|
result.put("certNo", null);
|
|
|
}
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 证书编号查验(公开接口复用):查得返回 {valid:true, certNo, level, year, seq, holderName(脱敏), issuedAt},
|
|
|
+ * 查无返回 {valid:false}。异常入参(null/空串)视为查无。
|
|
|
+ */
|
|
|
+ public Map<String, Object> verifyCertNo(String certNo) {
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ if (certNo == null || certNo.trim().isEmpty()) {
|
|
|
+ data.put("valid", false);
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+ TrainCertificate cert = trainCertificateMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<TrainCertificate>()
|
|
|
+ .eq(TrainCertificate::getCertNo, certNo.trim())
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ if (cert == null) {
|
|
|
+ data.put("valid", false);
|
|
|
+ } else {
|
|
|
+ data.put("valid", true);
|
|
|
+ data.put("certNo", cert.getCertNo());
|
|
|
+ data.put("level", cert.getLevel());
|
|
|
+ data.put("year", cert.getYear());
|
|
|
+ data.put("seq", cert.getSeq());
|
|
|
+ data.put("holderName", maskName(cert.getHolderName()));
|
|
|
+ data.put("issuedAt", cert.getIssuedAt());
|
|
|
+ }
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 姓名脱敏:仅保留首字 + *(如 张*、欧*),空值返回 * */
|
|
|
+ private String maskName(String name) {
|
|
|
+ if (name == null || name.isEmpty()) {
|
|
|
+ return "*";
|
|
|
+ }
|
|
|
+ return name.substring(0, 1) + "*";
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 离场验收 6 项逐项校验(供证书领取与大屏完课进度复用)。
|
|
|
* 返回:[{key,label,ok}]
|
|
|
@@ -121,6 +202,25 @@ public class CertService {
|
|
|
return items;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * 根据 user → train_class → train_course 解析课程等级。
|
|
|
+ * 任一环节缺失时默认 L0。
|
|
|
+ */
|
|
|
+ private String resolveLevel(TrainUser user) {
|
|
|
+ if (user == null || user.getClassId() == null) {
|
|
|
+ return "L0";
|
|
|
+ }
|
|
|
+ TrainClass clazz = trainClassMapper.selectById(user.getClassId());
|
|
|
+ if (clazz == null || clazz.getCourseId() == null) {
|
|
|
+ return "L0";
|
|
|
+ }
|
|
|
+ TrainCourse course = trainCourseMapper.selectById(clazz.getCourseId());
|
|
|
+ if (course == null || course.getLevel() == null || course.getLevel().isEmpty()) {
|
|
|
+ return "L0";
|
|
|
+ }
|
|
|
+ return course.getLevel();
|
|
|
+ }
|
|
|
+
|
|
|
private Map<String, Object> item(String key, String label, boolean ok) {
|
|
|
Map<String, Object> m = new HashMap<>();
|
|
|
m.put("key", key);
|
|
|
@@ -129,21 +229,6 @@ public class CertService {
|
|
|
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(
|
|
|
@@ -162,4 +247,4 @@ public class CertService {
|
|
|
entry.setTs(new Date());
|
|
|
trainAuditLogMapper.insert(entry);
|
|
|
}
|
|
|
-}
|
|
|
+}
|