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 调用点。
*
* 种子标签属于受国标约束的合规物料,一旦印出即无法召回,因此这里采取「启动即校验、不合法即拒绝启动」
* 的策略,而不是等到导出时才失败或静默降级:
*
* - 追溯网址必须为公众可访问地址,禁止内网 IP / 本机地址 / 未替换的占位符;
* - 生产经营者名称必填(标签四要素之一,缺失即为不合规标签);
* - 当前环境必须存在可渲染中文的字体(否则标签中文会变成方框)。
*
*/
@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
+ ",公众扫码无法访问,请填写外网可访问的地址。");
}
}
/**
* 校验生产经营者名称:标签四要素之一,缺失即为不合规标签
*
* 此处仅告警不阻断启动:生产经营者名称按主体区分,部分环境可能尚未配置,
* 强制启动失败会影响无关功能。真正的拦截点在标签构建处(缺失时拒绝导出),
* 因此不会出现"配置缺失却印出不合规标签"的情况。
*/
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);
}
}