#
Junjie
3 天以前 686fe55892de7bf8d206cddbead77a5fbdb0e091
#
7个文件已添加
16个文件已修改
1395 ■■■■ 已修改文件
src/main/java/com/zy/system/config/LicenseSchemaInitializer.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/controller/LicenseCreatorController.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/LicenseInfos.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/license/CustomLicenseManager.java 31 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/license/LicenseBindModel.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/license/LicenseBindingSupport.java 196 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/license/LicenseCheckListener.java 69 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/license/LicenseNodeCheck.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/license/LicenseRequestCode.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/license/LicenseUploadParam.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/license/LicenseUtils.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/entity/license/LicenseVerify.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/mapper/LicenseInfosMapper.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/service/LicenseInfosService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/service/impl/LicenseInfosServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/timer/LicenseTimer.java 82 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/LicenseInfosMapper.xml 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260307_add_request_code_to_sys_license_infos.sql 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/static/js/config/config.js 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/config/config.html 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/index.html 227 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/login.html 254 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/system/config/LicenseSchemaInitializer.java
New file
@@ -0,0 +1,49 @@
package com.zy.system.config;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.Statement;
@Component
public class LicenseSchemaInitializer {
    private final DataSource dataSource;
    public LicenseSchemaInitializer(DataSource dataSource) {
        this.dataSource = dataSource;
    }
    @PostConstruct
    public void init() {
        ensureColumn("sys_license_infos", "request_code", "VARCHAR(2048)");
    }
    private void ensureColumn(String tableName, String columnName, String columnDefinition) {
        try (Connection connection = dataSource.getConnection()) {
            if (hasColumn(connection, tableName, columnName)) {
                return;
            }
            try (Statement statement = connection.createStatement()) {
                statement.executeUpdate("ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + columnDefinition);
            }
        } catch (Exception ignored) {
        }
    }
    private boolean hasColumn(Connection connection, String tableName, String columnName) throws Exception {
        DatabaseMetaData metaData = connection.getMetaData();
        try (ResultSet resultSet = metaData.getColumns(connection.getCatalog(), null, tableName, null)) {
            while (resultSet.next()) {
                if (columnName.equalsIgnoreCase(resultSet.getString("COLUMN_NAME"))) {
                    return true;
                }
            }
        }
        return false;
    }
}
src/main/java/com/zy/system/controller/LicenseCreatorController.java
@@ -1,18 +1,21 @@
package com.zy.system.controller;
import com.core.common.R;
import com.zy.system.entity.LicenseInfos;
import com.zy.system.entity.license.*;
import com.zy.system.service.LicenseInfosService;
import com.zy.system.timer.LicenseTimer;
import de.schlichtherle.license.LicenseContent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
 *
@@ -24,12 +27,20 @@
    @Value("${license.subject}")
    private String licenseSubject;
    @Value("${license.publicAlias}")
    private String publicAlias;
    @Value("${license.storePass}")
    private String storePass;
    @Value("${license.licensePath}")
    private String licensePath;
    @Value("${license.publicKeysStorePath}")
    private String publicKeysStorePath;
    @Autowired
    private LicenseCheckListener licenseCheckListener;
    @Autowired
    private LicenseTimer licenseTimer;
    @Autowired
    private LicenseInfosService licenseInfosService;
    /**
     * 获取服务器硬件信息
     * @param osName 操作系统类型,如果为空则自动判断
@@ -37,6 +48,14 @@
    @RequestMapping(value = "/getServerInfos",produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    public LicenseCheck getServerInfos(@RequestParam(value = "osName",required = false) String osName) {
        return LicenseUtils.getServerInfos();
    }
    /**
     * 获取请求码。
     */
    @RequestMapping(value = "/getRequestCode")
    public R getRequestCode() {
        return R.ok(LicenseUtils.buildRequestCode(licenseSubject));
    }
    /**
@@ -51,42 +70,46 @@
        return R.ok().add(licenseDays);
    }
    @RequestMapping(value = "/updateLicense")
    public R updateLicense(@RequestParam("file") MultipartFile[] files){
        MultipartFile file = files[0];
        String licensePathFileName = this.getClass().getClassLoader().getResource(licensePath).getPath();
        File licensePathFile = new File(licensePathFileName);
        //服务器端保存的文件对象
        File serverFile = new File(licensePathFile.getPath());
        if (serverFile.exists()) {
            try {
                serverFile.delete();//存在文件,删除
            } catch (Exception e) {
                e.printStackTrace();
            }
    @RequestMapping(value = "/updateLicense", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    public R updateLicense(@RequestBody LicenseUploadParam param){
        if (param == null || param.getLicense() == null || param.getLicense().trim().isEmpty()) {
            return R.error("许可证内容不能为空");
        }
        try {
            //创建文件
            serverFile.createNewFile();
            //将上传的文件写入到服务器端文件内
            file.transferTo(serverFile);
        } catch (IOException e) {
            e.printStackTrace();
        String licenseBase64 = param.getLicense().trim();
        LicenseVerifyParam verifyParam = buildVerifyParam();
        LicenseVerify licenseVerify = new LicenseVerify();
        LicenseContent install = licenseVerify.install(verifyParam, licenseBase64);
        if (install == null) {
            return R.error("许可证内容无效");
        }
        //重新加载许可证
        boolean loadedLicense = licenseCheckListener.loadLicense();
        if (loadedLicense) {
            return R.ok();
        LicenseInfos licenseInfos = new LicenseInfos();
        licenseInfos.setLicense(licenseBase64);
        licenseInfos.setLicenseTime(formatLicenseTime(install));
        licenseInfos.setRequestCode(LicenseUtils.buildRequestCode(licenseSubject));
        licenseInfos.setCreateTime(new Date());
        if (!licenseInfosService.insert(licenseInfos)) {
            return R.error("许可证保存失败");
        }
        return R.error("许可证更新失败");
        boolean loadedLicense = licenseCheckListener.loadLicense(false);
        if (!loadedLicense) {
            return R.error("许可证激活失败");
        }
        licenseTimer.verify();
        if (!licenseTimer.getSystemSupport()) {
            return R.error("许可证校验失败");
        }
        return R.ok();
    }
    @RequestMapping(value = "/activate")
    public R activate() {
        licenseTimer.timer();
        if (!licenseTimer.getSystemSupport()) {
            return R.error("许可证激活失败");
        }
        return R.ok();
    }
@@ -95,4 +118,19 @@
        return R.ok(licenseSubject);
    }
}
    private LicenseVerifyParam buildVerifyParam() {
        LicenseVerifyParam param = new LicenseVerifyParam();
        param.setSubject(licenseSubject);
        param.setPublicAlias(publicAlias);
        param.setStorePass(storePass);
        param.setLicensePath(licensePath);
        param.setPublicKeysStorePath(publicKeysStorePath);
        return param;
    }
    private String formatLicenseTime(LicenseContent install) {
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return format.format(install.getNotBefore()) + "  -  " + format.format(install.getNotAfter());
    }
}
src/main/java/com/zy/system/entity/LicenseInfos.java
@@ -29,6 +29,10 @@
    private String licenseTime;
    @ApiModelProperty(value= "")
    @TableField("request_code")
    private String requestCode;
    @ApiModelProperty(value= "")
    @TableField("create_time")
    @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private Date createTime;
src/main/java/com/zy/system/entity/license/CustomLicenseManager.java
@@ -1,6 +1,5 @@
package com.zy.system.entity.license;
import com.core.common.Cools;
import de.schlichtherle.license.*;
import de.schlichtherle.xml.GenericCertificate;
import org.apache.logging.log4j.LogManager;
@@ -120,13 +119,23 @@
        //1. 首先调用父类的validate方法
        super.validate(content);
        //2. 然后校验自定义的License参数
        //License中可被允许的参数信息
        LicenseCheck expectedCheckModel = (LicenseCheck) content.getExtra();
        //当前服务器真实的参数信息
        Object extra = content.getExtra();
        LicenseCheck serverCheckModel = LicenseUtils.getServerInfos();
        if(expectedCheckModel != null && serverCheckModel != null){
        if (serverCheckModel == null) {
            throw new LicenseContentException("不能获取服务器硬件信息");
        }
        if (LicenseBindingSupport.isV2Extra(extra)) {
            LicenseBindModel bindModel = LicenseBindingSupport.parseBindModel(extra);
            if (!LicenseBindingSupport.matches(bindModel, serverCheckModel)) {
                throw new LicenseContentException("当前服务器不在授权节点范围内");
            }
            return;
        }
        LicenseCheck expectedCheckModel = LicenseBindingSupport.parseLegacyLicenseCheck(extra);
        if(expectedCheckModel != null){
            //校验IP地址
            if(!checkIpAddress(expectedCheckModel.getIpAddress(),serverCheckModel.getIpAddress())){
                throw new LicenseContentException("当前服务器的IP没在授权范围内");
@@ -210,8 +219,8 @@
     * 校验当前服务器硬件(主板、CPU等)序列号是否在可允许范围内
     */
    private boolean checkSerial(String expectedSerial,String serverSerial){
        if(!Cools.isEmpty(expectedSerial)){
            if(!Cools.isEmpty(serverSerial)){
        if(!isBlank(expectedSerial)){
            if(!isBlank(serverSerial)){
                if(expectedSerial.equals(serverSerial)){
                    return true;
                }
@@ -222,4 +231,8 @@
        }
    }
}
    private boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }
}
src/main/java/com/zy/system/entity/license/LicenseBindModel.java
New file
@@ -0,0 +1,21 @@
package com.zy.system.entity.license;
import lombok.Data;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
@Data
public class LicenseBindModel implements Serializable {
    private static final long serialVersionUID = 7064744215406459726L;
    private Integer version = 2;
    private String bindMode = "MULTI_NODE";
    private String matchMode = "ANY";
    private List<LicenseNodeCheck> nodes = new ArrayList<>();
}
src/main/java/com/zy/system/entity/license/LicenseBindingSupport.java
New file
@@ -0,0 +1,196 @@
package com.zy.system.entity.license;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class LicenseBindingSupport {
    private LicenseBindingSupport() {
    }
    public static boolean isV2Extra(Object extra) {
        if (extra == null) {
            return false;
        }
        if (extra instanceof LicenseBindModel) {
            return true;
        }
        if (extra instanceof JSONObject) {
            return isV2Json((JSONObject) extra);
        }
        if (extra instanceof String) {
            String text = ((String) extra).trim();
            if (isBlank(text) || !text.startsWith("{")) {
                return false;
            }
            return isV2Json(JSON.parseObject(text));
        }
        return false;
    }
    public static LicenseBindModel parseBindModel(Object extra) {
        if (extra == null) {
            return null;
        }
        if (extra instanceof LicenseBindModel) {
            return normalizeBindModel((LicenseBindModel) extra);
        }
        if (extra instanceof JSONObject) {
            return normalizeBindModel(((JSONObject) extra).toJavaObject(LicenseBindModel.class));
        }
        if (extra instanceof String) {
            String text = ((String) extra).trim();
            if (isBlank(text)) {
                return null;
            }
            return normalizeBindModel(JSON.parseObject(text, LicenseBindModel.class));
        }
        return normalizeBindModel(JSON.parseObject(JSON.toJSONString(extra), LicenseBindModel.class));
    }
    public static LicenseCheck parseLegacyLicenseCheck(Object extra) {
        if (extra == null) {
            return null;
        }
        if (extra instanceof LicenseCheck) {
            return (LicenseCheck) extra;
        }
        if (extra instanceof JSONObject) {
            return ((JSONObject) extra).toJavaObject(LicenseCheck.class);
        }
        if (extra instanceof String) {
            String text = ((String) extra).trim();
            if (isBlank(text)) {
                return null;
            }
            return JSON.parseObject(text, LicenseCheck.class);
        }
        return JSON.parseObject(JSON.toJSONString(extra), LicenseCheck.class);
    }
    public static boolean matches(LicenseBindModel licenseBind, LicenseCheck serverCheck) {
        if (licenseBind == null) {
            return false;
        }
        if ("UNLIMITED".equalsIgnoreCase(trimToEmpty(licenseBind.getBindMode()))) {
            return true;
        }
        List<LicenseNodeCheck> nodes = licenseBind.getNodes();
        if (nodes == null || nodes.isEmpty()) {
            return true;
        }
        for (LicenseNodeCheck node : nodes) {
            if (matchesNode(node, serverCheck)) {
                return true;
            }
        }
        return false;
    }
    private static boolean isV2Json(JSONObject jsonObject) {
        if (jsonObject == null) {
            return false;
        }
        return jsonObject.containsKey("nodes")
                || jsonObject.containsKey("bindMode")
                || jsonObject.containsKey("matchMode")
                || Integer.valueOf(2).equals(jsonObject.getInteger("version"));
    }
    private static LicenseBindModel normalizeBindModel(LicenseBindModel source) {
        if (source == null) {
            return null;
        }
        LicenseBindModel target = new LicenseBindModel();
        target.setVersion(source.getVersion() == null ? 2 : source.getVersion());
        target.setBindMode(isBlank(source.getBindMode()) ? "MULTI_NODE" : source.getBindMode().trim());
        target.setMatchMode(isBlank(source.getMatchMode()) ? "ANY" : source.getMatchMode().trim());
        List<LicenseNodeCheck> normalizedNodes = new ArrayList<>();
        if (source.getNodes() != null) {
            for (LicenseNodeCheck node : source.getNodes()) {
                if (node == null) {
                    continue;
                }
                LicenseNodeCheck normalizedNode = new LicenseNodeCheck();
                normalizedNode.setNodeId(trimToEmpty(node.getNodeId()));
                normalizedNode.setIpAddress(normalizeList(node.getIpAddress(), false));
                normalizedNode.setMacAddress(normalizeList(node.getMacAddress(), true));
                normalizedNode.setCpuSerial(trimToEmpty(node.getCpuSerial()));
                normalizedNode.setMainBoardSerial(trimToEmpty(node.getMainBoardSerial()));
                normalizedNodes.add(normalizedNode);
            }
        }
        target.setNodes(normalizedNodes);
        return target;
    }
    private static boolean matchesNode(LicenseNodeCheck node, LicenseCheck serverCheck) {
        if (node == null || serverCheck == null) {
            return false;
        }
        if (!checkList(node.getIpAddress(), serverCheck.getIpAddress())) {
            return false;
        }
        if (!checkList(node.getMacAddress(), serverCheck.getMacAddress())) {
            return false;
        }
        if (!checkSerial(node.getMainBoardSerial(), serverCheck.getMainBoardSerial())) {
            return false;
        }
        return checkSerial(node.getCpuSerial(), serverCheck.getCpuSerial());
    }
    private static boolean checkList(List<String> expectedList, List<String> actualList) {
        if (expectedList == null || expectedList.isEmpty()) {
            return true;
        }
        if (actualList == null || actualList.isEmpty()) {
            return false;
        }
        for (String expected : expectedList) {
            if (!isBlank(expected) && actualList.contains(expected.trim())) {
                return true;
            }
        }
        return false;
    }
    private static boolean checkSerial(String expectedSerial, String actualSerial) {
        if (isBlank(expectedSerial)) {
            return true;
        }
        return !isBlank(actualSerial) && expectedSerial.equals(actualSerial);
    }
    private static List<String> normalizeList(List<String> values, boolean upperCase) {
        List<String> result = new ArrayList<>();
        if (values == null || values.isEmpty()) {
            return result;
        }
        for (String value : values) {
            if (isBlank(value)) {
                continue;
            }
            String normalized = value.trim();
            normalized = upperCase ? normalized.toUpperCase() : normalized.toLowerCase();
            if (!result.contains(normalized)) {
                result.add(normalized);
            }
        }
        Collections.sort(result);
        return result;
    }
    private static boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }
    private static String trimToEmpty(String value) {
        return value == null ? "" : value.trim();
    }
}
src/main/java/com/zy/system/entity/license/LicenseCheckListener.java
@@ -16,6 +16,9 @@
import org.springframework.stereotype.Component;
import java.io.File;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.Base64;
import java.util.Date;
/**
@@ -72,12 +75,18 @@
    //加载证书
    public boolean loadLicense() {
        return loadLicense(true);
    }
    public boolean loadLicense(boolean fetchRemote) {
        if(!Cools.isEmpty(licensePath)){
            logger.info("++++++++ 开始加载许可证 ++++++++");
            try {
                licenseTimer.getRemoteLicense();
            } catch (Exception e) {
            if (fetchRemote) {
                try {
                    licenseTimer.getRemoteLicense();
                } catch (Exception e) {
                }
            }
            try {
@@ -89,15 +98,23 @@
                param.setPublicKeysStorePath(publicKeysStorePath);
                LicenseVerify licenseVerify = new LicenseVerify();
                String requestCode = LicenseUtils.buildRequestCode(subject);
                LicenseInfos latestLicense = licenseInfosService.getLatestLicenseByRequestCode(requestCode);
                LicenseInfos latestLicense = licenseInfosService.getLatestLicense();
                if (latestLicense == null) {
                LicenseContent install = null;
                if (latestLicense != null) {
                    install = licenseVerify.install(param, latestLicense.getLicense());
                }
                if (install == null) {
                    install = licenseVerify.install(param);
                    if (install != null) {
                        cacheLocalLicense(requestCode, install);
                    }
                }
                if (install == null) {
                    logger.info("许可证不存在");
                    return false;
                }
                //安装证书
                LicenseContent install = licenseVerify.install(param, latestLicense.getLicense());
                logger.info("++++++++ 许可证加载结束 ++++++++");
@@ -127,4 +144,38 @@
        licenseTimer.setSystemSupport(false);
        return false;
    }
}
    private void cacheLocalLicense(String requestCode, LicenseContent install) {
        try {
            File licenseFile = resolveLicenseFile();
            if (licenseFile == null || !licenseFile.exists()) {
                return;
            }
            LicenseInfos licenseInfos = new LicenseInfos();
            licenseInfos.setLicense(Base64.getEncoder().encodeToString(Files.readAllBytes(licenseFile.toPath())));
            licenseInfos.setLicenseTime(formatLicenseTime(install));
            licenseInfos.setRequestCode(requestCode);
            licenseInfos.setCreateTime(new Date());
            licenseInfosService.insert(licenseInfos);
        } catch (Exception ignored) {
        }
    }
    private File resolveLicenseFile() {
        try {
            if (this.getClass().getClassLoader().getResource(licensePath) != null) {
                return new File(this.getClass().getClassLoader().getResource(licensePath).toURI());
            }
        } catch (Exception ignored) {
        }
        return new File(licensePath);
    }
    private String formatLicenseTime(LicenseContent install) {
        if (install == null) {
            return "";
        }
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        return format.format(install.getNotBefore()) + "  -  " + format.format(install.getNotAfter());
    }
}
src/main/java/com/zy/system/entity/license/LicenseNodeCheck.java
New file
@@ -0,0 +1,22 @@
package com.zy.system.entity.license;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
@Data
public class LicenseNodeCheck implements Serializable {
    private static final long serialVersionUID = 3629488116939928951L;
    private String nodeId;
    private List<String> ipAddress;
    private List<String> macAddress;
    private String cpuSerial;
    private String mainBoardSerial;
}
src/main/java/com/zy/system/entity/license/LicenseRequestCode.java
New file
@@ -0,0 +1,19 @@
package com.zy.system.entity.license;
import lombok.Data;
import java.io.Serializable;
@Data
public class LicenseRequestCode implements Serializable {
    private static final long serialVersionUID = -6632394365188004836L;
    private Integer version = 2;
    private String subject;
    private String requestTime;
    private LicenseBindModel licenseBind;
}
src/main/java/com/zy/system/entity/license/LicenseUploadParam.java
New file
@@ -0,0 +1,9 @@
package com.zy.system.entity.license;
import lombok.Data;
@Data
public class LicenseUploadParam {
    private String license;
}
src/main/java/com/zy/system/entity/license/LicenseUtils.java
@@ -1,5 +1,15 @@
package com.zy.system.entity.license;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collections;
import java.util.List;
public class LicenseUtils {
    /**
@@ -23,4 +33,98 @@
        return abstractServerInfos.getServerInfos();
    }
    /**
     * 生成请求码,请求码中包含许可证名称和当前服务器的硬件信息。
     */
    public static String buildRequestCode(String subject) {
        return buildRequestCode(subject, getServerInfos());
    }
    public static String buildRequestCode(String subject, LicenseCheck licenseCheck) {
        if (isBlank(subject)) {
            throw new IllegalArgumentException("许可证名称不能为空");
        }
        LicenseCheck normalized = normalizeLicenseCheck(licenseCheck == null ? new LicenseCheck() : licenseCheck);
        JSONObject payload = new JSONObject(true);
        payload.put("version", 2);
        payload.put("subject", subject);
        payload.put("licenseBind", buildBindModel(normalized));
        return Base64.getEncoder().encodeToString(JSON.toJSONString(payload).getBytes(StandardCharsets.UTF_8));
    }
    private static JSONObject buildBindModel(LicenseCheck licenseCheck) {
        JSONObject bindModel = new JSONObject(true);
        bindModel.put("version", 2);
        bindModel.put("bindMode", "MULTI_NODE");
        bindModel.put("matchMode", "ANY");
        JSONObject node = new JSONObject(true);
        node.put("nodeId", getNodeId());
        node.put("ipAddress", toJsonArray(licenseCheck.getIpAddress()));
        node.put("macAddress", toJsonArray(licenseCheck.getMacAddress()));
        node.put("cpuSerial", trimToEmpty(licenseCheck.getCpuSerial()));
        node.put("mainBoardSerial", trimToEmpty(licenseCheck.getMainBoardSerial()));
        JSONArray nodes = new JSONArray();
        nodes.add(node);
        bindModel.put("nodes", nodes);
        return bindModel;
    }
    private static LicenseCheck normalizeLicenseCheck(LicenseCheck source) {
        LicenseCheck target = new LicenseCheck();
        target.setIpAddress(normalizeList(source.getIpAddress(), false));
        target.setMacAddress(normalizeList(source.getMacAddress(), true));
        target.setCpuSerial(trimToEmpty(source.getCpuSerial()));
        target.setMainBoardSerial(trimToEmpty(source.getMainBoardSerial()));
        return target;
    }
    private static List<String> normalizeList(List<String> values, boolean upperCase) {
        List<String> result = new ArrayList<>();
        if (values == null || values.isEmpty()) {
            return result;
        }
        for (String value : values) {
            if (isBlank(value)) {
                continue;
            }
            String normalized = value.trim();
            normalized = upperCase ? normalized.toUpperCase() : normalized.toLowerCase();
            if (!result.contains(normalized)) {
                result.add(normalized);
            }
        }
        Collections.sort(result);
        return result;
    }
    private static JSONArray toJsonArray(List<String> values) {
        JSONArray array = new JSONArray();
        if (values != null && !values.isEmpty()) {
            array.addAll(values);
        }
        return array;
    }
    private static String getNodeId() {
        try {
            String hostName = InetAddress.getLocalHost().getHostName();
            if (!isBlank(hostName)) {
                return hostName.trim();
            }
        } catch (Exception ignored) {
        }
        return "node-1";
    }
    private static boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }
    private static String trimToEmpty(String value) {
        return value == null ? "" : value.trim();
    }
}
src/main/java/com/zy/system/entity/license/LicenseVerify.java
@@ -6,6 +6,7 @@
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.text.DateFormat;
@@ -35,6 +36,28 @@
            File tempFileFromBase64 = createTempFileFromBase64(license);
            result = licenseManager.install(tempFileFromBase64);
            logger.info(MessageFormat.format("许可证加载成功,许可证有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
        } catch (Exception e) {
            logger.error("许可证加载失败!", e);
        }
        return result;
    }
    public synchronized LicenseContent install(LicenseVerifyParam param) {
        LicenseContent result = null;
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        try {
            LicenseParam licenseParam = initLicenseParam(param);
            LicenseManager licenseManager = LicenseManagerHolder.getInstance(licenseParam);
            licenseManager.uninstall();
            File licenseFile = resolveLicenseFile(param.getLicensePath());
            if (licenseFile == null || !licenseFile.exists()) {
                return null;
            }
            result = licenseManager.install(licenseFile);
            logger.info(MessageFormat.format("许可证加载成功,许可证有效期:{0} - {1}", format.format(result.getNotBefore()), format.format(result.getNotAfter())));
        } catch (Exception e) {
            logger.error("许可证加载失败!", e);
@@ -123,4 +146,15 @@
        return base64ToTempFile(base64Data, "temp_license_", ".bin");
    }
}
    private File resolveLicenseFile(String licensePath) {
        try {
            URL url = this.getClass().getClassLoader().getResource(licensePath);
            if (url != null) {
                return new File(url.toURI());
            }
        } catch (Exception ignored) {
        }
        return new File(licensePath);
    }
}
src/main/java/com/zy/system/mapper/LicenseInfosMapper.java
@@ -2,6 +2,7 @@
import com.zy.system.entity.LicenseInfos;
import com.baomidou.mybatisplus.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@@ -11,4 +12,6 @@
    LicenseInfos getLatestLicense();
    LicenseInfos getLatestLicenseByRequestCode(@Param("requestCode") String requestCode);
}
src/main/java/com/zy/system/service/LicenseInfosService.java
@@ -7,4 +7,6 @@
    LicenseInfos getLatestLicense();
    LicenseInfos getLatestLicenseByRequestCode(String requestCode);
}
src/main/java/com/zy/system/service/impl/LicenseInfosServiceImpl.java
@@ -13,4 +13,9 @@
    public LicenseInfos getLatestLicense() {
        return this.baseMapper.getLatestLicense();
    }
    @Override
    public LicenseInfos getLatestLicenseByRequestCode(String requestCode) {
        return this.baseMapper.getLatestLicenseByRequestCode(requestCode);
    }
}
src/main/java/com/zy/system/timer/LicenseTimer.java
@@ -52,6 +52,12 @@
    @Value("${license.publicKeysStorePath}")
    private String publicKeysStorePath;
    /**
     * 许可证服务端地址。
     */
    @Value("${license.remoteServerUrl:http://net.zoneyung.net:9999/license}")
    private String remoteServerUrl;
    @Autowired
    private LicenseInfosService licenseInfosService;
