feat: 报表模板同步功能

This commit is contained in:
2026-06-04 09:54:10 +08:00
parent e76b3a35aa
commit 5b87e2f7ca
5 changed files with 337 additions and 86 deletions

View File

@@ -1,7 +1,9 @@
package com.hotwj.platform.reportStatistics.controller;
import com.hotwj.platform.reportStatistics.domain.bo.SysReportTemplateBo;
import com.hotwj.platform.reportStatistics.domain.bo.SysReportTemplateSyncBo;
import com.hotwj.platform.reportStatistics.domain.vo.SysReportTemplateVo;
import com.hotwj.platform.reportStatistics.domain.vo.SysReportTemplateSyncCompareVo;
import com.hotwj.platform.reportStatistics.service.ISysReportTemplateService;
import com.hotwj.platform.reportStatistics.service.ReportRenderService;
import jakarta.servlet.http.HttpServletResponse;
@@ -97,6 +99,26 @@ public class SysReportTemplateController extends BaseController {
return R.ok();
}
/**
* 查询模板同步差异
*/
//@SaCheckPermission("report:template:sync")
@GetMapping("/sync/diff")
public R<List<SysReportTemplateSyncCompareVo>> syncDiff() {
return R.ok(sysReportTemplateService.querySyncDiffList());
}
/**
* 按选中项同步模板
*/
//@SaCheckPermission("report:template:sync")
@Log(title = "按项同步模版", businessType = BusinessType.UPDATE)
@PostMapping("/sync/selective")
public R<Void> selectiveSync(@Validated @RequestBody SysReportTemplateSyncBo bo) {
sysReportTemplateService.syncTemplatesFromClasspath(bo.getTemplateCodes());
return R.ok();
}
/**
* 删除报表模板
*/

View File

@@ -0,0 +1,19 @@
package com.hotwj.platform.reportStatistics.domain.bo;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
/**
* 报表模板按项同步请求
*/
@Data
public class SysReportTemplateSyncBo {
/**
* 需要同步的模板编码集合
*/
@NotEmpty(message = "请选择需要同步的模板")
private List<String> templateCodes;
}

View File

@@ -0,0 +1,45 @@
package com.hotwj.platform.reportStatistics.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 报表模板同步差异视图对象
*/
@Data
public class SysReportTemplateSyncCompareVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private String templateCode;
private Long dbId;
private String localTemplateName;
private String dbTemplateName;
private String localContent;
private String dbContent;
private String localMockData;
private String dbMockData;
private String localRemark;
private String dbRemark;
private Boolean existsInLocal;
private Boolean existsInDb;
private Boolean syncable;
private List<String> diffTypes;
}

View File

