package com.vincent.rsf.server.manager.service.impl; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.vincent.rsf.framework.exception.CoolException; import com.vincent.rsf.server.manager.entity.OrderPrintTemplate; import com.vincent.rsf.server.manager.mapper.OrderPrintTemplateMapper; import com.vincent.rsf.server.manager.service.OrderPrintTemplateService; import com.vincent.rsf.server.system.entity.User; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @Service("orderPrintTemplateService") public class OrderPrintTemplateServiceImpl extends ServiceImpl implements OrderPrintTemplateService { private static final Set SUPPORTED_ELEMENT_TYPES = Collections.unmodifiableSet( new LinkedHashSet<>(Arrays.asList("text", "barcode", "qrcode", "image", "line", "rect", "table")) ); private static final Set SUPPORTED_TEMPLATE_TYPES = Collections.unmodifiableSet( new LinkedHashSet<>(Arrays.asList("in", "out")) ); private static final Set SUPPORTED_DOCUMENT_PAPER_SIZES = Collections.unmodifiableSet( new LinkedHashSet<>(Arrays.asList("A4", "A5", "A6", "LETTER")) ); private static final Set SUPPORTED_DOCUMENT_ORIENTATIONS = Collections.unmodifiableSet( new LinkedHashSet<>(Arrays.asList("portrait", "landscape")) ); private static final Set SUPPORTED_DOCUMENT_ALIGNMENTS = Collections.unmodifiableSet( new LinkedHashSet<>(Arrays.asList("left", "center", "right")) ); private static final Set SUPPORTED_DOCUMENT_LOGO_POSITIONS = Collections.unmodifiableSet( new LinkedHashSet<>(Arrays.asList("left", "right")) ); @Override public List listCurrentTenantTemplates(String type) { String normalizedType = normalizeTemplateType(type); List templates = this.list(new LambdaQueryWrapper() .eq(OrderPrintTemplate::getType, normalizedType) .orderByDesc(OrderPrintTemplate::getIsDefault) .orderByDesc(OrderPrintTemplate::getUpdateTime) .orderByDesc(OrderPrintTemplate::getCreateTime) ); return templates == null ? new ArrayList<>() : templates; } @Override public OrderPrintTemplate getCurrentTenantTemplate(Long id) { if (id == null) { throw new CoolException("模板ID不能为空"); } OrderPrintTemplate template = this.getById(id); if (template == null) { throw new CoolException("模板不存在或已被删除"); } normalizeTemplateType(template.getType()); return template; } @Override public OrderPrintTemplate getCurrentTenantDefaultTemplate(String type) { String normalizedType = normalizeTemplateType(type); OrderPrintTemplate template = this.getOne(new LambdaQueryWrapper() .eq(OrderPrintTemplate::getType, normalizedType) .eq(OrderPrintTemplate::getStatus, 1) .eq(OrderPrintTemplate::getIsDefault, 1) .orderByDesc(OrderPrintTemplate::getUpdateTime) .last("limit 1") ); if (template != null) { return template; } return this.getOne(new LambdaQueryWrapper() .eq(OrderPrintTemplate::getType, normalizedType) .eq(OrderPrintTemplate::getStatus, 1) .orderByDesc(OrderPrintTemplate::getUpdateTime) .orderByDesc(OrderPrintTemplate::getCreateTime) .last("limit 1") ); } @Override @Transactional(rollbackFor = Exception.class) public OrderPrintTemplate saveTemplate(OrderPrintTemplate template) { OrderPrintTemplate normalized = prepareTemplateForSave(template, false); long currentCount = this.count(new LambdaQueryWrapper() .eq(OrderPrintTemplate::getType, normalized.getType()) ); boolean shouldDefault = Objects.equals(normalized.getIsDefault(), 1) || currentCount == 0; normalized.setIsDefault(shouldDefault ? 1 : 0); if (shouldDefault) { clearCurrentTypeDefaults(normalized.getType()); } if (!this.save(normalized)) { throw new CoolException("模板保存失败"); } return this.getCurrentTenantTemplate(normalized.getId()); } @Override @Transactional(rollbackFor = Exception.class) public OrderPrintTemplate updateTemplate(OrderPrintTemplate template) { if (template == null || template.getId() == null) { throw new CoolException("模板ID不能为空"); } OrderPrintTemplate existing = getCurrentTenantTemplate(template.getId()); OrderPrintTemplate normalized = prepareTemplateForSave(template, true); normalized.setType(existing.getType()); normalized.setTenantId(existing.getTenantId()); normalized.setCreateBy(existing.getCreateBy()); normalized.setCreateTime(existing.getCreateTime()); normalized.setDeleted(existing.getDeleted()); boolean shouldDefault = Objects.equals(normalized.getIsDefault(), 1); if (shouldDefault) { clearCurrentTypeDefaults(existing.getType()); } else if (Objects.equals(existing.getIsDefault(), 1)) { normalized.setIsDefault(1); } if (!this.updateById(normalized)) { throw new CoolException("模板更新失败"); } ensureOneDefaultTemplate(existing.getType()); return this.getCurrentTenantTemplate(normalized.getId()); } @Override @Transactional(rollbackFor = Exception.class) public boolean removeTemplates(List ids) { if (ids == null || ids.isEmpty()) { throw new CoolException("请选择要删除的模板"); } List templates = this.listByIds(ids); if (templates == null || templates.isEmpty()) { return true; } Set affectedTypes = new LinkedHashSet<>(); boolean removedDefault = false; for (OrderPrintTemplate template : templates) { if (template == null) { continue; } affectedTypes.add(normalizeTemplateType(template.getType())); if (Objects.equals(template.getIsDefault(), 1)) { removedDefault = true; } } if (!this.removeByIds(ids)) { throw new CoolException("模板删除失败"); } if (removedDefault) { for (String type : affectedTypes) { ensureOneDefaultTemplate(type); } } return true; } @Override @Transactional(rollbackFor = Exception.class) public boolean setDefaultTemplate(Long id) { OrderPrintTemplate template = getCurrentTenantTemplate(id); String type = normalizeTemplateType(template.getType()); clearCurrentTypeDefaults(type); boolean updated = this.update(new LambdaUpdateWrapper() .eq(OrderPrintTemplate::getId, template.getId()) .set(OrderPrintTemplate::getIsDefault, 1) .set(OrderPrintTemplate::getUpdateBy, resolveCurrentUserId()) .set(OrderPrintTemplate::getUpdateTime, new Date()) ); if (!updated) { throw new CoolException("默认模板设置失败"); } return true; } private OrderPrintTemplate prepareTemplateForSave(OrderPrintTemplate template, boolean updating) { if (template == null) { throw new CoolException("模板参数不能为空"); } Long currentTenantId = resolveCurrentTenantId(); Long currentUserId = resolveCurrentUserId(); if (currentTenantId == null) { throw new CoolException("当前租户信息缺失"); } String type = normalizeTemplateType(template.getType()); String name = normalizeText(template.getName()); String code = normalizeText(template.getCode()); if (name.isEmpty()) { throw new CoolException("模板名称不能为空"); } if (code.isEmpty()) { throw new CoolException("模板编码不能为空"); } Map canvasJson = template.getCanvasJson(); if (canvasJson == null || canvasJson.isEmpty()) { throw new CoolException("模板配置不能为空"); } validateCanvasJson(canvasJson); ensureTemplateCodeUnique(type, code, updating ? template.getId() : null); Date now = new Date(); template.setTenantId(currentTenantId) .setType(type) .setName(name) .setCode(code) .setStatus(template.getStatus() == null ? 1 : template.getStatus()) .setIsDefault(Objects.equals(template.getIsDefault(), 1) ? 1 : 0) .setMemo(normalizeText(template.getMemo())) .setUpdateBy(currentUserId) .setUpdateTime(now); if (!updating) { template.setCreateBy(currentUserId); template.setCreateTime(now); } return template; } private void ensureTemplateCodeUnique(String type, String code, Long excludeId) { long duplicateCount = this.count(new LambdaQueryWrapper() .eq(OrderPrintTemplate::getType, type) .eq(OrderPrintTemplate::getCode, code) .ne(excludeId != null, OrderPrintTemplate::getId, excludeId) ); if (duplicateCount > 0) { throw new CoolException("模板编码已存在,请更换后重试"); } } private void clearCurrentTypeDefaults(String type) { this.update(new LambdaUpdateWrapper() .eq(OrderPrintTemplate::getType, type) .eq(OrderPrintTemplate::getIsDefault, 1) .set(OrderPrintTemplate::getIsDefault, 0) .set(OrderPrintTemplate::getUpdateBy, resolveCurrentUserId()) .set(OrderPrintTemplate::getUpdateTime, new Date()) ); } private void ensureOneDefaultTemplate(String type) { long defaultCount = this.count(new LambdaQueryWrapper() .eq(OrderPrintTemplate::getType, type) .eq(OrderPrintTemplate::getIsDefault, 1) ); if (defaultCount > 0) { return; } OrderPrintTemplate newest = this.getOne(new LambdaQueryWrapper() .eq(OrderPrintTemplate::getType, type) .orderByDesc(OrderPrintTemplate::getUpdateTime) .orderByDesc(OrderPrintTemplate::getCreateTime) .last("limit 1") ); if (newest == null) { return; } this.update(new LambdaUpdateWrapper() .eq(OrderPrintTemplate::getId, newest.getId()) .set(OrderPrintTemplate::getIsDefault, 1) .set(OrderPrintTemplate::getUpdateBy, resolveCurrentUserId()) .set(OrderPrintTemplate::getUpdateTime, new Date()) ); } private void validateCanvasJson(Map canvasJson) { JSONObject root = JSONObject.parseObject(JSON.toJSONString(canvasJson)); if (root == null) { throw new CoolException("模板配置格式不正确"); } if (root.getInteger("version") == null) { throw new CoolException("模板版本不能为空"); } if (isDocumentSchema(root)) { validateDocumentSchema(root); return; } validateLegacyCanvasSchema(root); } private boolean isDocumentSchema(JSONObject root) { return "document".equals(normalizeText(root.getString("mode"))) || root.containsKey("headerFields") || root.containsKey("tableColumns") || root.containsKey("footerFields"); } private void validateDocumentSchema(JSONObject root) { String title = normalizeText(root.getString("title")); if (title.isEmpty()) { throw new CoolException("单据标题不能为空"); } JSONObject page = root.getJSONObject("page"); if (page == null) { throw new CoolException("纸张配置不能为空"); } String size = normalizeText(page.getString("size")); if (!SUPPORTED_DOCUMENT_PAPER_SIZES.contains(size)) { throw new CoolException("纸张仅支持 A4、A5、A6 或 LETTER"); } String orientation = normalizeText(page.getString("orientation")); if (!SUPPORTED_DOCUMENT_ORIENTATIONS.contains(orientation)) { throw new CoolException("纸张方向仅支持 portrait 或 landscape"); } ensureNumber(page, "marginTop", "上边距"); ensureNumber(page, "marginRight", "右边距"); ensureNumber(page, "marginBottom", "下边距"); ensureNumber(page, "marginLeft", "左边距"); if (root.getBooleanValue("showLogo")) { String logoSrc = normalizeText(root.getString("logoSrc")); if (logoSrc.isEmpty()) { throw new CoolException("启用Logo时必须上传Logo图片"); } String logoPosition = normalizeText(root.getString("logoPosition")); if (!logoPosition.isEmpty() && !SUPPORTED_DOCUMENT_LOGO_POSITIONS.contains(logoPosition)) { throw new CoolException("Logo位置仅支持 left 或 right"); } getPositiveNumber(root, "logoWidth", "Logo宽度"); } validateDocumentFields(root.getJSONArray("headerFields"), "页头字段", false); validateDocumentFields(root.getJSONArray("tableColumns"), "明细列", true); validateDocumentFields(root.getJSONArray("footerFields"), "页尾字段", false); if (root.getBooleanValue("showBarcode") && normalizeText(root.getString("barcodeField")).isEmpty()) { throw new CoolException("启用条码时必须设置条码字段"); } } private void validateDocumentFields(JSONArray fields, String label, boolean tableSection) { if (fields == null) { throw new CoolException(label + "不能为空"); } for (int index = 0; index < fields.size(); index++) { JSONObject field = fields.getJSONObject(index); if (field == null) { throw new CoolException(label + "格式不正确"); } if (normalizeText(field.getString("key")).isEmpty()) { throw new CoolException(label + "第" + (index + 1) + "项缺少字段标识"); } if (normalizeText(field.getString("label")).isEmpty()) { throw new CoolException(label + "第" + (index + 1) + "项缺少字段名称"); } if (tableSection) { getPositiveNumber(field, "width", label + "宽度"); String align = normalizeText(field.getString("align")); if (!align.isEmpty() && !SUPPORTED_DOCUMENT_ALIGNMENTS.contains(align)) { throw new CoolException(label + "对齐方式仅支持 left、center 或 right"); } } else if (field.containsKey("span")) { getPositiveNumber(field, "span", label + "栅格宽度"); } } } private void validateLegacyCanvasSchema(JSONObject root) { JSONObject canvas = root.getJSONObject("canvas"); if (canvas == null) { throw new CoolException("模板画布配置不能为空"); } double width = getPositiveNumber(canvas, "width", "画布宽度"); double height = getPositiveNumber(canvas, "height", "画布高度"); if (width <= 0 || height <= 0) { throw new CoolException("画布尺寸必须大于0"); } String unit = normalizeText(canvas.getString("unit")); if (!"mm".equals(unit)) { throw new CoolException("画布单位仅支持 mm"); } JSONArray elements = root.getJSONArray("elements"); if (elements == null) { throw new CoolException("模板元素不能为空"); } for (int index = 0; index < elements.size(); index++) { JSONObject element = elements.getJSONObject(index); if (element == null) { throw new CoolException("模板元素格式不正确"); } validateElement(element, index); } } private void validateElement(JSONObject element, int index) { String type = normalizeText(element.getString("type")); if (!SUPPORTED_ELEMENT_TYPES.contains(type)) { throw new CoolException("第" + (index + 1) + "个元素类型不支持"); } if (normalizeText(element.getString("id")).isEmpty()) { throw new CoolException("第" + (index + 1) + "个元素缺少 ID"); } ensureNumber(element, "x", "元素 X 坐标"); ensureNumber(element, "y", "元素 Y 坐标"); if (!"line".equals(type)) { getPositiveNumber(element, "w", "元素宽度"); getPositiveNumber(element, "h", "元素高度"); } else { String direction = normalizeText(element.getString("direction")); if (!Arrays.asList("horizontal", "vertical").contains(direction)) { throw new CoolException("线条元素方向仅支持 horizontal 或 vertical"); } getPositiveNumber(element, "w", "线条长度"); getPositiveNumber(element, "h", "线条粗细"); } switch (type) { case "text": String contentMode = normalizeText(element.getString("contentMode")); if (!Arrays.asList("static", "template").contains(contentMode)) { throw new CoolException("文本元素内容模式不支持"); } if (normalizeText(element.getString("contentTemplate")).isEmpty()) { throw new CoolException("文本元素内容不能为空"); } break; case "barcode": if (normalizeText(element.getString("valueTemplate")).isEmpty()) { throw new CoolException("条码元素值模板不能为空"); } String symbology = normalizeText(element.getString("symbology")); if (!symbology.isEmpty() && !"CODE128".equals(symbology)) { throw new CoolException("一维码仅支持 CODE128"); } break; case "qrcode": if (normalizeText(element.getString("valueTemplate")).isEmpty()) { throw new CoolException("二维码元素值模板不能为空"); } break; case "image": if (normalizeText(element.getString("src")).isEmpty()) { throw new CoolException("图片元素地址不能为空"); } String objectFit = normalizeText(element.getString("objectFit")); if (!objectFit.isEmpty() && !Arrays.asList("contain", "cover", "fill").contains(objectFit)) { throw new CoolException("图片元素填充方式仅支持 contain、cover 或 fill"); } break; case "table": if (element.getJSONArray("columns") == null) { throw new CoolException("表格元素 columns 不能为空"); } if (element.getJSONArray("rows") == null) { throw new CoolException("表格元素 rows 不能为空"); } if (element.getJSONArray("cells") == null) { throw new CoolException("表格元素 cells 不能为空"); } break; default: break; } } private void ensureNumber(JSONObject object, String key, String label) { if (object.getBigDecimal(key) == null) { throw new CoolException(label + "不能为空"); } } private double getPositiveNumber(JSONObject object, String key, String label) { if (object.getBigDecimal(key) == null) { throw new CoolException(label + "不能为空"); } double value = object.getBigDecimal(key).doubleValue(); if (value <= 0) { throw new CoolException(label + "必须大于0"); } return value; } private String normalizeTemplateType(String value) { String type = normalizeText(value); if (!SUPPORTED_TEMPLATE_TYPES.contains(type)) { throw new CoolException("模板类型仅支持 in 或 out"); } return type; } private String normalizeText(String value) { return value == null ? "" : value.trim(); } private Long resolveCurrentTenantId() { User loginUser = getCurrentUser(); return loginUser == null ? null : loginUser.getTenantId(); } private Long resolveCurrentUserId() { User loginUser = getCurrentUser(); return loginUser == null ? null : loginUser.getId(); } private User getCurrentUser() { try { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.getPrincipal() instanceof User) { return (User) authentication.getPrincipal(); } } catch (Exception ignored) { } return null; } }