package cn.iocoder.yudao.module.qcreport.service.template; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.qcreport.controller.admin.template.vo.QcReportTemplatePageReqVO; import cn.iocoder.yudao.module.qcreport.controller.admin.template.vo.QcReportTemplateSaveReqVO; 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.mysql.template.QcReportTemplateMapper; import cn.iocoder.yudao.module.qcreport.dal.mysql.version.QcReportTemplateVersionMapper; import cn.iocoder.yudao.module.qcreport.engine.QualityReportEngine; import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi; import cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum; import jakarta.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.TEMPLATE_CODE_DUPLICATE; import static cn.iocoder.yudao.module.qcreport.enums.ErrorCodeConstants.TEMPLATE_NOT_EXISTS; import static cn.iocoder.yudao.module.qcreport.enums.QcReportEnums.TemplateStatusEnum.ENABLE; import static cn.iocoder.yudao.module.qcreport.enums.QcReportEnums.VersionStatusEnum.DRAFT; import static cn.iocoder.yudao.module.qcreport.enums.QcReportEnums.VersionStatusEnum.PUBLISHED; /** * 智能质检报告模板 Service 实现类 */ @Service @Validated public class QcReportTemplateServiceImpl implements QcReportTemplateService { /** * 复制模板时,编码冲突的重试上限 */ private static final int COPY_CODE_MAX_RETRY = 50; @Resource private QcReportTemplateMapper templateMapper; @Resource private QcReportTemplateVersionMapper versionMapper; @Resource private StorageAttachmentApi storageAttachmentApi; @Override public Long createTemplate(QcReportTemplateSaveReqVO createReqVO) { validateTemplateCodeUnique(null, createReqVO.getTemplateCode()); QcReportTemplateDO template = BeanUtils.toBean(createReqVO, QcReportTemplateDO.class); if (template.getStatus() == null) { template.setStatus(ENABLE.getStatus()); } templateMapper.insert(template); return template.getId(); } @Override public void updateTemplate(QcReportTemplateSaveReqVO updateReqVO) { validateTemplateExists(updateReqVO.getId()); validateTemplateCodeUnique(updateReqVO.getId(), updateReqVO.getTemplateCode()); QcReportTemplateDO updateObj = BeanUtils.toBean(updateReqVO, QcReportTemplateDO.class); templateMapper.updateById(updateObj); } @Override @Transactional(rollbackFor = Exception.class) public void deleteTemplate(Long id) { validateTemplateExists(id); // 先清附件再删模板:模板一没,AI 导入时上传的源文件就成了查不到主人的孤儿 // (附件行 + 磁盘文件双份)。顺序反过来的话,清理失败就再也没机会补救了—— // 模板已不存在,无从知道该删谁的附件。与 deleteInstance 同构。 storageAttachmentApi.deleteAttachmentsByRecord(StorageRecordTypeEnum.QC_REPORT_TEMPLATE.getType(), id); // 逻辑删除模板 templateMapper.deleteById(id); // 级联逻辑删除其下所有版本 versionMapper.delete(new LambdaQueryWrapperX() .eq(QcReportTemplateVersionDO::getTemplateId, id)); } @Override @Transactional(rollbackFor = Exception.class) public Long copyTemplate(Long id) { QcReportTemplateDO source = validateTemplateExists(id); // 复制模板元数据,编码与名称需避让已存在的记录 QcReportTemplateDO target = BeanUtils.toBean(source, QcReportTemplateDO.class); target.setId(null); target.setTemplateCode(generateCopyCode(source.getTemplateCode())); target.setTemplateName(source.getTemplateName() + "(副本)"); target.setStatus(ENABLE.getStatus()); target.setCurrentVersion(null); resetAuditFields(target); templateMapper.insert(target); // 把源模板的最新版本 Schema 复制为副本的 v1.0 草稿,避免复制出来是个空模板 QcReportTemplateVersionDO latestVersion = versionMapper.selectLatestByTemplateId(id); if (latestVersion != null) { QcReportTemplateVersionDO copiedVersion = BeanUtils.toBean(latestVersion, QcReportTemplateVersionDO.class); copiedVersion.setId(null); copiedVersion.setTemplateId(target.getId()); copiedVersion.setVersion("v1.0"); copiedVersion.setStatus(DRAFT.getStatus()); copiedVersion.setSchema(latestVersion.getSchema()); resetAuditFields(copiedVersion); versionMapper.insert(copiedVersion); } return target.getId(); } @Override public QcReportTemplateDO validateTemplateExists(Long id) { QcReportTemplateDO template = templateMapper.selectById(id); if (template == null) { throw exception(TEMPLATE_NOT_EXISTS); } return template; } @Override public QcReportTemplateDO getTemplate(Long id) { return templateMapper.selectById(id); } @Override public PageResult getTemplatePage(QcReportTemplatePageReqVO pageReqVO) { return templateMapper.selectPage(pageReqVO); } @Override public List getTemplateList(Collection ids) { if (CollUtil.isEmpty(ids)) { return Collections.emptyList(); } return templateMapper.selectByIds(ids); } @Override public List getSelectableTemplates(String reportType) { // 模板层过滤:报告类型一致 + 模板已启用 List templates = templateMapper.selectListByReportTypeAndStatus( reportType, ENABLE.getStatus()); if (CollUtil.isEmpty(templates)) { return Collections.emptyList(); } // 版本层过滤:current_version 指向的那个版本必须「已发布」,且画布真的能渲染出东西。 // 两件事都要在这里挡:停用版本不会清空 current_version,用户一旦选中,出件必然报 1_070_101_003; // 而「打开过设计器但一个组件都没放」的版本画布非空却没有根组件,出件不报错、直接给一张白纸 PDF。 // 两种都是已知会失败的组合,不该摆到出报告弹窗里让用户去踩。 // // 这一层不额外查库:下面这条查询本来就要把候选版本整行读出来(含 schema_json), // 用它比 hasCanvas 是白捡的;若改用 exists 子查询,反而要多一次往返。 List templateIds = templates.stream().map(QcReportTemplateDO::getId).collect(Collectors.toList()); Map> publishedVersions = versionMapper .selectListByTemplateIdsAndStatus(templateIds, PUBLISHED.getStatus()) .stream() .collect(Collectors.groupingBy(QcReportTemplateVersionDO::getTemplateId, Collectors.toMap(QcReportTemplateVersionDO::getVersion, Function.identity()))); return templates.stream() .filter(template -> { QcReportTemplateVersionDO current = StrUtil.isBlank(template.getCurrentVersion()) ? null : publishedVersions.getOrDefault(template.getId(), Collections.emptyMap()) .get(template.getCurrentVersion()); return current != null && QualityReportEngine.hasCanvas(current.getSchema()); }) .collect(Collectors.toList()); } @Override public void updateCurrentVersion(Long id, String version) { QcReportTemplateDO updateObj = new QcReportTemplateDO(); updateObj.setId(id); updateObj.setCurrentVersion(version); templateMapper.updateById(updateObj); } private void validateTemplateCodeUnique(Long id, String templateCode) { QcReportTemplateDO template = templateMapper.selectByCode(templateCode); if (template == null) { return; } if (ObjUtil.notEqual(template.getId(), id)) { throw exception(TEMPLATE_CODE_DUPLICATE); } } /** * 生成副本编码:源编码 + _COPY,若已被占用则追加序号 */ private String generateCopyCode(String sourceCode) { String baseCode = sourceCode + "_COPY"; for (int i = 0; i < COPY_CODE_MAX_RETRY; i++) { String candidate = i == 0 ? baseCode : baseCode + (i + 1); if (templateMapper.selectByCode(candidate) == null) { return candidate; } } // 极端情况下仍冲突,用时间戳兜底,避免复制功能直接不可用 return baseCode + System.currentTimeMillis(); } /** * 清空审计字段,让框架在插入时按新记录重新填充,避免副本沿用源记录的创建人/创建时间 */ private void resetAuditFields(BaseDO baseDO) { baseDO.setCreator(null); baseDO.setCreateTime(null); baseDO.setUpdater(null); baseDO.setUpdateTime(null); } }