@@ -73,24 +79,18 @@
    public void getRemoteLicense() {
        try {
            String requestCode = LicenseUtils.buildRequestCode(subject);
            LicenseCheck serverInfos = LicenseUtils.getServerInfos();
            HashMap<String, Object> map = new HashMap<>();
            map.put("subject", subject);
            map.put("licenseCheck", serverInfos);
            String response = new HttpHandler.Builder()
                    .setUri("http://net.zoneyung.net:9999/license")
                    .setPath("/remoteQueryLicense")
                    .setJson(JSON.toJSONString(map))
                    .build()
                    .doPost();
            JSONObject jsonObject = JSON.parseObject(response);
            if (jsonObject.getString("result").equals("ok")) {
            JSONObject response = requestRemoteLicense(buildRequestCodePayload(requestCode));
            if (!isSuccess(response)) {
                response = requestRemoteLicense(buildLegacyPayload(serverInfos));
            }
            if (isSuccess(response)) {
                LicenseInfos licenseInfos = new LicenseInfos();
                licenseInfos.setLicense(jsonObject.getString("data"));
                licenseInfos.setLicense(response.getString("data"));
                licenseInfos.setCreateTime(new Date());
                licenseInfos.setLicenseTime(jsonObject.getString("licenseTime"));
                licenseInfos.setLicenseTime(response.getString("licenseTime"));
                licenseInfos.setRequestCode(requestCode);
                licenseInfosService.insert(licenseInfos);
            }
        } catch (Exception e) {
@@ -98,14 +98,42 @@
        }
    }
    public void verify() {
        LicenseInfos latestLicense = licenseInfosService.getLatestLicense();
        if (latestLicense == null) {
            setLicenseDays(0);
            setSystemSupport(false);
            return;
    private JSONObject requestRemoteLicense(String json) {
        try {
            String response = new HttpHandler.Builder()
                    .setUri(remoteServerUrl)
                    .setPath("/remoteQueryLicense")
                    .setJson(json)
                    .build()
                    .doPost();
            if (response == null || response.trim().isEmpty()) {
                return null;
            }
            return JSON.parseObject(response);
        } catch (Exception e) {
            return null;
        }
    }
    private String buildRequestCodePayload(String requestCode) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("subject", subject);
        map.put("requestCode", requestCode);
        return JSON.toJSONString(map);
    }
    private String buildLegacyPayload(LicenseCheck serverInfos) {
        HashMap<String, Object> map = new HashMap<>();
        map.put("subject", subject);
        map.put("licenseCheck", serverInfos);
        return JSON.toJSONString(map);
    }
    private boolean isSuccess(JSONObject jsonObject) {
        return jsonObject != null && "ok".equalsIgnoreCase(jsonObject.getString("result"));
    }
    public void verify() {
        LicenseVerifyParam param = new LicenseVerifyParam();
        param.setSubject(subject);
        param.setPublicAlias(publicAlias);
@@ -113,10 +141,18 @@
        param.setLicensePath(licensePath);
        param.setPublicKeysStorePath(publicKeysStorePath);
        String requestCode = LicenseUtils.buildRequestCode(subject);
        LicenseInfos latestLicense = licenseInfosService.getLatestLicenseByRequestCode(requestCode);
        // 验证许可证是否有效
        LicenseVerify licenseVerify = new LicenseVerify();
        // 安装证书
        LicenseContent install = licenseVerify.install(param, latestLicense.getLicense());
        LicenseContent install = null;
        if (latestLicense != null) {
            install = licenseVerify.install(param, latestLicense.getLicense());
        }
        if (install == null) {
            install = licenseVerify.install(param);
        }
        if (install != null) {
            Date start = new Date();
src/main/resources/application.yml
@@ -1,6 +1,6 @@
# 系统版本信息
app:
  version: 1.0.5.1
  version: 1.0.5.2
  version-type: dev  # prd 或 dev
server:
@@ -66,6 +66,7 @@
  storePass: public_zhongyang_123456789
  licensePath: license.lic
  publicKeysStorePath: publicCerts.keystore
  remoteServerUrl: http://net.zoneyung.net:9999/license
deviceExecuteConfig:
  # 每个线程管控设备执行数量
src/main/resources/mapper/LicenseInfosMapper.xml
@@ -7,6 +7,7 @@
        <id column="id" property="id" />
        <result column="license" property="license" />
        <result column="license_time" property="licenseTime" />
        <result column="request_code" property="requestCode" />
        <result column="create_time" property="createTime" />
    </resultMap>
@@ -15,4 +16,8 @@
        select * from sys_license_infos order by create_time desc limit 0,1
    </select>
    <select id="getLatestLicenseByRequestCode" resultMap="BaseResultMap">
        select * from sys_license_infos where request_code = #{requestCode} order by create_time desc limit 0,1
    </select>
</mapper>
src/main/resources/sql/20260307_add_request_code_to_sys_license_infos.sql
New file
@@ -0,0 +1,42 @@
-- sys_license_infos 增加 request_code 字段
-- 用途:按当前机器请求码筛选并激活对应许可证
-- 适用数据库:MySQL
SET @current_db := DATABASE();
SET @column_exists := (
  SELECT COUNT(1)
  FROM information_schema.COLUMNS
  WHERE TABLE_SCHEMA = @current_db
    AND TABLE_NAME = 'sys_license_infos'
    AND COLUMN_NAME = 'request_code'
);
SET @add_column_sql := IF(
  @column_exists = 0,
  'ALTER TABLE sys_license_infos ADD COLUMN request_code VARCHAR(2048) NULL COMMENT ''许可证请求码'' AFTER license_time',
  'SELECT ''column request_code already exists'' '
);
PREPARE stmt_add_column FROM @add_column_sql;
EXECUTE stmt_add_column;
DEALLOCATE PREPARE stmt_add_column;
SET @index_exists := (
  SELECT COUNT(1)
  FROM information_schema.STATISTICS
  WHERE TABLE_SCHEMA = @current_db
    AND TABLE_NAME = 'sys_license_infos'
    AND INDEX_NAME = 'idx_sys_license_infos_request_code_create_time'
);
SET @add_index_sql := IF(
  @index_exists = 0,
  'ALTER TABLE sys_license_infos ADD INDEX idx_sys_license_infos_request_code_create_time (request_code(191), create_time)',
  'SELECT ''index idx_sys_license_infos_request_code_create_time already exists'' '
);
PREPARE stmt_add_index FROM @add_index_sql;
EXECUTE stmt_add_index;
DEALLOCATE PREPARE stmt_add_index;
SHOW COLUMNS FROM sys_license_infos LIKE 'request_code';
SHOW INDEX FROM sys_license_infos WHERE Key_name = 'idx_sys_license_infos_request_code_create_time';
src/main/webapp/static/js/config/config.js
@@ -187,117 +187,6 @@
                    });
                });
                break;
            case 'serverInfo':
                $.ajax({
                    url: baseUrl + "/license/getServerInfos",
                    headers: { 'token': localStorage.getItem('token') },
                    method: 'GET',
                    success: function (res) {
                        var pretty = '';
                        try {
                            pretty = JSON.stringify(res, null, 2);
                        } catch (e) {
                            pretty = res;
                        }
                        var html = ''
                            + '<div style="padding:15px 20px 5px 20px;">'
                            + '<div style="font-weight:600;margin-bottom:8px;">系统配置信息</div>'
                            + '<pre id="server-info-pre" style="background:#f7f7f7;border:1px solid #e6e6e6;border-radius:6px;padding:12px;white-space:pre-wrap;word-wrap:break-word;max-height:360px;overflow:auto;">'
                            + pretty
                            + '</pre>'
                            + '<div class="layui-btn-container" style="text-align:right;margin-top:6px;">'
                            + '<button class="layui-btn layui-btn-primary" id="copy-server-info">复制</button>'
                            + '</div>'
                            + '</div>';
                        layer.open({
                            type: 1,
                            title: '获取系统配置',
                            area: ['640px', '480px'],
                            shadeClose: true,
                            content: html,
                            success: function (layero, index) {
                                layero.find('#copy-server-info').on('click', function () {
                                    var text = layero.find('#server-info-pre').text();
                                    if (navigator.clipboard && navigator.clipboard.writeText) {
                                        navigator.clipboard.writeText(text).then(function () {
                                            layer.msg('已复制到剪贴板');
                                        }).catch(function () {
                                            try {
                                                var textarea = document.createElement('textarea');
                                                textarea.value = text;
                                                textarea.style.position = 'fixed';
                                                textarea.style.opacity = '0';
                                                document.body.appendChild(textarea);
                                                textarea.select();
                                                document.execCommand('copy');
                                                document.body.removeChild(textarea);
                                                layer.msg('已复制到剪贴板');
                                            } catch (err) {
                                                layer.msg('复制失败');
                                            }
                                        });
                                    } else {
                                        try {
                                            var textarea = document.createElement('textarea');
                                            textarea.value = text;
                                            textarea.style.position = 'fixed';
                                            textarea.style.opacity = '0';
                                            document.body.appendChild(textarea);
                                            textarea.select();
                                            document.execCommand('copy');
                                            document.body.removeChild(textarea);
                                            layer.msg('已复制到剪贴板');
                                        } catch (err) {
                                            layer.msg('复制失败');
                                        }
                                    }
                                });
                            }
                        });
                    },
                    error: function () {
                        layer.msg('获取系统配置信息失败');
                    }
                });
                break;
            case 'activate':
                layer.confirm('确定执行一键激活吗', function () {
                    $.ajax({
                        url: baseUrl + "/license/activate",
                        headers: { 'token': localStorage.getItem('token') },
                        method: 'POST',
                        success: function (res) {
                            if (res.code === 200) {
                                layer.msg('激活成功');
                            } else if (res.code === 403) {
                                top.location.href = baseUrl + "/";
                            } else {
                                layer.msg(res.msg)
                            }
                        },
                        error: function () {
                            layer.msg('激活失败');
                        }
                    });
                });
                break;
            case 'projectName':
                $.ajax({
                    url: baseUrl + "/license/getProjectName",
                    headers: { 'token': localStorage.getItem('token') },
                    method: 'GET',
                    success: function (res) {
                        if (res.code === 200) {
                            layer.alert(res.msg);
                        } else {
                            layer.msg(res.msg)
                        }
                    },
                    error: function () {
                        layer.msg('获取项目名称失败');
                    }
                });
                break;
        }
    });
