| | |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.core.env.Environment; |
| | | |
| | | import java.net.URI; |
| | | import java.util.Arrays; |
| | | |
| | | /** |
| | |
| | | * <li>生产经营者名称必填(标签四要素之一,缺失即为不合规标签);</li> |
| | | * <li>当前环境必须存在可渲染中文的字体(否则标签中文会变成方框)。</li> |
| | | * </ul> |
| | | * 追溯网址与生产经营者名称的**运行时**取值优先来自字典 {@code mes_trace_config}, |
| | | * 可在管理后台「字典管理」中修改而无需重启;配置文件的值退居为兜底默认值。 |
| | | * <p> |
| | | * 但启动校验**只针对配置文件**,原因有二:一是这道防线必须在数据库可用之前就能拦住不合规配置; |
| | | * 二是 @PostConstruct 阶段不应访问数据库。字典值的合规校验由 {@link MesTraceConfigResolver} |
| | | * 在读取时完成(非法则记 ERROR 并回退配置文件,见该类注释)。 |
| | | */ |
| | | @Slf4j |
| | | @Configuration(proxyBeanMethods = false) |
| | |
| | | @Resource |
| | | private Environment environment; |
| | | |
| | | @Resource |
| | | private MesTraceConfigResolver mesTraceConfigResolver; |
| | | |
| | | @PostConstruct |
| | | public void init() { |
| | | // 1. 注入配置(字体校验独立于业务配置,本地环境同样必须通过,否则标签中文不可读) |
| | | // 1. 注入配置文件的值(字体校验独立于业务配置,本地环境同样必须通过,否则标签中文不可读) |
| | | MesProQrLabelBuilder.setTraceBaseUrl(mesProperties.getTraceBaseUrl()); |
| | | MesProQrLabelBuilder.setOperatorName(mesProperties.getOperatorName()); |
| | | MesProQrLabelBuilder.setImporterName(mesProperties.getImporterName()); |
| | |
| | | MesProQrLabelBuilder.setAllowPlainQrCode(mesProperties.isAllowPlainQrCode()); |
| | | QrCodeImageUtils.assertCjkFontAvailable(); |
| | | |
| | | // 2. 非本地环境执行严格校验 |
| | | // 2. 校验配置文件。下面的校验与日志都必须读配置文件的值,因此字典来源不能在此之前装配 |
| | | if (isLocalProfile()) { |
| | | log.info("[init] 当前为本地环境({}),跳过标签合规配置的严格校验;traceBaseUrl={}", |
| | | log.info("[init] 当前为本地环境({}),跳过标签合规配置的严格校验;配置文件 traceBaseUrl={}", |
| | | PROFILE_LOCAL, mesProperties.getTraceBaseUrl()); |
| | | return; |
| | | } else { |
| | | validateTraceBaseUrl(); |
| | | validateOperatorName(); |
| | | log.info("[init] 二维码标签配置校验通过;配置文件 traceBaseUrl={},生产经营者={}", |
| | | mesProperties.getTraceBaseUrl(), MesProQrLabelBuilder.resolveOperatorName()); |
| | | } |
| | | validateTraceBaseUrl(); |
| | | validateOperatorName(); |
| | | log.info("[init] 二维码标签配置校验通过;traceBaseUrl={},生产经营者={}", |
| | | mesProperties.getTraceBaseUrl(), MesProQrLabelBuilder.resolveOperatorName()); |
| | | |
| | | // 3. 最后装配字典来源:此后 resolveTraceUrl()/resolveOperatorName() 优先走字典 |
| | | // (字典未配置则自动回退配置文件)。此处只装配方法引用、绝不调用—— |
| | | // 解析字典需要访问数据库,不能在 @PostConstruct 阶段触发。 |
| | | MesProQrLabelBuilder.setTraceConfigProvider(mesTraceConfigResolver::resolveSnapshot); |
| | | } |
| | | |
| | | private boolean isLocalProfile() { |
| | |
| | | } |
| | | |
| | | /** |
| | | * 校验追溯网址:必须为公众扫码可访问的地址 |
| | | * 校验配置文件中的追溯网址:必须为公众扫码可访问的地址 |
| | | * <p> |
| | | * 与字典值共用 {@link MesTraceUrlValidator},保证两个来源的准入门槛完全一致, |
| | | * 不会出现「配置文件拦得住、字典绕得过」的缺口。 |
| | | */ |
| | | private void validateTraceBaseUrl() { |
| | | String url = StrUtil.trim(mesProperties.getTraceBaseUrl()); |
| | | if (StrUtil.isEmpty(url)) { |
| | | throw new IllegalStateException("mes.trace-base-url 未配置。二维码标签必须包含追溯网址," |
| | | + "请在配置中填写公众扫码可访问的 H5 溯源页地址。"); |
| | | } |
| | | if (StrUtil.containsAnyIgnoreCase(url, "your-domain", "example.com", "localhost")) { |
| | | throw new IllegalStateException("mes.trace-base-url 仍为占位符或本机地址:" + url |
| | | + ",印出的标签消费者将无法访问,请填写真实的外网地址。"); |
| | | } |
| | | String host = extractHost(url); |
| | | if (StrUtil.isEmpty(host)) { |
| | | throw new IllegalStateException("mes.trace-base-url 格式非法,无法解析主机名:" + url); |
| | | } |
| | | if (isPrivateHost(host)) { |
| | | throw new IllegalStateException("mes.trace-base-url 指向内网地址(" + host + "):" + url |
| | | + ",公众扫码无法访问,请填写外网可访问的地址。"); |
| | | } |
| | | MesTraceUrlValidator.invalidReason(url).ifPresent(reason -> { |
| | | throw new IllegalStateException("mes.trace-base-url " + reason |
| | | + "(该项是字典 " + MesTraceConfigResolver.DICT_TYPE + " 未配置时的兜底值)"); |
| | | }); |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | private void validateOperatorName() { |
| | | if (StrUtil.isBlank(MesProQrLabelBuilder.resolveOperatorName())) { |
| | | log.warn("[init] mes.operator-name(生产经营者名称)未配置。该字段为种子标签四要素之一," |
| | | + "缺失时将无法导出二维码标签,请尽快在配置中补充。"); |
| | | log.warn("[init] 生产经营者名称未配置(配置文件 mes.operator-name 与字典 {} 均为空)。" |
| | | + "该字段为种子标签四要素之一,缺失时将无法导出二维码标签,请尽快补充。", |
| | | MesTraceConfigResolver.DICT_TYPE); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 从 URL 中提取主机名 |
| | | */ |
| | | private String extractHost(String url) { |
| | | try { |
| | | URI uri = URI.create(url); |
| | | return uri.getHost(); |
| | | } catch (IllegalArgumentException ex) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 判断是否为本机 / 内网 / 链路本地地址 |
| | | */ |
| | | private boolean isPrivateHost(String host) { |
| | | if ("localhost".equalsIgnoreCase(host) || host.endsWith(".local")) { |
| | | return true; |
| | | } |
| | | String[] parts = host.split("\\."); |
| | | if (parts.length != 4) { |
| | | return false; // 域名形式,交由 DNS 解析,不在此处拦截 |
| | | } |
| | | int a, b; |
| | | try { |
| | | a = Integer.parseInt(parts[0]); |
| | | b = Integer.parseInt(parts[1]); |
| | | } catch (NumberFormatException ex) { |
| | | return false; |
| | | } |
| | | // 127.0.0.0/8 本机、10.0.0.0/8、172.16.0.0/12、192.168.0.0/16 私网、169.254.0.0/16 链路本地 |
| | | return a == 127 |
| | | || a == 10 |
| | | || (a == 172 && b >= 16 && b <= 31) |
| | | || (a == 192 && b == 168) |
| | | || (a == 169 && b == 254); |
| | | } |
| | | |
| | | } |