| | |
| | | package cn.iocoder.yudao.module.mes.service.wm.barcode; |
| | | |
| | | import cn.hutool.core.codec.Base64; |
| | | import cn.hutool.core.collection.CollUtil; |
| | | import cn.hutool.core.util.ObjUtil; |
| | | import cn.hutool.core.util.StrUtil; |
| | |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.barcode.MesWmBarcodeConfigDO; |
| | | import cn.iocoder.yudao.module.mes.dal.dataobject.wm.barcode.MesWmBarcodeDO; |
| | | import cn.iocoder.yudao.module.mes.dal.mysql.wm.barcode.MesWmBarcodeMapper; |
| | | import cn.iocoder.yudao.module.mes.enums.wm.BarcodeFormatEnum; |
| | | import com.google.zxing.BarcodeFormat; |
| | | import com.google.zxing.MultiFormatWriter; |
| | | import com.google.zxing.client.j2se.MatrixToImageWriter; |
| | | import com.google.zxing.common.BitMatrix; |
| | | import jakarta.annotation.Resource; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.validation.annotation.Validated; |
| | | |
| | | import javax.imageio.ImageIO; |
| | | import java.awt.image.BufferedImage; |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.util.ArrayList; |
| | | import java.util.Collection; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | 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.mes.enums.ErrorCodeConstants.*; |
| | |
| | | /** |
| | | * MES 条码清单 Service 实现类 |
| | | * |
| | | * @author 芋道源码 |
| | | * @author 超级管理员 |
| | | */ |
| | | @Service |
| | | @Validated |
| | | @Slf4j |
| | | public class MesWmBarcodeServiceImpl implements MesWmBarcodeService { |
| | | |
| | | /** |
| | | * 内容格式模板中的占位符识别正则 |
| | | */ |
| | | private static final Pattern PLACEHOLDER_PATTERN = Pattern.compile("\\{[A-Za-z0-9_]+\\}"); |
| | | |
| | | @Resource |
| | | private MesWmBarcodeMapper barcodeMapper; |
| | |
| | | |
| | | // 2. 生成条码内容,并校验唯一性 |
| | | String content = generateAndValidateContent(createReqVO.getId(), |
| | | createReqVO.getContent(), config.getContentFormat(), createReqVO.getBizCode()); |
| | | createReqVO.getContent(), config.getContentFormat(), createReqVO.getBizCode(), |
| | | createReqVO.getBizName()); |
| | | |
| | | // 3. 保存条码记录 |
| | | MesWmBarcodeDO barcode = BeanUtils.toBean(createReqVO, MesWmBarcodeDO.class).setContent(content) |
| | |
| | | } |
| | | |
| | | @Override |
| | | public String generateBarcodeContent(Integer bizType, String bizCode) { |
| | | public String generateBarcodeContent(Integer bizType, String bizCode, String bizName) { |
| | | // 1.1 校验参数 |
| | | if (bizType == null) { |
| | | throw exception(BARCODE_BIZ_TYPE_NOT_EXISTS); |
| | |
| | | MesWmBarcodeConfigDO config = barcodeConfigService.validateBarcodeConfigByBizType(bizType); |
| | | |
| | | // 2. 生成条码内容 |
| | | return generateBarcodeContent(config.getContentFormat(), bizCode); |
| | | return generateBarcodeContent(config.getContentFormat(), bizCode, bizName); |
| | | } |
| | | |
| | | @Override |
| | |
| | | |
| | | // 2. 生成条码内容,并校验唯一性 |
| | | String content = generateAndValidateContent(updateReqVO.getId(), |
| | | updateReqVO.getContent(), config.getContentFormat(), updateReqVO.getBizCode()); |
| | | updateReqVO.getContent(), config.getContentFormat(), updateReqVO.getBizCode(), |
| | | updateReqVO.getBizName()); |
| | | |
| | | // 3. 更新(刷新 configId 和 format,确保与最新配置一致) |
| | | MesWmBarcodeDO updateObj = BeanUtils.toBean(updateReqVO, MesWmBarcodeDO.class) |
| | |
| | | * @param bizCode 业务编码 |
| | | * @return 最终条码内容 |
| | | */ |
| | | private String generateAndValidateContent(Long id, String content, String contentFormat, String bizCode) { |
| | | private String generateAndValidateContent(Long id, String content, String contentFormat, String bizCode, String bizName) { |
| | | // 1. 如果前端未传递内容,则自动生成 |
| | | if (StrUtil.isBlank(content)) { |
| | | content = generateBarcodeContent(contentFormat, bizCode); |
| | | content = generateBarcodeContent(contentFormat, bizCode, bizName); |
| | | } |
| | | // 2. 校验条码内容唯一性(排除自身) |
| | | if (StrUtil.isNotBlank(content)) { |
| | |
| | | * |
| | | * @param contentFormat 内容格式模板 |
| | | * @param bizCode 业务编码 |
| | | * @param bizName 业务名称 |
| | | * @return 条码内容 |
| | | */ |
| | | private String generateBarcodeContent(String contentFormat, String bizCode) { |
| | | private String generateBarcodeContent(String contentFormat, String bizCode, String bizName) { |
| | | if (StrUtil.isBlank(contentFormat)) { |
| | | return bizCode; |
| | | } |
| | | return contentFormat.replace(MesWmBarcodeConfigDO.PLACEHOLDER_BUSINESS_CODE, bizCode); |
| | | // 1. 替换已支持的占位符 |
| | | String content = contentFormat |
| | | .replace(MesWmBarcodeConfigDO.PLACEHOLDER_BUSINESS_CODE, StrUtil.nullToEmpty(bizCode)) |
| | | .replace(MesWmBarcodeConfigDO.PLACEHOLDER_BUSINESS_NAME, StrUtil.nullToEmpty(bizName)); |
| | | // 2. 清空模板中其它未填充的未知占位符,避免 {XXX} 残留影响扫码 |
| | | return PLACEHOLDER_PATTERN.matcher(content).replaceAll(""); |
| | | } |
| | | |
| | | @Override |
| | | public String renderBarcodeImage(String content, Integer format, Integer width, Integer height) { |
| | | if (StrUtil.isBlank(content)) { |
| | | throw exception(BARCODE_CONTENT_EMPTY); |
| | | } |
| | | int w = width != null && width > 0 ? width : 200; |
| | | int h = height != null && height > 0 ? height : 200; |
| | | try { |
| | | BitMatrix bitMatrix = new MultiFormatWriter().encode(content, toZxingFormat(format), w, h); |
| | | BufferedImage image = MatrixToImageWriter.toBufferedImage(bitMatrix); |
| | | ByteArrayOutputStream out = new ByteArrayOutputStream(); |
| | | ImageIO.write(image, "png", out); |
| | | return "data:image/png;base64," + Base64.encode(out.toByteArray()); |
| | | } catch (Exception e) { |
| | | log.error("[renderBarcodeImage] 条码渲染失败,content={}, format={}", content, format, e); |
| | | throw new RuntimeException("条码渲染失败"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 将项目条码格式枚举值映射为 zxing 条码格式 |
| | | * |
| | | * @param format 项目条码格式 {@link BarcodeFormatEnum} |
| | | * @return zxing 条码格式 |
| | | */ |
| | | private BarcodeFormat toZxingFormat(Integer format) { |
| | | if (format == null) { |
| | | return BarcodeFormat.CODE_128; |
| | | } |
| | | switch (format) { |
| | | case 1: // BarcodeFormatEnum.QR_CODE |
| | | return BarcodeFormat.QR_CODE; |
| | | case 2: // BarcodeFormatEnum.EAN13 |
| | | return BarcodeFormat.EAN_13; |
| | | case 3: // BarcodeFormatEnum.CODE39 |
| | | return BarcodeFormat.CODE_39; |
| | | case 4: // BarcodeFormatEnum.UPC_A |
| | | return BarcodeFormat.UPC_A; |
| | | default: |
| | | throw exception(BARCODE_FORMAT_INVALID); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | public Map<String, Object> parseBarcodeContent(String content) { |
| | | if (StrUtil.isBlank(content)) { |
| | | throw exception(BARCODE_CONTENT_EMPTY); |
| | | } |
| | | // 1. 优先按条码内容精确命中已有条码记录 |
| | | MesWmBarcodeDO barcode = barcodeMapper.selectByContent(content); |
| | | if (barcode != null) { |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("matched", true); |
| | | result.put("barcodeId", barcode.getId()); |
| | | result.put("configId", barcode.getConfigId()); |
| | | result.put("format", barcode.getFormat()); |
| | | result.put("bizType", barcode.getBizType()); |
| | | result.put("bizId", barcode.getBizId()); |
| | | result.put("bizCode", barcode.getBizCode()); |
| | | result.put("bizName", barcode.getBizName()); |
| | | result.put("content", barcode.getContent()); |
| | | return result; |
| | | } |
| | | // 2. 未命中时,遍历启用状态的配置按内容格式模板反解 |
| | | List<MesWmBarcodeConfigDO> configs = barcodeConfigService.getEnableBarcodeConfigList(); |
| | | for (MesWmBarcodeConfigDO config : configs) { |
| | | Map<String, Object> fields = matchByTemplate(config.getContentFormat(), content); |
| | | if (fields != null) { |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("matched", false); |
| | | result.put("configId", config.getId()); |
| | | result.put("format", config.getFormat()); |
| | | result.put("bizType", config.getBizType()); |
| | | result.put("bizCode", fields.get(MesWmBarcodeConfigDO.PLACEHOLDER_BUSINESS_CODE)); |
| | | result.put("bizName", fields.get(MesWmBarcodeConfigDO.PLACEHOLDER_BUSINESS_NAME)); |
| | | result.put("fields", fields); |
| | | return result; |
| | | } |
| | | } |
| | | // 3. 均未命中 |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("matched", false); |
| | | result.put("content", content); |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * 按内容格式模板反解条码内容 |
| | | * |
| | | * @param contentFormat 内容格式模板 |
| | | * @param content 待解析的条码内容 |
| | | * @return 占位符 -> 实际值的映射;不匹配返回 null |
| | | */ |
| | | private Map<String, Object> matchByTemplate(String contentFormat, String content) { |
| | | if (StrUtil.isBlank(contentFormat) || !contentFormat.contains("{")) { |
| | | return null; |
| | | } |
| | | // 1. 将模板转为正则:字面量转义,占位符转为捕获组 |
| | | List<String> placeholders = new ArrayList<>(); |
| | | StringBuilder regexSb = new StringBuilder(); |
| | | Matcher matcher = PLACEHOLDER_PATTERN.matcher(contentFormat); |
| | | int lastEnd = 0; |
| | | while (matcher.find()) { |
| | | regexSb.append(Pattern.quote(contentFormat.substring(lastEnd, matcher.start()))); |
| | | placeholders.add(matcher.group()); |
| | | regexSb.append("(.+?)"); |
| | | lastEnd = matcher.end(); |
| | | } |
| | | if (placeholders.isEmpty()) { |
| | | return null; |
| | | } |
| | | regexSb.append(Pattern.quote(contentFormat.substring(lastEnd))); |
| | | |
| | | // 2. 全量匹配并提取各占位符值 |
| | | Matcher contentMatcher = Pattern.compile(regexSb.toString()).matcher(content); |
| | | if (!contentMatcher.matches()) { |
| | | return null; |
| | | } |
| | | Map<String, Object> fields = new LinkedHashMap<>(); |
| | | for (int i = 0; i < placeholders.size(); i++) { |
| | | fields.put(placeholders.get(i), contentMatcher.group(i + 1)); |
| | | } |
| | | return fields; |
| | | } |
| | | |
| | | @Override |