8 小时以前 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
230
231
232
233
234
235
236
237
238
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<QcReportTemplateVersionDO> getVersionPage(QcReportTemplateVersionPageReqVO pageReqVO) {
        return versionMapper.selectPage(pageReqVO);
    }
 
    @Override
    public List<QcReportTemplateVersionDO> getVersionListByTemplateId(Long templateId) {
        return versionMapper.selectListByTemplateId(templateId);
    }
 
    /**
     * 校验设计器提交的 Schema 结构。Schema 允许为空(先建版本后设计),
     * 但一旦提供就必须带 schemaVersion,避免存入无法识别的内容
     */
    /**
     * 校验模板 Schema,不合格的直接退回,不让它落库。
     * <p>
     * 规则必须在这里拦:规则写错了要到出件时才炸,用户看到的是「报告出不来」,
     * 却不知道是模板里哪条规则写坏的。存的时候就说清楚,改起来才有着落。
     */
    private void validateSchema(ReportTemplateSchema schema) {
        if (schema == null) {
            return;
        }
        if (StrUtil.isBlank(schema.getSchemaVersion())) {
            throw exception(TEMPLATE_VERSION_SCHEMA_INVALID);
        }
        List<String> 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<String> 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);
    }
 
}