src/main/webapp/views/config/config.html
@@ -46,9 +46,6 @@
        <button class="layui-btn layui-btn-sm" id="btn-delete" lay-event="deleteData">删除</button>
        <button class="layui-btn layui-btn-primary layui-btn-sm" id="btn-export" lay-event="exportData">导出</button>
        <button class="layui-btn layui-btn-warm layui-btn-sm" id="btn-refresh-cache" lay-event="refreshCache">刷新缓存</button>
        <button class="layui-btn layui-btn-normal layui-btn-sm" id="btn-project-name" lay-event="projectName">获取项目名称</button>
        <button class="layui-btn layui-btn-normal layui-btn-sm" id="btn-server-info" lay-event="serverInfo">获取系统配置</button>
        <button class="layui-btn layui-btn-normal layui-btn-sm" id="btn-activate" lay-event="activate">一键激活</button>
    </div>
</script>
src/main/webapp/views/index.html
@@ -228,6 +228,44 @@
      color: rgba(255, 255, 255, 0.58);
    }
    .aside-loading {
      padding: 4px 12px 12px;
      box-sizing: border-box;
    }
    .aside-skeleton-item {
      position: relative;
      height: 46px;
      margin: 0 10px 6px;
      overflow: hidden;
      border-radius: 10px;
      background: rgba(255, 255, 255, 0.08);
    }
    .aside-skeleton-item::after {
      content: "";
      position: absolute;
      inset: 0;
      transform: translateX(-100%);
      background: linear-gradient(90deg,
      rgba(255, 255, 255, 0) 0%,
      rgba(255, 255, 255, 0.16) 48%,
      rgba(255, 255, 255, 0) 100%);
      animation: asideSkeletonShimmer 1.25s ease-in-out infinite;
    }
    .aside-skeleton-item:nth-child(3n) {
      width: calc(100% - 28px);
    }
    .aside-skeleton-item:nth-child(4n + 2) {
      width: calc(100% - 44px);
    }
    .aside-skeleton-item:nth-child(5n) {
      width: calc(100% - 36px);
    }
    .aside-footer {
      padding: 14px 16px 16px;
      border-top: 1px solid rgba(255, 255, 255, 0.08);
@@ -450,6 +488,12 @@
      }
    }
    @keyframes asideSkeletonShimmer {
      100% {
        transform: translateX(100%);
      }
    }
    .ai-drawer-layer {
      box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15) !important;
      border-radius: 8px 0 0 8px !important;
