9 小时以前 9bad721754fe8bbe2e5f459d0706e0fefac569f3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
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);
    }
 
}