@@ -2,6 +2,7 @@ package com.hotwj.platform.reportStatistics.service;
import com.hotwj.platform.reportStatistics.domain.bo.SysReportTemplateBo;
import com.hotwj.platform.reportStatistics.domain.vo.SysReportTemplateVo;
import com.hotwj.platform.reportStatistics.domain.vo.SysReportTemplateSyncCompareVo;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
@@ -56,6 +57,16 @@ public interface ISysReportTemplateService {
*/
void syncTemplatesFromClasspath();
/**
* 查询本地模板与数据库模板的差异
*/
List<SysReportTemplateSyncCompareVo> querySyncDiffList();
/**
* 按模板编码同步本地模板到数据库
*/
void syncTemplatesFromClasspath(Collection<String> templateCodes);
/**
* 根据模板编码查找对应的 Provider
*/

View File

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hotwj.platform.reportStatistics.domain.SysReportTemplate;
import com.hotwj.platform.reportStatistics.domain.bo.SysReportTemplateBo;
import com.hotwj.platform.reportStatistics.domain.vo.SysReportTemplateSyncCompareVo;
import com.hotwj.platform.reportStatistics.domain.vo.SysReportTemplateVo;
import com.hotwj.platform.reportStatistics.mapper.SysReportTemplateMapper;
import com.hotwj.platform.reportStatistics.service.IReportDataProvider;
@@ -16,6 +17,7 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.springframework.core.io.ClassPathResource;
@@ -27,9 +29,15 @@ import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 报表模板Service业务层处理
@@ -42,6 +50,9 @@ import java.util.Map;
@Service
public class SysReportTemplateServiceImpl implements ISysReportTemplateService {
private static final String CLASSPATH_SCAN_REMARK = "Synced from classpath scan";
private static final String PROVIDER_REMARK_PREFIX = "Synced from Provider: ";
private final SysReportTemplateMapper baseMapper;
private final List<IReportDataProvider> reportDataProviders;
@@ -153,114 +164,257 @@ public class SysReportTemplateServiceImpl implements ISysReportTemplateService {
@Override
@Transactional(rollbackFor = Exception.class)
public void syncTemplatesFromClasspath() {
// 1. Sync from Providers (Source of truth for Title and MockData)
syncTemplatesFromClasspath(null);
}
@Override
public List<SysReportTemplateSyncCompareVo> querySyncDiffList() {
LinkedHashMap<String, LocalTemplateSnapshot> localTemplates = loadLocalTemplateSnapshots();
Map<String, SysReportTemplateVo> dbTemplates = queryExistingTemplates(null);
Set<String> allCodes = new LinkedHashSet<>();
allCodes.addAll(localTemplates.keySet());
allCodes.addAll(dbTemplates.keySet());
return allCodes.stream()
.map(code -> buildCompareVo(code, localTemplates.get(code), dbTemplates.get(code)))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
@Override
@Transactional(rollbackFor = Exception.class)
public void syncTemplatesFromClasspath(Collection<String> templateCodes) {
LinkedHashMap<String, LocalTemplateSnapshot> localTemplates = loadLocalTemplateSnapshots();
Set<String> targetCodes = normalizeTemplateCodes(templateCodes);
if (targetCodes != null && targetCodes.isEmpty()) {
throw new ServiceException("请选择需要同步的模板");
}
if (targetCodes != null) {
List<String> missingCodes = targetCodes.stream()
.filter(code -> !localTemplates.containsKey(code))
.collect(Collectors.toList());
if (!missingCodes.isEmpty()) {
throw new ServiceException("以下模板在本地不存在,无法同步: " + String.join(", ", missingCodes));
}
}
Map<String, SysReportTemplateVo> dbTemplates = queryExistingTemplates(targetCodes);
for (LocalTemplateSnapshot snapshot : localTemplates.values()) {
if (targetCodes != null && !targetCodes.contains(snapshot.getTemplateCode())) {
continue;
}
SysReportTemplateVo existing = dbTemplates.get(snapshot.getTemplateCode());
if (existing == null) {
SysReportTemplate insert = new SysReportTemplate();
insert.setTemplateCode(snapshot.getTemplateCode());
insert.setTemplateName(snapshot.getTemplateName());
insert.setContent(snapshot.getContent());
insert.setMockData(snapshot.getMockData());
insert.setVersion(1);
insert.setRemark(snapshot.getRemark());
baseMapper.insert(insert);
log.info("Inserted template from local source: {}", snapshot.getTemplateCode());
continue;
}
if (!hasDiff(snapshot, existing)) {
continue;
}
SysReportTemplate update = new SysReportTemplate();
update.setId(existing.getId());
update.setTemplateCode(snapshot.getTemplateCode());
update.setTemplateName(snapshot.getTemplateName());
update.setContent(snapshot.getContent());
update.setMockData(snapshot.getMockData());
update.setRemark(snapshot.getRemark());
update.setVersion(existing.getVersion() == null ? 1 : existing.getVersion() + 1);
baseMapper.updateById(update);
log.info("Updated template from local source: {}", snapshot.getTemplateCode());
}
}
private SysReportTemplateSyncCompareVo buildCompareVo(String code, LocalTemplateSnapshot local, SysReportTemplateVo db) {
List<String> diffTypes = buildDiffTypes(local, db);
if (diffTypes.isEmpty()) {
return null;
}
SysReportTemplateSyncCompareVo vo = new SysReportTemplateSyncCompareVo();
vo.setTemplateCode(code);
vo.setDbId(db == null ? null : db.getId());
vo.setLocalTemplateName(local == null ? "" : local.getTemplateName());
vo.setDbTemplateName(db == null ? "" : db.getTemplateName());
vo.setLocalContent(local == null ? "" : local.getContent());
vo.setDbContent(db == null ? "" : normalizeText(db.getContent()));
vo.setLocalMockData(toJsonText(local == null ? null : local.getMockData()));
vo.setDbMockData(toJsonText(db == null ? null : db.getMockData()));
vo.setLocalRemark(local == null ? "" : local.getRemark());
vo.setDbRemark(db == null ? "" : normalizeText(db.getRemark()));
vo.setExistsInLocal(local != null);
vo.setExistsInDb(db != null);
vo.setSyncable(local != null);
vo.setDiffTypes(diffTypes);
return vo;
}
private boolean hasDiff(LocalTemplateSnapshot local, SysReportTemplateVo db) {
return !buildDiffTypes(local, db).isEmpty();
}
private List<String> buildDiffTypes(LocalTemplateSnapshot local, SysReportTemplateVo db) {
List<String> diffTypes = new ArrayList<>();
if (local == null && db != null) {
diffTypes.add("仅数据库");
return diffTypes;
}
if (local != null && db == null) {
diffTypes.add("仅本地");
return diffTypes;
}
if (local == null) {
return diffTypes;
}
if (!Objects.equals(normalizeText(local.getTemplateName()), normalizeText(db.getTemplateName()))) {
diffTypes.add("名称");
}
if (!Objects.equals(normalizeText(local.getContent()), normalizeText(db.getContent()))) {
diffTypes.add("内容");
}
if (!Objects.equals(toJsonText(local.getMockData()), toJsonText(db.getMockData()))) {
diffTypes.add("模拟数据");
}
if (!Objects.equals(normalizeText(local.getRemark()), normalizeText(db.getRemark()))) {
diffTypes.add("备注");
}
return diffTypes;
}
private Map<String, SysReportTemplateVo> queryExistingTemplates(Set<String> templateCodes) {
LambdaQueryWrapper<SysReportTemplate> queryWrapper = Wrappers.lambdaQuery();
if (templateCodes != null && !templateCodes.isEmpty()) {
queryWrapper.in(SysReportTemplate::getTemplateCode, templateCodes);
}
return baseMapper.selectVoList(queryWrapper).stream()
.collect(Collectors.toMap(SysReportTemplateVo::getTemplateCode, item -> item, (left, right) -> left, LinkedHashMap::new));
}
private LinkedHashMap<String, LocalTemplateSnapshot> loadLocalTemplateSnapshots() {
LinkedHashMap<String, LocalTemplateSnapshot> templateMap = new LinkedHashMap<>();
if (reportDataProviders != null) {
for (IReportDataProvider provider : reportDataProviders) {
try {
String code = provider.getTemplateCode();
if (StringUtils.isBlank(code)) continue;
SysReportTemplateVo existing = queryByCode(code);
// Read content
String content = null;
try {
ClassPathResource resource = new ClassPathResource(code);
if (resource.exists()) {
content = IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8);
}
} catch (Exception e) {
log.warn("Template file not found for provider: {}", provider.getClass().getSimpleName());
if (StringUtils.isBlank(code)) {
continue;
}
if (existing == null) {
if (StringUtils.isBlank(content)) {
log.warn("Skipping provider {} because template file {} not found", provider.getClass().getSimpleName(), code);
continue;
}
SysReportTemplate template = new SysReportTemplate();
template.setTemplateCode(code);
template.setTemplateName(provider.getReportTitle());
template.setContent(content);
template.setMockData(provider.getMockData());
template.setVersion(1);
template.setRemark("Synced from Provider: " + provider.getClass().getSimpleName());
baseMapper.insert(template);
log.info("Inserted template from provider: {}", code);
} else {
// Update metadata
SysReportTemplate update = new SysReportTemplate();
update.setId(existing.getId());
boolean changed = false;
// Sync Title
String title = provider.getReportTitle();
if (StringUtils.isNotBlank(title) && !title.equals(existing.getTemplateName())) {
update.setTemplateName(title);
changed = true;
}
// Sync MockData if provided
Map<String, Object> mockData = provider.getMockData();
if (mockData != null && !mockData.isEmpty()) {
update.setMockData(mockData);
changed = true;
}
// Optional: Sync content if it was empty in DB (unlikely)
if (StringUtils.isBlank(existing.getContent()) && StringUtils.isNotBlank(content)) {
update.setContent(content);
changed = true;
}
if (changed) {
baseMapper.updateById(update);
log.info("Updated template metadata from provider: {}", code);
}
String content = readTemplateContent(code);
if (StringUtils.isBlank(content)) {
log.warn("Skipping provider {} because template file {} not found", provider.getClass().getSimpleName(), code);
continue;
}
LocalTemplateSnapshot snapshot = new LocalTemplateSnapshot();
snapshot.setTemplateCode(code);
snapshot.setTemplateName(provider.getReportTitle());
snapshot.setContent(content);
snapshot.setMockData(provider.getMockData());
snapshot.setRemark(PROVIDER_REMARK_PREFIX + provider.getClass().getSimpleName());
templateMap.put(code, snapshot);
} catch (Exception e) {
log.error("Error syncing provider template: " + provider.getClass().getSimpleName(), e);
log.error("Error loading provider template: {}", provider.getClass().getSimpleName(), e);
}
}
}
// 2. Scan remaining files (for layouts and fragments)
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
try {
// Find all .vm files in templates directory
Resource[] resources = resolver.getResources("classpath*:templates/**/*.vm");
for (Resource resource : resources) {
try {
String path = resource.getURL().getPath();
// Normalize path separators
path = path.replace("\\", "/");
int index = path.lastIndexOf("templates/");
if (index > -1) {
String code = path.substring(index); // Include "templates/" in the code
// Check if exists
SysReportTemplateVo exist = queryByCode(code);
if (exist == null) {
String content = IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8);
SysReportTemplate template = new SysReportTemplate();
template.setTemplateCode(code);
// Use filename as name
template.setTemplateName(FileUtil.mainName(resource.getFilename()));
template.setContent(content);
template.setVersion(1);
template.setRemark("Synced from classpath scan");
baseMapper.insert(template);
log.info("Synced template from scan: {}", code);
}
String code = extractTemplateCode(resource);
if (StringUtils.isBlank(code) || templateMap.containsKey(code)) {
continue;
}
LocalTemplateSnapshot snapshot = new LocalTemplateSnapshot();
snapshot.setTemplateCode(code);
snapshot.setTemplateName(FileUtil.mainName(resource.getFilename()));
snapshot.setContent(IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8));
snapshot.setRemark(CLASSPATH_SCAN_REMARK);
templateMap.put(code, snapshot);
} catch (Exception e) {
log.error("Error processing resource: " + resource, e);
log.error("Error processing resource: {}", resource, e);
}
}
} catch (IOException e) {
log.error("Failed to sync templates", e);
throw new ServiceException("同步模版失败");
log.error("Failed to load local templates", e);
throw new ServiceException("读取本地模板失败");
}
return templateMap;
}
private String extractTemplateCode(Resource resource) throws IOException {
String path = resource.getURL().getPath().replace("\\", "/");
int index = path.lastIndexOf("templates/");
if (index < 0) {
return null;
}
return path.substring(index);
}
private String readTemplateContent(String code) {
try {
ClassPathResource resource = new ClassPathResource(code);
if (!resource.exists()) {
return null;
}
return IoUtil.read(resource.getInputStream(), StandardCharsets.UTF_8);
} catch (Exception e) {
log.warn("Template file not found: {}", code, e);
return null;
}
}
private Set<String> normalizeTemplateCodes(Collection<String> templateCodes) {
if (templateCodes == null) {
return null;
}
return templateCodes.stream()
.filter(StringUtils::isNotBlank)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
private String normalizeText(String value) {
return value == null ? "" : value;
}
private String toJsonText(Object value) {
if (value == null) {
return "";
}
return JsonUtils.toJsonString(value);
}
@lombok.Data
private static class LocalTemplateSnapshot {
private String templateCode;
private String templateName;
private String content;
private Object mockData;
private String remark;
}
}