package cn.iocoder.yudao.module.mes.framework.qrcode; import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.exception.ErrorCode; import cn.iocoder.yudao.module.mes.dal.dataobject.mdm.MesMdmItemDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.bagcode.MesProBagCodeDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.batch.MesProBatchDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.pallet.MesProPalletDO; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; 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.PRO_QR_LABEL_ITEM_NAME_MISSING; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_LABEL_OPERATOR_MISSING; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_LABEL_TRACE_URL_NOT_CONFIGURED; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_LABEL_UNIT_CODE_INVALID; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.PRO_QR_LABEL_UNIT_CODE_MISSING; /** * MES 二维码标签构建器:统一"二维码编码内容 + 图片叠加可读文本"的文本组装规则。 *
* 种子标签受《农作物种子标签二维码编码规则》与《农作物种子标签通则》(GB 20464-2024)约束, * 最小销售单元的标签必须包含四项信息且按固定顺序排列: *
* 二维码内容在配置了 {@code mes.trace-base-url} 时为带 code 参数的溯源 URL,否则按 * {@code mes.allow-plain-qr-code} 决定是否降级为纯码值(仅限本地兼容历史码)。 */ public final class MesProQrLabelBuilder { private MesProQrLabelBuilder() { } /** 内部业务标签独立于公众追溯标签,不拼接 H5 网址。 */ public static QrLabelInfo buildBagBusinessCodeLabel(MesProBagCodeDO bag) { return buildBusinessCodeLabel(bag.getBusinessCode()); } public static QrLabelInfo buildPalletBusinessCodeLabel(MesProPalletDO pallet) { return buildBusinessCodeLabel(pallet.getBusinessCode()); } public static QrLabelInfo buildBatchBusinessCodeLabel(MesProBatchDO batch) { return buildBusinessCodeLabel(batch.getBusinessCode()); } private static QrLabelInfo buildBusinessCodeLabel(String code) { if (code == null || !code.matches("^[0-9]{16}$")) { throw new cn.iocoder.yudao.framework.common.exception.ServiceException(400, "缺少有效业务码,请先补生成业务码"); } return QrLabelInfo.builder().fileName(code).code(code).keyValues(new LinkedHashMap<>()).build(); } /** * 单元识别代码格式:1~20 位数字或字母数字组合(国标要求,不含连字符等符号) */ private static final Pattern UNIT_CODE_PATTERN = Pattern.compile("^[0-9A-Za-z]{1,20}$"); /** H5 溯源页面基础地址,由 {@code MesQrCodeConfig} 启动时注入;为空时二维码编码纯码值 */ private static volatile String traceBaseUrl = ""; /** 生产经营者名称,由 {@code MesQrCodeConfig} 启动时注入 */ private static volatile String operatorName = ""; /** 进口商名称,进口种子场景使用 */ private static volatile String importerName = ""; /** 是否进口种子场景 */ private static volatile boolean imported = false; /** 是否允许二维码降级编码纯码值(仅限本地兼容历史码) */ private static volatile boolean allowPlainQrCode = false; public static void setTraceBaseUrl(String traceBaseUrl) { MesProQrLabelBuilder.traceBaseUrl = StrUtil.blankToDefault(traceBaseUrl, ""); } public static void setOperatorName(String operatorName) { MesProQrLabelBuilder.operatorName = StrUtil.blankToDefault(operatorName, ""); } public static void setImporterName(String importerName) { MesProQrLabelBuilder.importerName = StrUtil.blankToDefault(importerName, ""); } public static void setImported(boolean imported) { MesProQrLabelBuilder.imported = imported; } public static void setAllowPlainQrCode(boolean allowPlainQrCode) { MesProQrLabelBuilder.allowPlainQrCode = allowPlainQrCode; } /** * 当前应标注的生产经营者名称:进口种子场景优先取进口商名称,否则取生产经营者名称 * * @return 名称,未配置时返回空串 */ public static String resolveOperatorName() { if (imported && StrUtil.isNotBlank(importerName)) { return importerName; } return operatorName; } /** * 追溯网址(对外展示用,不含 code 参数) */ public static String resolveTraceUrl() { return traceBaseUrl; } /** * 是否进口种子场景:为 true 时标签与溯源页应展示「进口商名称」 */ public static boolean isImported() { return imported; } /** * 组装二维码内容:配置了溯源地址则拼 URL,否则按开关决定是否降级为纯码值 *
* 未配置溯源地址且未开启降级开关时直接报错,避免印出不含追溯网址的标签。
*/
private static String buildQrCodeContent(String code) {
if (StrUtil.isEmpty(traceBaseUrl)) {
if (allowPlainQrCode) {
return code;
}
throw exception(PRO_QR_LABEL_TRACE_URL_NOT_CONFIGURED);
}
return traceBaseUrl + "?code=" + URLEncoder.encode(code, StandardCharsets.UTF_8);
}
/**
* 批次二维码标签
*/
public static QrLabelInfo buildBatchLabel(MesProBatchDO batch,
Map
* 按国标要求,四要素置于标签最前并按固定顺序排列:品种名称 → 生产经营者 → 单元识别代码 → 追溯网址;
* 袋序号、批次号、产线、责任人等生产信息作为辅助内容排列在其后。四要素任一缺失即抛异常,拒绝导出。
*/
public static QrLabelInfo buildBagCodeLabel(MesProBagCodeDO bagCode,
MesProBatchDO batch,
Map
* 注意:此处不再写入「品种」,品种名称统一由 {@link #resolveItemName} 解析,
* 避免批次品种覆盖袋码品种导致标签与溯源页展示不一致。
*/
private static void fillBatchKeyValues(LinkedHashMap