@@ -500,42 +544,52 @@
        </el-input>
      </div>
      <el-scrollbar class="aside-scroll" v-loading="menuLoading">
        <el-menu
            ref="sideMenu"
            class="side-menu"
            :default-active="activeMenuKey"
            :collapse="isCollapse"
            :collapse-transition="false"
            :default-openeds="defaultOpeneds"
            unique-opened
            background-color="transparent"
            text-color="#c6d1df"
            active-text-color="#ffffff">
          <el-submenu
              v-for="group in filteredMenus"
              :key="'group-' + group.menuId"
              :index="'group-' + group.menuId">
            <template slot="title">
              <i :class="resolveMenuIcon(group.menuCode)"></i>
              <span>{{ group.menu }}</span>
            </template>
            <el-menu-item
                v-for="item in group.subMenu"
                :key="item.tabKey"
                :index="item.tabKey"
                @click="handleMenuSelect(group, item)">
              {{ item.name }}
            </el-menu-item>
          </el-submenu>
        </el-menu>
        <div class="aside-empty" v-if="!menuLoading && filteredMenus.length === 0">
          <el-empty
              :image-size="80"
              :description="menuKeyword ? '没有匹配菜单' : '当前账号没有可用菜单'">
          </el-empty>
      <el-scrollbar class="aside-scroll">
        <div v-if="menuLoading" class="aside-loading" aria-hidden="true">
          <div
              v-for="n in 8"
              :key="'menu-skeleton-' + n"
              class="aside-skeleton-item">
          </div>
        </div>
        <template v-else>
          <el-menu
              ref="sideMenu"
              class="side-menu"
              :default-active="activeMenuKey"
              :collapse="isCollapse"
              :collapse-transition="false"
              :default-openeds="defaultOpeneds"
              unique-opened
              background-color="transparent"
              text-color="#c6d1df"
              active-text-color="#ffffff">
            <el-submenu
                v-for="group in filteredMenus"
                :key="'group-' + group.menuId"
                :index="'group-' + group.menuId">
              <template slot="title">
                <i :class="resolveMenuIcon(group.menuCode)"></i>
                <span>{{ group.menu }}</span>
              </template>
              <el-menu-item
                  v-for="item in group.subMenu"
                  :key="item.tabKey"
                  :index="item.tabKey"
                  @click="handleMenuSelect(group, item)">
                {{ item.name }}
              </el-menu-item>
            </el-submenu>
          </el-menu>
          <div class="aside-empty" v-if="filteredMenus.length === 0">
            <el-empty
                :image-size="80"
                :description="menuKeyword ? '没有匹配菜单' : '当前账号没有可用菜单'">
            </el-empty>
          </div>
        </template>
      </el-scrollbar>
      <div class="aside-footer" v-show="!isCollapse">
