package jnpf.dmsService; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import jnpf.base.DictionaryDataApi; import jnpf.base.entity.DictionaryDataEntity; import jnpf.audit.sdk.annotation.AuditLog; import jnpf.dmsEntity.watermark.DmsWatermarkConfigEntity; import jnpf.dmsEntity.watermark.DmsWatermarkPositionEntity; import jnpf.dmsEntity.watermark.form.DmsWatermarkConfigForm; import jnpf.dmsEntity.watermark.form.DmsWatermarkPositionForm; import jnpf.dmsEntity.watermark.form.DmsWatermarkQuery; import jnpf.dmsEntity.watermark.vo.DmsWatermarkConfigVO; import jnpf.dmsMapper.DmsWatermarkConfigMapper; import jnpf.dmsMapper.DmsWatermarkPositionMapper; import jnpf.dmsPermission.DmsCurrentIdentity; import jnpf.exception.DataException; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeanUtils; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class DmsWatermarkConfigService { private static final String BUSINESS_TYPE_DICTIONARY_ID = "879384732964547653"; private static final String DUPLICATE_NAME_MESSAGE = "已存在同名水印,请更换名称"; private static final String DUPLICATE_NAME_CONSTRAINT = "uq_dms_watermark_configs_active_name"; private static final Set TYPES = new HashSet<>(Arrays.asList("TEXT", "IMAGE", "BARCODE")); private static final Set VERSION_SCOPES = new HashSet<>(Arrays.asList("ALL", "CURRENT")); private static final Set SCENES = new HashSet<>(Arrays.asList("DOWNLOAD", "PREVIEW", "PRINT")); private static final Set LAYOUTS = new HashSet<>(Arrays.asList("POSITION", "TILED")); private static final Set HORIZONTAL_ALIGNS = Set.of("LEFT", "CENTER", "RIGHT"); private static final Set VERTICAL_ALIGNS = Set.of("TOP", "MIDDLE", "BOTTOM"); private static final Set BARCODE_FORMATS = new HashSet<>(Arrays.asList("CODE_128", "QR_CODE")); private final DmsWatermarkConfigMapper configMapper; private final DmsWatermarkPositionMapper positionMapper; private final DmsCurrentIdentity identity; private final DictionaryDataApi dictionaryDataApi; @Transactional(readOnly = true) public List list(DmsWatermarkQuery query) throws DataException { DmsWatermarkQuery q = query == null ? new DmsWatermarkQuery() : query; if (q.getPageSize() > 200) throw new DataException("pageSize 不能超过 200"); LambdaQueryWrapper wrapper = baseQuery(); String keyword = trim(q.getKeyword()); if (keyword != null) wrapper.and(w -> w.like(DmsWatermarkConfigEntity::getName, keyword) .or().like(DmsWatermarkConfigEntity::getContentTemplate, keyword) .or().like(DmsWatermarkConfigEntity::getEnterpriseName, keyword)); if (StringUtils.hasText(q.getWatermarkType())) wrapper.eq(DmsWatermarkConfigEntity::getWatermarkType, q.getWatermarkType().trim().toUpperCase()); if (q.getEnabled() != null) wrapper.eq(DmsWatermarkConfigEntity::getEnabled, q.getEnabled()); if (StringUtils.hasText(q.getEnterpriseId())) wrapper.eq(DmsWatermarkConfigEntity::getEnterpriseId, q.getEnterpriseId().trim()); Page result = configMapper.selectPage( new Page<>(q.getCurrentPage(), q.getPageSize()), wrapper.orderByDesc(DmsWatermarkConfigEntity::getUpdatedAt)); List rows = result.getRecords().stream().map(row -> toVO(row, false)).collect(Collectors.toList()); q.setData(rows, result.getTotal()); return rows; } @Transactional(readOnly = true) public DmsWatermarkConfigVO detail(UUID id) throws DataException { return toVO(requireConfig(id), true); } @AuditLog(action = "CREATE", label = "新增水印配置", bizType = "WATERMARK_CONFIG", bizModule = "DMS", bizCodeExpr = "#result.id", targetTable = "dms_watermark_configs", targetIdExpr = "#result.id") @Transactional(rollbackFor = Exception.class) public DmsWatermarkConfigVO create(DmsWatermarkConfigForm form) throws DataException { Normalized value = normalize(form); requireUniqueName(value.row, null); Date now = new Date(); DmsWatermarkConfigEntity row = value.row; row.setId(configMapper.nextId()); row.setTenantId(identity.tenantId()); row.setRevision(1L); row.setCreatedBy(identity.userId()); row.setCreatedAt(now); row.setUpdatedBy(identity.userId()); row.setUpdatedAt(now); try { configMapper.insert(row); } catch (DuplicateKeyException ex) { throw duplicateNameOrOriginal(ex); } replacePositions(row, value.positions); replaceBusinessTypes(row, value.businessTypes); return toVO(row, true); } @AuditLog(action = "UPDATE", label = "编辑水印配置", bizType = "WATERMARK_CONFIG", bizModule = "DMS", bizCodeExpr = "#result.id", targetTable = "dms_watermark_configs", targetIdExpr = "#result.id") @Transactional(rollbackFor = Exception.class) public DmsWatermarkConfigVO update(UUID id, DmsWatermarkConfigForm form) throws DataException { DmsWatermarkConfigEntity existing = requireConfig(id); requireRevision(form == null ? null : form.getRevision(), existing); Normalized value = normalize(form); DmsWatermarkConfigEntity row = value.row; requireUniqueName(row, id); row.setUpdatedAt(new Date()); row.setUpdatedBy(identity.userId()); row.setRevision(existing.getRevision() + 1); int count; try { count = configMapper.update(row, new LambdaUpdateWrapper() .eq(DmsWatermarkConfigEntity::getId, id) .eq(DmsWatermarkConfigEntity::getTenantId, identity.tenantId()) .eq(DmsWatermarkConfigEntity::getRevision, existing.getRevision()) .isNull(DmsWatermarkConfigEntity::getDeletedAt)); } catch (DuplicateKeyException ex) { throw duplicateNameOrOriginal(ex); } if (count != 1) throw new DataException("水印配置已被其他用户修改,请刷新后重试"); row.setId(id); row.setTenantId(identity.tenantId()); positionMapper.delete(new LambdaQueryWrapper() .eq(DmsWatermarkPositionEntity::getTenantId, identity.tenantId()) .eq(DmsWatermarkPositionEntity::getConfigId, id)); replacePositions(row, value.positions); replaceBusinessTypes(row, value.businessTypes); return toVO(row, true); } @AuditLog(action = "WATERMARK_TOGGLE", label = "启停水印配置", bizType = "WATERMARK_CONFIG", bizModule = "DMS", bizCodeExpr = "#result.id", targetTable = "dms_watermark_configs", targetIdExpr = "#result.id") @Transactional(rollbackFor = Exception.class) public DmsWatermarkConfigVO setEnabled(UUID id, Long revision, Boolean enabled) throws DataException { if (enabled == null) throw new DataException("enabled 不能为空"); DmsWatermarkConfigEntity existing = requireConfig(id); requireRevision(revision, existing); DmsWatermarkConfigEntity update = new DmsWatermarkConfigEntity(); update.setEnabled(enabled); update.setRevision(existing.getRevision() + 1); update.setUpdatedBy(identity.userId()); update.setUpdatedAt(new Date()); int count = configMapper.update(update, versionWrapper(id, existing.getRevision())); if (count != 1) throw new DataException("水印配置已被其他用户修改,请刷新后重试"); return detail(id); } @AuditLog(action = "DELETE", label = "删除水印配置", bizType = "WATERMARK_CONFIG", bizModule = "DMS", bizCodeExpr = "#result.id", targetTable = "dms_watermark_configs", targetIdExpr = "#result.id") @Transactional(rollbackFor = Exception.class) public DmsWatermarkConfigVO delete(UUID id, Long revision) throws DataException { DmsWatermarkConfigEntity existing = requireConfig(id); requireRevision(revision, existing); DmsWatermarkConfigEntity update = new DmsWatermarkConfigEntity(); update.setDeletedAt(new Date()); update.setDeletedBy(identity.userId()); update.setUpdatedAt(new Date()); update.setUpdatedBy(identity.userId()); update.setRevision(existing.getRevision() + 1); int count = configMapper.update(update, versionWrapper(id, existing.getRevision())); if (count != 1) throw new DataException("水印配置已被其他用户修改,请刷新后重试"); existing.setRevision(existing.getRevision() + 1); return toVO(existing, false); } private LambdaUpdateWrapper versionWrapper(UUID id, Long revision) { return new LambdaUpdateWrapper() .eq(DmsWatermarkConfigEntity::getId, id) .eq(DmsWatermarkConfigEntity::getTenantId, identity.tenantId()) .eq(DmsWatermarkConfigEntity::getRevision, revision) .isNull(DmsWatermarkConfigEntity::getDeletedAt); } private LambdaQueryWrapper baseQuery() { return new LambdaQueryWrapper() .eq(DmsWatermarkConfigEntity::getTenantId, identity.tenantId()) .isNull(DmsWatermarkConfigEntity::getDeletedAt); } private void requireUniqueName(DmsWatermarkConfigEntity row, UUID excludedId) throws DataException { LambdaQueryWrapper query = baseQuery() .eq(DmsWatermarkConfigEntity::getEnterpriseId, row.getEnterpriseId()) .apply("lower(name) = lower({0})", row.getName()); if (excludedId != null) query.ne(DmsWatermarkConfigEntity::getId, excludedId); Long count = configMapper.selectCount(query); if (count != null && count > 0) throw new DataException(DUPLICATE_NAME_MESSAGE); } private DataException duplicateNameOrOriginal(DuplicateKeyException ex) throws DataException { if (ex.getMessage() != null && ex.getMessage().contains(DUPLICATE_NAME_CONSTRAINT)) { return new DataException(DUPLICATE_NAME_MESSAGE); } throw ex; } private DmsWatermarkConfigEntity requireConfig(UUID id) throws DataException { if (id == null) throw new DataException("水印配置ID不能为空"); DmsWatermarkConfigEntity row = configMapper.selectOne(baseQuery().eq(DmsWatermarkConfigEntity::getId, id)); if (row == null) throw new DataException("水印配置不存在"); return row; } private void requireRevision(Long revision, DmsWatermarkConfigEntity row) throws DataException { if (revision == null || !revision.equals(row.getRevision())) throw new DataException("水印配置已被其他用户修改,请刷新后重试"); } private Normalized normalize(DmsWatermarkConfigForm form) throws DataException { if (form == null) throw new DataException("配置内容不能为空"); DmsWatermarkConfigEntity row = new DmsWatermarkConfigEntity(); row.setName(required(form.getName(), 100, "水印名称")); row.setWatermarkType(enumText(form.getWatermarkType(), TYPES, "水印类型")); row.setVersionScope(enumText(form.getVersionScope(), VERSION_SCOPES, "适用版本")); List scenes = normalizeScenes(form.getLoadScenes()); row.setLoadDownload(scenes.contains("DOWNLOAD")); row.setLoadPreview(scenes.contains("PREVIEW")); row.setLoadPrint(scenes.contains("PRINT")); row.setEnabled(Boolean.TRUE.equals(form.getEnabled())); String enterpriseName = required(form.getEnterpriseName(), 100, "所属企业"); row.setEnterpriseId(enterpriseName); row.setEnterpriseName(enterpriseName); row.setContentTemplate(optional(form.getContentTemplate(), 2000, "水印内容")); row.setImageUrl(optional(form.getImageUrl(), 1000, "水印图片")); row.setBarcodeFormat(optional(form.getBarcodeFormat(), 30, "条码格式")); row.setShowBarcodeText("BARCODE".equals(row.getWatermarkType()) && Boolean.TRUE.equals(form.getShowBarcodeText())); if ("TEXT".equals(row.getWatermarkType())) row.setContentTemplate(required(row.getContentTemplate(), 2000, "水印内容")); if ("IMAGE".equals(row.getWatermarkType())) row.setImageUrl(required(row.getImageUrl(), 1000, "水印图片")); if ("BARCODE".equals(row.getWatermarkType())) { row.setContentTemplate(required(row.getContentTemplate(), 2000, "条码内容")); row.setBarcodeFormat(enumText(form.getBarcodeFormat(), BARCODE_FORMATS, "条码格式")); } String customTemplate = jnpf.dmsEntity.watermark.DmsWatermarkParameters.CUSTOM_BARCODE_TEMPLATE; if (row.getContentTemplate() != null && row.getContentTemplate().contains(customTemplate)) { if (!"BARCODE".equals(row.getWatermarkType()) || !customTemplate.equals(row.getContentTemplate())) { throw new DataException("自定义条码文本只能作为条码水印的完整内容来源"); } row.setShowBarcodeText(true); } row.setFontFamily(optional(form.getFontFamily(), 100, "字体")); if ("TEXT".equals(row.getWatermarkType()) && row.getFontFamily() == null) row.setFontFamily("SimHei"); row.setFontColor(optional(form.getFontColor(), 30, "字体颜色")); if ("TEXT".equals(row.getWatermarkType()) && row.getFontColor() == null) row.setFontColor("#666666"); row.setFontSize(number(form.getFontSize(), 6, 144, 24, "字体大小")); row.setRotationAngle(number(form.getRotationAngle(), -180, 180, 0, "旋转角度")); row.setOpacity(number(form.getOpacity(), 0, 100, 100, "透明度")); row.setLayoutMode(enumText(form.getLayoutMode(), LAYOUTS, "布局方式")); row.setHorizontalAlign(enumText(form.getHorizontalAlign() == null ? "LEFT" : form.getHorizontalAlign(), HORIZONTAL_ALIGNS, "水平对齐")); row.setVerticalAlign(enumText(form.getVerticalAlign() == null ? "TOP" : form.getVerticalAlign(), VERTICAL_ALIGNS, "垂直对齐")); row.setMarkWidthMm(decimal(form.getMarkWidthMm(), new BigDecimal("1"), new BigDecimal("1000"), new BigDecimal("80"), "水印宽度")); row.setMarkHeightMm(decimal(form.getMarkHeightMm(), new BigDecimal("1"), new BigDecimal("1000"), new BigDecimal("20"), "水印高度")); if ("TILED".equals(row.getLayoutMode())) { row.setRepeatSpacingXMm(decimal(form.getRepeatSpacingXMm(), BigDecimal.ZERO, new BigDecimal("1000"), new BigDecimal("20"), "横向间隔")); row.setRepeatSpacingYMm(decimal(form.getRepeatSpacingYMm(), BigDecimal.ZERO, new BigDecimal("1000"), new BigDecimal("20"), "纵向间隔")); } List positions = normalizePositions(form.getPositions()); return new Normalized(row, positions, normalizeBusinessTypes(form.getWatermarkBusinessTypes())); } private List normalizeBusinessTypes(List values) { if (values == null || values.isEmpty() || values.size() > 50) { throw new DataException("水印业务配置必须选择 1 到 50 项"); } List entries = dictionaryDataApi.getList(BUSINESS_TYPE_DICTIONARY_ID, "true"); if (entries == null) throw new DataException("水印业务类型字典查询失败"); Set allowed = entries.stream() .filter(item -> item != null && item.getDeleteMark() == null && Integer.valueOf(1).equals(item.getEnabledMark()) && BUSINESS_TYPE_DICTIONARY_ID.equals(item.getDictionaryTypeId())) .map(DictionaryDataEntity::getEnCode) .filter(StringUtils::hasText) .collect(Collectors.toSet()); LinkedHashSet result = new LinkedHashSet<>(); for (String value : values) { if (value == null || value.isBlank() || value.length() > 100 || !value.equals(value.trim()) || !allowed.contains(value)) { throw new DataException("水印业务配置包含无效的字典编码"); } if (!result.add(value)) throw new DataException("水印业务配置不能重复选择"); } return new ArrayList<>(result); } private void replaceBusinessTypes(DmsWatermarkConfigEntity config, List types) { configMapper.deleteBusinessTypes(config.getTenantId(), config.getId()); for (String type : types) configMapper.insertBusinessType(config.getTenantId(), config.getId(), type); } private List normalizeScenes(List values) throws DataException { if (values == null || values.isEmpty()) throw new DataException("至少选择一个水印加载场景"); List result = new ArrayList<>(); for (String value : values) { String scene = enumText(value, SCENES, "水印加载"); if (!result.contains(scene)) result.add(scene); } return result; } private List normalizePositions(List values) throws DataException { if (values == null || values.isEmpty() || values.size() > 20) throw new DataException("水印位置数量必须在 1 到 20 之间"); Set paperTypes = new HashSet<>(); List result = new ArrayList<>(); for (DmsWatermarkPositionForm value : values) { if (value == null) throw new DataException("水印位置不能包含空项"); DmsWatermarkPositionForm item = new DmsWatermarkPositionForm(); item.setPaperType(required(value.getPaperType(), 30, "纸张类型").toUpperCase()); if (!paperTypes.add(item.getPaperType())) throw new DataException("同一纸张类型只能配置一次"); item.setPaperWidthMm(decimal(value.getPaperWidthMm(), new BigDecimal("1"), new BigDecimal("2000"), null, "纸张宽度")); item.setPaperHeightMm(decimal(value.getPaperHeightMm(), new BigDecimal("1"), new BigDecimal("2000"), null, "纸张高度")); item.setPortraitXMm(coordinate(value.getPortraitXMm(), item.getPaperWidthMm(), "纵向横坐标")); item.setPortraitYMm(coordinate(value.getPortraitYMm(), item.getPaperHeightMm(), "纵向纵坐标")); item.setLandscapeXMm(coordinate(value.getLandscapeXMm(), item.getPaperHeightMm(), "横向横坐标")); item.setLandscapeYMm(coordinate(value.getLandscapeYMm(), item.getPaperWidthMm(), "横向纵坐标")); result.add(item); } return result; } private void replacePositions(DmsWatermarkConfigEntity config, List values) { for (int i = 0; i < values.size(); i++) { DmsWatermarkPositionForm value = values.get(i); DmsWatermarkPositionEntity row = new DmsWatermarkPositionEntity(); BeanUtils.copyProperties(value, row); row.setId(configMapper.nextId()); row.setTenantId(config.getTenantId()); row.setConfigId(config.getId()); row.setSortOrder(i); positionMapper.insert(row); } } private DmsWatermarkConfigVO toVO(DmsWatermarkConfigEntity row, boolean includePositions) { DmsWatermarkConfigVO vo = new DmsWatermarkConfigVO(); BeanUtils.copyProperties(row, vo); vo.setHorizontalAlign(row.getHorizontalAlign() == null ? "LEFT" : row.getHorizontalAlign()); vo.setVerticalAlign(row.getVerticalAlign() == null ? "TOP" : row.getVerticalAlign()); List scenes = new ArrayList<>(); if (Boolean.TRUE.equals(row.getLoadDownload())) scenes.add("DOWNLOAD"); if (Boolean.TRUE.equals(row.getLoadPreview())) scenes.add("PREVIEW"); if (Boolean.TRUE.equals(row.getLoadPrint())) scenes.add("PRINT"); vo.setLoadScenes(scenes); vo.setWatermarkBusinessTypes(configMapper.selectBusinessTypes(identity.tenantId(), row.getId())); if (includePositions) { List rows = positionMapper.selectList(new LambdaQueryWrapper() .eq(DmsWatermarkPositionEntity::getTenantId, identity.tenantId()) .eq(DmsWatermarkPositionEntity::getConfigId, row.getId()) .orderByAsc(DmsWatermarkPositionEntity::getSortOrder)); vo.setPositions(rows.stream().map(value -> { DmsWatermarkPositionForm item = new DmsWatermarkPositionForm(); BeanUtils.copyProperties(value, item); return item; }).collect(Collectors.toList())); } return vo; } private String enumText(String value, Set allowed, String label) throws DataException { String normalized = required(value, 50, label).toUpperCase(); if (!allowed.contains(normalized)) throw new DataException(label + "取值无效"); return normalized; } private int number(Integer value, int min, int max, int fallback, String label) throws DataException { int normalized = value == null ? fallback : value; if (normalized < min || normalized > max) throw new DataException(label + "必须在 " + min + " 到 " + max + " 之间"); return normalized; } private BigDecimal decimal(BigDecimal value, BigDecimal min, BigDecimal max, BigDecimal fallback, String label) throws DataException { BigDecimal normalized = value == null ? fallback : value; if (normalized == null) throw new DataException(label + "不能为空"); if (normalized.compareTo(min) < 0 || normalized.compareTo(max) > 0) throw new DataException(label + "超出允许范围"); return normalized; } private BigDecimal coordinate(BigDecimal value, BigDecimal max, String label) throws DataException { return decimal(value, BigDecimal.ZERO, max, null, label); } private String required(String value, int max, String label) throws DataException { String normalized = optional(value, max, label); if (normalized == null) throw new DataException(label + "不能为空"); return normalized; } private String optional(String value, int max, String label) throws DataException { String normalized = trim(value); if (normalized != null && normalized.length() > max) throw new DataException(label + "长度不能超过 " + max); return normalized; } private String trim(String value) { return value == null || value.trim().isEmpty() ? null : value.trim(); } private static class Normalized { private final DmsWatermarkConfigEntity row; private final List positions; private final List businessTypes; private Normalized(DmsWatermarkConfigEntity row, List positions, List businessTypes) { this.row = row; this.positions = positions; this.businessTypes = businessTypes; } } }