liu
昨天 4ca4444ad33a2a1a51f5c06e78ee9365dfb1eed6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package cn.iocoder.yudao.module.mes.framework.config;
 
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.mes.framework.qrcode.MesProQrLabelBuilder;
import cn.iocoder.yudao.module.mes.framework.qrcode.QrCodeImageUtils;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
 
import java.net.URI;
import java.util.Arrays;
 
/**
 * MES 二维码相关配置:启动时校验并把配置注入二维码标签构建器,避免侵入 Controller 调用点。
 * <p>
 * 种子标签属于受国标约束的合规物料,一旦印出即无法召回,因此这里采取「启动即校验、不合法即拒绝启动」
 * 的策略,而不是等到导出时才失败或静默降级:
 * <ul>
 *     <li>追溯网址必须为公众可访问地址,禁止内网 IP / 本机地址 / 未替换的占位符;</li>
 *     <li>生产经营者名称必填(标签四要素之一,缺失即为不合规标签);</li>
 *     <li>当前环境必须存在可渲染中文的字体(否则标签中文会变成方框)。</li>
 * </ul>
 */
@Slf4j
@Configuration(proxyBeanMethods = false)
public class MesQrCodeConfig {
 
    /**
     * 本地环境 profile 名:该 profile 下放宽校验,便于本地开发调试
     */
    private static final String PROFILE_LOCAL = "local";
 
    @Resource
    private MesProperties mesProperties;
 
    @Resource
    private Environment environment;
 
    @PostConstruct
    public void init() {
        // 1. 注入配置(字体校验独立于业务配置,本地环境同样必须通过,否则标签中文不可读)
        MesProQrLabelBuilder.setTraceBaseUrl(mesProperties.getTraceBaseUrl());
        MesProQrLabelBuilder.setOperatorName(mesProperties.getOperatorName());
        MesProQrLabelBuilder.setImporterName(mesProperties.getImporterName());
        MesProQrLabelBuilder.setImported(mesProperties.isImported());
        MesProQrLabelBuilder.setAllowPlainQrCode(mesProperties.isAllowPlainQrCode());
        QrCodeImageUtils.assertCjkFontAvailable();
 
        // 2. 非本地环境执行严格校验
        if (isLocalProfile()) {
            log.info("[init] 当前为本地环境({}),跳过标签合规配置的严格校验;traceBaseUrl={}",
                    PROFILE_LOCAL, mesProperties.getTraceBaseUrl());
            return;
        }
        validateTraceBaseUrl();
        validateOperatorName();
        log.info("[init] 二维码标签配置校验通过;traceBaseUrl={},生产经营者={}",
                mesProperties.getTraceBaseUrl(), MesProQrLabelBuilder.resolveOperatorName());
    }
 
    private boolean isLocalProfile() {
        return Arrays.asList(environment.getActiveProfiles()).contains(PROFILE_LOCAL);
    }
 
    /**
     * 校验追溯网址:必须为公众扫码可访问的地址
     */
    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
                    + ",公众扫码无法访问,请填写外网可访问的地址。");
        }
    }
 
    /**
     * 校验生产经营者名称:标签四要素之一,缺失即为不合规标签
     * <p>
     * 此处仅告警不阻断启动:生产经营者名称按主体区分,部分环境可能尚未配置,
     * 强制启动失败会影响无关功能。真正的拦截点在标签构建处(缺失时拒绝导出),
     * 因此不会出现"配置缺失却印出不合规标签"的情况。
     */
    private void validateOperatorName() {
        if (StrUtil.isBlank(MesProQrLabelBuilder.resolveOperatorName())) {
            log.warn("[init] mes.operator-name(生产经营者名称)未配置。该字段为种子标签四要素之一,"
                    + "缺失时将无法导出二维码标签,请尽快在配置中补充。");
        }
    }
 
    /**
     * 从 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);
    }
 
}