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<QcReportTemplateVersionDO>()
|
.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<QcReportTemplateDO> getTemplatePage(QcReportTemplatePageReqVO pageReqVO) {
|
return templateMapper.selectPage(pageReqVO);
|
}
|
|
@Override
|
public List<QcReportTemplateDO> getTemplateList(Collection<Long> ids) {
|
if (CollUtil.isEmpty(ids)) {
|
return Collections.emptyList();
|
}
|
return templateMapper.selectByIds(ids);
|
}
|
|
@Override
|
public List<QcReportTemplateDO> getSelectableTemplates(String reportType) {
|
// 模板层过滤:报告类型一致 + 模板已启用
|
List<QcReportTemplateDO> 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<Long> templateIds = templates.stream().map(QcReportTemplateDO::getId).collect(Collectors.toList());
|
Map<Long, Map<String, QcReportTemplateVersionDO>> 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);
|
}
|
|
}
|