liu
9 小时以前 da93fafc417b4f590b6ab639ccbbe67c14c8753c
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
package cn.iocoder.yudao.module.mes.framework.config;
 
import cn.hutool.core.util.StrUtil;
 
import java.net.URI;
import java.util.Optional;
 
/**
 * 追溯网址校验器:判断一个追溯网址是否能印在种子标签上让消费者扫得开。
 * <p>
 * 追溯网址有两个来源——配置文件 {@code mes.trace-base-url} 与字典 {@code mes_trace_config}。
 * 两者必须走**同一套**校验规则,否则从字典进来的值就能绕过标签合规检查,
 * 印出消费者扫不开的标签。因此把规则收敛到这里,供启动校验与字典解析共同复用。
 * <p>
 * 注意:本类只做「地址本身是否可用」的静态判断,不含任何配置读取与异常抛出,便于单测。
 */
public final class MesTraceUrlValidator {
 
    private MesTraceUrlValidator() {
    }
 
    /**
     * 该地址是否可用于标签:空、占位符、本机地址、内网地址、无法解析的地址均视为不可用。
     *
     * @param url 追溯网址
     * @return true 表示可印在标签上
     */
    public static boolean isValid(String url) {
        return invalidReason(url).isEmpty();
    }
 
    /**
     * 判断追溯网址是否可用,不可用时给出人类可读的原因。
     *
     * @param url 追溯网址
     * @return 空表示可用;有值即为不可用的原因
     */
    public static Optional<String> invalidReason(String url) {
        String trimmed = StrUtil.trim(url);
        if (StrUtil.isEmpty(trimmed)) {
            return Optional.of("未配置。二维码标签必须包含追溯网址,"
                    + "请填写公众扫码可访问的 H5 溯源页地址。");
        }
        // 占位符与本机地址:多为从示例配置直接拷来、忘记替换,印出去消费者必然扫不开
        if (StrUtil.containsAnyIgnoreCase(trimmed, "your-domain", "example.com", "localhost")) {
            return Optional.of("仍为占位符或本机地址:" + trimmed
                    + ",印出的标签消费者将无法访问,请填写真实的外网地址。");
        }
        String host = extractHost(trimmed);
        if (StrUtil.isEmpty(host)) {
            return Optional.of("格式非法,无法解析主机名:" + trimmed);
        }
        if (isPrivateHost(host)) {
            return Optional.of("指向内网地址(" + host + "):" + trimmed
                    + ",公众扫码无法访问,请填写外网可访问的地址。");
        }
        return Optional.empty();
    }
 
    /**
     * 从 URL 中提取主机名,无法解析时返回 null
     */
    private static String extractHost(String url) {
        try {
            return URI.create(url).getHost();
        } catch (IllegalArgumentException ex) {
            return null;
        }
    }
 
    /**
     * 判断是否为本机 / 内网 / 链路本地地址
     */
    private static 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);
    }
 
}