package cn.iocoder.yudao.module.qcreport.service.version; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.qcreport.controller.admin.version.vo.QcReportTemplateVersionPageReqVO; import cn.iocoder.yudao.module.qcreport.controller.admin.version.vo.QcReportTemplateVersionSaveReqVO; import cn.iocoder.yudao.module.qcreport.controller.admin.version.vo.QcReportTemplateVersionUpdateReqVO; import cn.iocoder.yudao.module.qcreport.dal.dataobject.template.QcReportTemplateDO; import cn.iocoder.yudao.module.qcreport.dal.dataobject.version.QcReportTemplateVersionDO; import cn.iocoder.yudao.module.qcreport.dal.dataobject.version.ReportTemplateSchema; import cn.iocoder.yudao.module.qcreport.dal.mysql.version.QcReportTemplateVersionMapper; import cn.iocoder.yudao.module.qcreport.engine.QualityReportEngine; import cn.iocoder.yudao.module.qcreport.engine.render.CanvasSafety; import cn.iocoder.yudao.module.qcreport.service.template.QcReportTemplateService; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.qcreport.enums.QcReportEnums.TemplateStatusEnum.ENABLE; import static cn.iocoder.yudao.module.qcreport.enums.QcReportEnums.VersionStatusEnum.*; /** * 智能质检报告模板版本 Service 实现类 */ @Service @Validated public class QcReportTemplateVersionServiceImpl implements QcReportTemplateVersionService { private static final Pattern VERSION_PATTERN = Pattern.compile("^v(\\d+)(?:\\.(\\d+))*$"); @Resource private QcReportTemplateVersionMapper versionMapper; @Resource private QcReportTemplateService templateService; @Override public Long createVersion(QcReportTemplateVersionSaveReqVO createReqVO) { QcReportTemplateDO template = templateService.validateTemplateExists(createReqVO.getTemplateId()); if (!ENABLE.getStatus().equals(template.getStatus())) { throw exception(TEMPLATE_STATUS_DISABLED); } // 未指定版本号时,按最新版本自动递增(v1.0 -> v1.1) String version = StrUtil.blankToDefault(createReqVO.getVersion(), null); if (version == null) { QcReportTemplateVersionDO latest = versionMapper.selectLatestByTemplateId(createReqVO.getTemplateId()); version = nextVersion(latest == null ? null : latest.getVersion()); } validateVersionUnique(createReqVO.getTemplateId(), version, null); validateSchema(createReqVO.getSchema()); QcReportTemplateVersionDO createObj = BeanUtils.toBean(createReqVO, QcReportTemplateVersionDO.class); createObj.setVersion(version); createObj.setStatus(DRAFT.getStatus()); versionMapper.insert(createObj); return createObj.getId(); } @Override public void updateVersion(QcReportTemplateVersionUpdateReqVO updateReqVO) { QcReportTemplateVersionDO version = validateVersionExists(updateReqVO.getId()); // 已发布的版本不允许修改,只能新建版本,保证历史报告可复现 if (PUBLISHED.getStatus().equals(version.getStatus())) { throw exception(TEMPLATE_VERSION_PUBLISHED_CANNOT_MODIFY); } validateSchema(updateReqVO.getSchema()); QcReportTemplateVersionDO updateObj = BeanUtils.toBean(updateReqVO, QcReportTemplateVersionDO.class); versionMapper.updateById(updateObj); } @Override public void deleteVersion(Long id) { QcReportTemplateVersionDO version = validateVersionExists(id); if (!DRAFT.getStatus().equals(version.getStatus())) { throw exception(TEMPLATE_VERSION_PUBLISHED_CANNOT_MODIFY); } versionMapper.deleteById(id); } @Override @Transactional(rollbackFor = Exception.class) public void publishVersion(Long id) { QcReportTemplateVersionDO version = validateVersionExists(id); if (version.getSchema() == null) { throw exception(TEMPLATE_VERSION_SCHEMA_EMPTY); } QcReportTemplateVersionDO updateObj = new QcReportTemplateVersionDO(); updateObj.setId(id); updateObj.setStatus(PUBLISHED.getStatus()); versionMapper.updateById(updateObj); // 模板的当前版本指向刚发布的版本 templateService.updateCurrentVersion(version.getTemplateId(), version.getVersion()); } @Override public void rollbackVersion(Long id) { QcReportTemplateVersionDO version = validateVersionExists(id); if (!PUBLISHED.getStatus().equals(version.getStatus())) { throw exception(TEMPLATE_VERSION_NOT_PUBLISHED); } // 只切换模板的当前版本号,不改动任何版本内容 templateService.updateCurrentVersion(version.getTemplateId(), version.getVersion()); } @Override public void disableVersion(Long id) { QcReportTemplateVersionDO version = validateVersionExists(id); // 停用「当前版本」会造出一份自相矛盾的模板:template.current_version 还指着它,它却已经不可用, // 于是这个模板自此出不了报告(到出件那一刻才报 1_070_101_003),而用户不会想到起因是刚才那次停用。 // 这里直接拒绝,而不是顺手把 current_version 改掉——「改哪个版本」是用户的决定, // 不该由停用这个动作悄悄代劳;要整个模板停下来,另有「停用模板」这个入口。 QcReportTemplateDO template = templateService.getTemplate(version.getTemplateId()); if (template != null && version.getVersion().equals(template.getCurrentVersion())) { throw exception(TEMPLATE_VERSION_CURRENT_CANNOT_DISABLE, version.getVersion(), template.getTemplateName()); } QcReportTemplateVersionDO updateObj = new QcReportTemplateVersionDO(); updateObj.setId(id); updateObj.setStatus(DISABLED.getStatus()); versionMapper.updateById(updateObj); } @Override public QcReportTemplateVersionDO validateVersionExists(Long id) { QcReportTemplateVersionDO version = versionMapper.selectById(id); if (version == null) { throw exception(TEMPLATE_VERSION_NOT_EXISTS); } return version; } @Override public QcReportTemplateVersionDO getVersion(Long id) { return versionMapper.selectById(id); } @Override public QcReportTemplateVersionDO getPublishedVersion(Long templateId, String version) { QcReportTemplateVersionDO versionDO = versionMapper.selectByTemplateIdAndVersion(templateId, version); if (versionDO == null) { throw exception(TEMPLATE_VERSION_NOT_EXISTS); } if (!PUBLISHED.getStatus().equals(versionDO.getStatus())) { throw exception(TEMPLATE_VERSION_NOT_PUBLISHED); } return versionDO; } @Override public QcReportTemplateVersionDO getVersionByTemplateIdAndVersion(Long templateId, String version) { return versionMapper.selectByTemplateIdAndVersion(templateId, version); } @Override public PageResult getVersionPage(QcReportTemplateVersionPageReqVO pageReqVO) { return versionMapper.selectPage(pageReqVO); } @Override public List getVersionListByTemplateId(Long templateId) { return versionMapper.selectListByTemplateId(templateId); } /** * 校验设计器提交的 Schema 结构。Schema 允许为空(先建版本后设计), * 但一旦提供就必须带 schemaVersion,避免存入无法识别的内容 */ /** * 校验模板 Schema,不合格的直接退回,不让它落库。 *

* 规则必须在这里拦:规则写错了要到出件时才炸,用户看到的是「报告出不来」, * 却不知道是模板里哪条规则写坏的。存的时候就说清楚,改起来才有着落。 */ private void validateSchema(ReportTemplateSchema schema) { if (schema == null) { return; } if (StrUtil.isBlank(schema.getSchemaVersion())) { throw exception(TEMPLATE_VERSION_SCHEMA_INVALID); } List ruleErrors = QualityReportEngine.validateRules(schema); if (!ruleErrors.isEmpty()) { // 逐条列出具体规则与原因:只报「规则不合法」等于让用户自己去猜是哪条 throw new ServiceException(TEMPLATE_VERSION_RULE_INVALID.getCode(), TEMPLATE_VERSION_RULE_INVALID.getMsg() + ":" + String.join(";", ruleErrors)); } // 画布里有渲染时会原样拼进产物的内容(标签名、属性名、整段 CSS),在这里卡住; // 位置放在渲染侧就动了 HtmlRenderer,会让对拍产物与存量报告的逐字复现一起变,见 CanvasSafety 类注释 List canvasProblems = CanvasSafety.validate(schema.getGrapes()); if (!canvasProblems.isEmpty()) { throw new ServiceException(TEMPLATE_VERSION_CANVAS_UNSAFE.getCode(), StrUtil.format(TEMPLATE_VERSION_CANVAS_UNSAFE.getMsg(), String.join(";", canvasProblems))); } } private void validateVersionUnique(Long templateId, String version, Long id) { QcReportTemplateVersionDO exists = versionMapper.selectByTemplateIdAndVersion(templateId, version); if (exists != null && !exists.getId().equals(id)) { throw exception(TEMPLATE_VERSION_DUPLICATE); } } /** * 计算下一个版本号:v1.0 -> v1.1;无历史或格式无法识别时从 v1.0 开始 */ private String nextVersion(String currentVersion) { if (StrUtil.isBlank(currentVersion)) { return "v1.0"; } Matcher matcher = VERSION_PATTERN.matcher(currentVersion); if (!matcher.matches()) { return "v1.0"; } // 取 "v" 之后的第 1 段作为主版本,第 2 段作为次版本 String[] segments = currentVersion.substring(1).split("\\."); int major = Integer.parseInt(segments[0]); int minor = segments.length > 1 ? Integer.parseInt(segments[1]) : 0; return "v" + major + "." + (minor + 1); } }