@@ -606,7 +660,6 @@
            class="page-tabs"
            v-model="activeTab"
            type="card"
            @tab-click="handleTabClick"
            @tab-remove="removeTab">
          <el-tab-pane
              v-for="tab in tabs"
@@ -695,7 +748,7 @@
    data: function () {
      return {
        isCollapse: false,
        menuLoading: false,
        menuLoading: true,
        pageLoading: true,
        loadingText: "正在加载页面...",
        menuKeyword: "",
@@ -712,6 +765,8 @@
        fakeVisible: false,
        fakeRunning: false,
        fakeStatusInterval: null,
        menuSyncVersion: 0,
        menuSyncTimer: null,
        userName: localStorage.getItem(USER_STORAGE_KEY) || "管理员",
        aiLayerIndex: null,
        aiTipIndex: null
@@ -812,7 +867,7 @@
    watch: {
      activeTab: function () {
        var tab = this.getTabByName(this.activeTab);
        this.activeMenuKey = tab ? (tab.menuKey || this.findMenuKeyByUrl(tab.url)) : "";
        this.syncMenuStateByUrl(tab ? tab.url : HOME_TAB_CONFIG.url);
        this.pageLoading = !!(tab && !tab.loaded);
        if (this.pageLoading) {
          this.loadingText = "正在加载 “" + tab.title + "” ...";
@@ -847,6 +902,10 @@
      if (this.userSyncTimer) {
        clearInterval(this.userSyncTimer);
        this.userSyncTimer = null;
      }
      if (this.menuSyncTimer) {
        clearTimeout(this.menuSyncTimer);
        this.menuSyncTimer = null;
      }
      if (this.aiTipIndex) {
        layer.close(this.aiTipIndex);
@@ -961,6 +1020,9 @@
      hasTab: function (name) {
        return !!this.getTabByName(name);
      },
      isHomeTabUrl: function (url) {
        return this.resolveViewSrc(url || HOME_TAB_CONFIG.url) === this.resolveViewSrc(HOME_TAB_CONFIG.url);
      },
      getTabByName: function (name) {
        var i;
        for (i = 0; i < this.tabs.length; i++) {
@@ -995,7 +1057,7 @@
        this.loadingText = "正在加载 “" + tab.title + "” ...";
        this.pageLoading = !tab.loaded;
        this.activeTab = tab.name;
        this.activeMenuKey = tab.menuKey || this.findMenuKeyByUrl(tab.url);
        this.syncMenuStateByUrl(tab.url);
      },
      openHomeTab: function () {
        this.addOrActivateTab(HOME_TAB_CONFIG);
@@ -1006,7 +1068,7 @@
      closeAllTabs: function () {
        this.tabs = [this.createTab(HOME_TAB_CONFIG)];
        this.activeTab = HOME_TAB_CONFIG.url;
        this.activeMenuKey = "";
        this.syncMenuStateByUrl(HOME_TAB_CONFIG.url);
        this.pageLoading = true;
        this.loadingText = "正在加载 “控制中心” ...";
        this.persistTabs();
@@ -1048,9 +1110,6 @@
        this.activeTab = nextTabName;
        this.persistTabs();
      },
      handleTabClick: function () {
        this.activeMenuKey = this.findMenuKeyByUrl(this.activeTabUrl);
      },
      handleFrameLoad: function (name) {
        var tab = this.getTabByName(name);
@@ -1099,6 +1158,24 @@
            item = group.subMenu[j];
            if (item.url === normalized) {
              return item.tabKey;
            }
          }
        }
        return "";
      },
      findMenuGroupIndexByUrl: function (url) {
        var normalized = this.resolveViewSrc(url);
        var i;
        var j;
        var group;
        var item;
        for (i = 0; i < this.menus.length; i++) {
          group = this.menus[i];
          for (j = 0; j < group.subMenu.length; j++) {
            item = group.subMenu[j];
            if (item.url === normalized) {
              return "group-" + group.menuId;
            }
          }
        }
@@ -1188,6 +1265,62 @@
          menu.close(openedMenus[i]);
        }
      },
      syncMenuStateByUrl: function (url) {
        var targetUrl = this.resolveViewSrc(url || HOME_TAB_CONFIG.url);
        var activeMenuKey = "";
        var groupIndex = "";
        var that = this;
        var syncVersion;
        var applyMenuState;
        if (!this.isHomeTabUrl(targetUrl)) {
          activeMenuKey = this.findMenuKeyByUrl(targetUrl);
          if (activeMenuKey) {
            groupIndex = this.findMenuGroupIndexByUrl(targetUrl);
          }
        }
        this.activeMenuKey = activeMenuKey;
        this.defaultOpeneds = groupIndex ? [groupIndex] : [];
        this.menuSyncVersion += 1;
        syncVersion = this.menuSyncVersion;
        applyMenuState = function () {
          var menu = that.$refs.sideMenu;
          var openedMenus;
          if (syncVersion !== that.menuSyncVersion) {
            return;
          }
          if (!menu) {
            return;
          }
          openedMenus = menu.openedMenus ? menu.openedMenus.slice() : [];
          if (!groupIndex) {
            if (openedMenus.length) {
              that.collapseAllMenus();
            }
            return;
          }
          if (openedMenus.indexOf(groupIndex) > -1) {
            return;
          }
          if (openedMenus.length) {
            that.collapseAllMenus();
          }
          menu.open(groupIndex);
        };
        this.$nextTick(function () {
          applyMenuState();
          if (that.menuSyncTimer) {
            clearTimeout(that.menuSyncTimer);
            that.menuSyncTimer = null;
          }
          that.menuSyncTimer = setTimeout(function () {
            applyMenuState();
            that.menuSyncTimer = null;
          }, 160);
        });
      },
      loadMenu: function () {
        var that = this;
        this.menuLoading = true;
@@ -1199,11 +1332,7 @@
            that.menuLoading = false;
            if (res.code === 200) {
              that.menus = that.normalizeMenuData(res.data || []);
              that.defaultOpeneds = [];
              that.activeMenuKey = that.findMenuKeyByUrl(that.activeTabUrl);
              that.$nextTick(function () {
                that.collapseAllMenus();
              });
              that.syncMenuStateByUrl(that.activeTabUrl);
            } else if (res.code === 403) {
              top.location.href = baseUrl + "/login";
            } else {
@@ -1448,7 +1577,7 @@
        };
        window.admin.changeTheme = window.admin.changeTheme || function () {};
        window.admin.activeNav = function (url) {
          that.activeMenuKey = that.findMenuKeyByUrl(that.resolveViewSrc(url));
          that.syncMenuStateByUrl(that.resolveViewSrc(url));
        };
        window.index = window.index || {};
src/main/webapp/views/login.html
@@ -88,11 +88,6 @@
<div id="login-wrapper" class="animate__animated animate__bounceInDown">
    <header>
        <h2 id="login-title" style="cursor: pointer; user-select: none;">WCS系统V3.0</h2>
        <div id="system-btns" style="display: none; margin-bottom: 20px;">
            <button class="layui-btn layui-btn-normal layui-btn-sm" id="btn-project-name">获取项目名称</button>
            <button class="layui-btn layui-btn-normal layui-btn-sm" id="btn-server-info">获取系统配置</button>
            <button class="layui-btn layui-btn-normal layui-btn-sm" id="btn-activate">一键激活</button>
        </div>
    </header>
    <div class="layui-form layadmin-user-login-body">
        <div class="layui-form-item">
@@ -103,18 +98,27 @@
            <label class="layui-icon layui-icon-password layadmin-user-login-icon"></label>
            <input id="password" class="layui-input" type="password" name="password" lay-verify="password" placeholder="密码">
        </div>
<!--        <div id="code-box" class="layui-form-item" style="">-->
<!--            <label id="code-label" class="layui-icon layui-icon-vercode layadmin-user-login-icon"></label>-->
<!--            <input id="code" class="layui-input" type="text" name="password" lay-verify="code" placeholder="验证码">-->
<!--            <img id="codeImg" title="看不清?点击换一张。">-->
<!--        </div>-->
        <!--<div class="layui-form-item">-->
            <!--<input id="rememberPwd" style="vertical-align: middle" type="checkbox" lay-filter="remPwd" lay-skin="switch" lay-text="开启|关闭" title="记住密码" checked="checked">-->
            <!--<span style="vertical-align: middle;font-size: 15px">记住密码</span>-->
        <!--</div>-->
    </div>
    <div class="layui-form-item login-submit">
        <button id="login-button" class="layui-btn layui-btn-fluid layui-btn-normal" lay-submit="" lay-filter="login">登 &nbsp  &nbsp 录</button>
    </div>
</div>
<div id="system-tools-panel" style="display: none; padding: 20px;">
    <div style="margin-bottom: 18px;">
        <div style="margin-bottom: 10px; color: #666; font-weight: 600;">推荐操作</div>
        <div style="display: flex; flex-wrap: wrap; gap: 12px;">
            <button class="layui-btn layui-btn-normal layui-btn-sm" id="btn-request-code">获取请求码</button>
            <button class="layui-btn layui-btn-normal layui-btn-sm" id="btn-activate">一键激活</button>
        </div>
        <div style="margin-top: 8px; color: #999; font-size: 12px;">优先使用“获取请求码”和“一键激活”完成许可证申请与激活。</div>
    </div>
    <div>
        <div style="margin-bottom: 10px; color: #666; font-weight: 600;">其他工具</div>
        <div style="display: flex; flex-wrap: wrap; gap: 12px;">
            <button class="layui-btn layui-btn-primary layui-btn-sm" id="btn-project-name">获取项目名称</button>
            <button class="layui-btn layui-btn-primary layui-btn-sm" id="btn-server-info">获取系统配置</button>
            <button class="layui-btn layui-btn-primary layui-btn-sm" id="btn-upload-license">录入许可证</button>
        </div>
    </div>
</div>
@@ -122,40 +126,6 @@
<script type="text/javascript" src="../static/js/jquery/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="../static/js/tools/md5.js"></script>
<script type="text/javascript">
    // // 验证码开关
    // var codeSwitch = 'Y';
    // $.ajax({
    //     url: baseUrl+"/code/switch.action",
    //     async: false,
    //     success: function (res) {
    //         if (res.data === 'N'){
    //             codeSwitch = res.data;
    //             $('#code-box').css("display", "none");
    //         }
    //     }
    // });
    // // 初始化验证码
    // initCode();
    // $('#codeImg').click(function () {
    //     initCode();
    // });
    // function initCode() {
    //     var random = Math.random();
    //     $('#codeImg').attr("src", baseUrl+"/code.action?sd="+random);
    //     setTimeout(function () {
    //         $.ajax({
    //             url: baseUrl+"/code.do",
    //             data: {sd: random},
    //             method: 'POST',
    //             async: false,
    //             success: function (code) {
    //                 sessionStorage.setItem("code", code);
    //             }
    //         });
    //     }, 100);
    // }
    layui.use(['form','layer'],function () {
        var form = layui.form,
@@ -171,8 +141,8 @@
                clearTimeout(titleClickTimer);
            }
            if (titleClickCount >= 3) {
                $('#system-btns').show();
                titleClickCount = 0;
                openSystemToolsDialog();
            } else {
                titleClickTimer = setTimeout(function() {
                    titleClickCount = 0;
@@ -180,56 +150,18 @@
            }
        });
        // 获取系统配置
        $('#btn-server-info').click(function() {
            $.ajax({
                url: baseUrl + "/license/getServerInfos",
                method: 'GET',
                success: function (res) {
                    var pretty = '';
                    try {
                        pretty = JSON.stringify(res, null, 2);
                    } catch (e) {
                        pretty = res;
                    }
                    var html = ''
                        + '<div style="padding:15px 20px 5px 20px;">'
                        +   '<div style="font-weight:600;margin-bottom:8px;">系统配置信息</div>'
                        +   '<pre id="server-info-pre" style="background:#f7f7f7;border:1px solid #e6e6e6;border-radius:6px;padding:12px;white-space:pre-wrap;word-wrap:break-word;max-height:360px;overflow:auto;">'
                        +       pretty
                        +   '</pre>'
                        +   '<div class="layui-btn-container" style="text-align:right;margin-top:6px;">'
                        +       '<button class="layui-btn layui-btn-primary" id="copy-server-info">复制</button>'
                        +   '</div>'
                        + '</div>';
                    layer.open({
                        type: 1,
                        title: '获取系统配置',
                        area: ['640px','480px'],
                        shadeClose: true,
                        content: html,
                        success: function (layero, index) {
                            layero.find('#copy-server-info').on('click', function () {
                                var text = layero.find('#server-info-pre').text();
                                if (navigator.clipboard && navigator.clipboard.writeText) {
                                    navigator.clipboard.writeText(text).then(function () {
                                        layer.msg('已复制到剪贴板');
                                    }).catch(function () {
                                        fallbackCopy(text);
                                    });
                                } else {
                                    fallbackCopy(text);
                                }
                            });
                        }
                    });
                },
                error: function () {
                    layer.msg('获取系统配置信息失败');
        function openSystemToolsDialog() {
            layer.open({
                type: 1,
                title: '系统工具',
                area: ['560px', '300px'],
                shadeClose: true,
                content: $('#system-tools-panel'),
                end: function () {
                    $('#system-tools-panel').hide();
                }
            });
            return false;
        });
        }
        function fallbackCopy(text) {
            try {
@@ -246,6 +178,123 @@
                layer.msg('复制失败');
            }
        }
        function openTextDialog(title, label, text, tip) {
            var html = ''
                + '<div style="padding:15px 20px 5px 20px;">'
                +   '<div style="font-weight:600;margin-bottom:8px;">' + label + '</div>'
                +   (tip ? '<div style="color:#999;margin-bottom:8px;">' + tip + '</div>' : '')
                +   '<textarea id="dialog-text" readonly style="width:100%;min-height:220px;background:#f7f7f7;border:1px solid #e6e6e6;border-radius:6px;padding:12px;line-height:1.6;resize:none;">'
                +       text
                +   '</textarea>'
                +   '<div class="layui-btn-container" style="text-align:right;margin-top:10px;">'
                +       '<button class="layui-btn layui-btn-primary" id="copy-dialog-text">复制</button>'
                +   '</div>'
                + '</div>';
            layer.open({
                type: 1,
                title: title,
                area: ['720px','460px'],
                shadeClose: true,
                content: html,
                success: function (layero) {
                    layero.find('#copy-dialog-text').on('click', function () {
                        var value = layero.find('#dialog-text').val();
                        if (navigator.clipboard && navigator.clipboard.writeText) {
                            navigator.clipboard.writeText(value).then(function () {
                                layer.msg('已复制到剪贴板');
                            }).catch(function () {
                                fallbackCopy(value);
                            });
                        } else {
                            fallbackCopy(value);
                        }
                    });
                }
            });
        }
        // 获取请求码
        $('#btn-request-code').click(function() {
            $.ajax({
                url: baseUrl + "/license/getRequestCode",
                method: 'GET',
                success: function (res) {
                    if (res.code === 200){
                        openTextDialog('获取请求码', '请求码', res.msg || '', '请求码中已包含项目名称,直接发给许可证服务端即可。');
                    } else {
                        layer.msg(res.msg || '获取请求码失败');
                    }
                },
                error: function () {
                    layer.msg('获取请求码失败');
                }
            });
            return false;
        });
        // 获取系统配置
        $('#btn-server-info').click(function() {
            $.ajax({
                url: baseUrl + "/license/getServerInfos",
                method: 'GET',
                success: function (res) {
                    var pretty = '';
                    try {
                        pretty = JSON.stringify(res, null, 2);
                    } catch (e) {
                        pretty = res;
                    }
                    openTextDialog('获取系统配置', '系统配置信息', pretty, '老项目仍可继续使用这份硬件信息 JSON 申请许可证。');
                },
                error: function () {
                    layer.msg('获取系统配置信息失败');
                }
            });
            return false;
        });
        // 录入许可证
        $('#btn-upload-license').click(function () {
            layer.open({
                type: 1,
                title: '录入许可证',
                area: ['760px', '420px'],
                shadeClose: true,
                content: ''
                    + '<div style="padding:15px 20px 5px 20px;">'
                    +   '<div style="font-weight:600;margin-bottom:8px;">许可证 Base64</div>'
                    +   '<div style="color:#999;margin-bottom:8px;">将许可证服务端返回的 license 字段完整粘贴到这里。</div>'
                    +   '<textarea id="license-base64-input" style="width:100%;min-height:240px;background:#fff;border:1px solid #e6e6e6;border-radius:6px;padding:12px;line-height:1.6;resize:none;"></textarea>'
                    + '</div>',
                btn: ['提交', '取消'],
                yes: function (index, layero) {
                    var license = $.trim(layero.find('#license-base64-input').val());
                    if (!license) {
                        layer.msg('许可证内容不能为空');
                        return;
                    }
                    $.ajax({
                        url: baseUrl + '/license/updateLicense',
                        method: 'POST',
                        contentType: 'application/json;charset=UTF-8',
                        data: JSON.stringify({license: license}),
                        success: function (res) {
                            if (res.code === 200) {
                                layer.close(index);
                                layer.msg('许可证更新成功');
                            } else {
                                layer.msg(res.msg || '许可证更新失败');
                            }
                        },
                        error: function () {
                            layer.msg('许可证录入失败');
                        }
                    });
                }
            });
            return false;
        });
        // 一键激活
        $('#btn-activate').click(function() {
@@ -299,15 +348,6 @@
                layer.msg("请输入密码", {offset: '150px'});
                return;
            }
            // var code = $("#code").val();
            // if (code === "" && codeSwitch === 'Y') {
            //     layer.msg("请输入验证码", {offset: '150px'});
            //     return;
            // }
            // if (sessionStorage.getItem("code").toUpperCase() !== code.toUpperCase()&&codeSwitch==='Y'){
            //     layer.msg("验证码错误", {offset: '150px'});
            //     return;
            // }
            var user = {
                mobile: mobile,
@@ -339,4 +379,4 @@
    });
</script>
<script type></script>
</html>
</html>