package org.example.Utils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.servlet.http.HttpServletRequest;
import org.example.Api.LoggerHandler;
import org.example.Entity.BaseResponse.BaseResponse;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.io.*;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.text.MessageFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.ResponseExtractor;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.function.Consumer;
import java.util.stream.IntStream;

import org.mozilla.universalchardet.UniversalDetector;
import org.springframework.web.multipart.MultipartFile;

import static org.example.Utils.NativeExtensionUtils.*;

/**
 * 文件操作工具类，处理文件上传、移动、删除、下载等功能
 */
@Component
public class FileUtil {
    private static final Logger log = LoggerFactory.getLogger(FileUtil.class);



    private static final List<String> EXCLUDE_TYPES = Arrays.asList("aspx", "ashx", "php", "asp", "bat");
    private static final List<String> ALLOW_FILE_TYPES = Arrays.asList(
            ".png", ".jpg", ".jpeg", ".gif", ".bmp",
            ".flv", ".swf", ".mkv", ".avi", ".rm", ".rmvb", ".mpeg", ".mpg",
            ".ogg", ".ogv", ".mov", ".wmv", ".mp4", ".webm", ".mp3", ".wav", ".mid",
            ".rar", ".zip", ".tar", ".gz", ".7z", ".bz2", ".cab", ".iso",
            ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", ".pdf", ".txt", ".md", ".xml"
    );

    /**
     * 文件路径信息实体类
     */
    public static class PathInfo {
        public String WebPath;
        public String WebRelPath;
        public String RelativePath;

        //@JsonIgnore
        public String SavePath;

        public String OldFileName;
        public String FileName;
        @JsonIgnore
        public String ModuleId;
        @JsonIgnore
        public String IdValue;
        @JsonIgnore
        public String SpecNo;
        @JsonIgnore
        public long TotSize;
        @JsonIgnore
        public String StepCode;
        public String fileId;
        public String UserName;

        //        private static ILog _log;
        public Date getCreateTime() {
            return new Date(); // 每次调用都返回最新时间
        }

        @JsonIgnore
        public Map<String, Object> getDatas() {
            Map<String, Object> datas = new HashMap<>();
            datas.put("webpath", WebPath);
            datas.put("savepath", SavePath);
            datas.put("oldfilename", OldFileName);
            datas.put("filename", FileName);
            datas.put("moduleid", ModuleId);
            datas.put("idvalue", IdValue);
            datas.put("fileid", fileId);
            datas.put("specno", SpecNo);
            datas.put("totsize", TotSize);
            datas.put("stepcode", StepCode);
            return datas;
        }
    }

    /**
     * 将文件转换为字节数组
     */
    public byte[] toBinary(String path) throws IOException {
        try (FileInputStream fs = new FileInputStream(path)) {
            return FileCopyUtils.copyToByteArray(fs);
        }
    }

    /**
     * 将文件另存
     *
     * @param sourceFilePath 原始文件全名，相对路径
     * @param newDir         新目录
     * @param newName        新文件名
     * @param absFileVPath   绝对物理路径如D://fileRoot
     * @param cover          是否覆盖
     * @return 操作结果响应
     */
    public static BaseResponse moveTo(String sourceFilePath, String newDir,
                                      String newName, String absFileVPath, boolean cover) {
        BaseResponse response = new BaseResponse();
        response.setData("");

        String absNewDir = newDir;
        StringBuilder relNewDir = new StringBuilder(newDir);

        WebConfigUtil_web webConfig = new WebConfigUtil_web();
        String fileVPath = webConfig.getFileVPath();

        // 处理源文件路径
        if (sourceFilePath != null && sourceFilePath.trim().startsWith("/")
                && sourceFilePath.trim().substring(1).startsWith(fileVPath)) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//            去掉 Web 应用的实际部署位置,更换为数据库存储位置
//            sourceFilePath = request.getServletContext().getRealPath(sourceFilePath);

// 原始 sourceFilePath：/fileRoot/2000301/KH00002/√销售部门设置 .xlsx
// 处理步骤1：去除开头的 '/' 和 'fileRoot' 前缀，保留后续子路径
            String subPath;
            if (sourceFilePath.startsWith("/fileRoot/")) {
                // 截取 "/fileRoot/" 之后的部分（如 "2000301/KH00002/√销售部门设置 .xlsx"）
                subPath = sourceFilePath.substring("/fileRoot/".length());
            } else {
                // 极端情况：若路径格式异常，默认保留原路径（可根据实际需求调整）
                subPath = sourceFilePath.startsWith("/") ? sourceFilePath.substring(1) : sourceFilePath;
            }

// 处理步骤2：确保 absFileVPath 以 "fileRoot" 结尾
            Path absPath = Paths.get(absFileVPath);
// 判断 absFileVPath 的最后一级是否为 "fileRoot"
            if (!"fileRoot".equals(absPath.getFileName().toString())) {
                // 若不是，则拼接 "fileRoot" 使其以 fileRoot 结尾
                absPath = absPath.resolve("fileRoot");
            }

// 最终拼接：absPath（确保以fileRoot结尾） + subPath（已去除fileRoot）
            sourceFilePath = absPath.resolve(subPath).toString();
        } else {
            sourceFilePath = Paths.get(webConfig.getFilePath(), sourceFilePath).toString();
        }

        // 处理目标目录路径
        absNewDir = toAbsPath(newDir, absFileVPath, relNewDir)[0];


        // 验证文件是否存在及权限
        File sourceFile = new File(sourceFilePath);
//判断源文件是否在request中,2025.11.12去除
        //        if (!sourceFile.exists() || !checkFileAuthory(sourceFilePath, absFileVPath)) {
//            response.setMsg("文件上传路径不合法!");
//            return response;
//        }
        if (!checkFileAuthory(sourceFilePath, absFileVPath)) {
            response.setMsg("文件上传路径不合法!");
            return response;
        }

        // 准备文件信息
        Hashtable<String, Object> fileInfos = new Hashtable<>();
        fileInfos.put("Length", sourceFile.length());
        fileInfos.put("Name", sourceFile.getName());
        newName = (newName == null || newName.isEmpty()) ? sourceFile.getName() : newName;
        fileInfos.put("newName", newName);

        try {
            Date start = new Date();
            File targetDir = new File(absNewDir);

            // 创建目标目录
            if (!targetDir.exists()) {
                boolean dirCreated = targetDir.mkdirs();
                LoggerHandler.debug(new Object(), MessageFormat.format("创建目录：{0}", absNewDir));
            }

            String newFilePath = Paths.get(absNewDir, newName).toString();
            LoggerHandler.debug(new Object(), MessageFormat.format("移动至目标：{0}", newFilePath));

            File targetFile = new File(newFilePath);
            // 处理覆盖逻辑
            if (targetFile.exists() && cover) {
                boolean deleted = targetFile.delete();
            }

            if (!targetFile.exists()) {
                File oldDir = sourceFile.getParentFile();
                boolean moveSuccess = false;

                try {
                    // 尝试移动文件
                    moveSuccess = sourceFile.renameTo(targetFile);
                } catch (Exception e) {
                    LoggerHandler.error(new Object(), MessageFormat.format("文件移动失败！{0}", e.getMessage()), e);
                }

                if (targetFile.exists()) {
                    response.setSuccess(true);
                } else {
                    response.setSuccess(false);
                    response.setMsg("文件被占用，移动失败！尝试拷贝文件！");
                    LoggerHandler.debug(new Object(), "文件被占用，移动失败！尝试拷贝文件！");

                    try {
                        // 尝试拷贝文件
                        Files.copy(sourceFile.toPath(), targetFile.toPath());
                        if (targetFile.exists()) {
                            response.setSuccess(true);
                            response.setMsg("文件被占用，拷贝成功！");
                            LoggerHandler.debug(new Object(), "文件被占用，拷贝成功！5分钟后自动尝试删除临时文件！");

                            // 5分钟后删除原文件
                            new Timer().schedule(new TimerTask() {
                                @Override
                                public void run() {
                                    try {
                                        if (sourceFile.exists()) {
                                            boolean deleted = sourceFile.delete();
                                            LoggerHandler.debug(new Object(), "自动尝试删除临时文件成功！");
                                        }
                                    } catch (Exception e) {
                                        LoggerHandler.error(new Object(), MessageFormat.format("文件删除失败！请手动删除{0}", e.getMessage()), e);
                                    }
                                }
                            }, 5 * 60 * 1000); // 5分钟
                        }
                    } catch (IOException e) {
                        LoggerHandler.error(new Object(), MessageFormat.format("文件拷贝失败！{0}", e.getMessage()), e);
                    }
                }

                // 计算移动耗时
                LoggerHandler.debug(new Object(), MessageFormat.format("移动文件耗时：{0}ms",
                        (new Date().getTime() - start.getTime())));

                // 删除空目录
                deleteEmptyDir(oldDir.getName());
                start = new Date();
                LoggerHandler.debug(new Object(), MessageFormat.format("删除空文件夹耗时：{0}ms",
                        (new Date().getTime() - start.getTime())));

                response.setOther(fileInfos);
                response.setData(urlEncode(Paths.get(relNewDir.toString(), newName).toString(), false, true));
            } else {
                // 文件已存在且不覆盖
                File oldDir = sourceFile.getParentFile();
                boolean deleted = sourceFile.delete();
                deleteEmptyDir(oldDir.getName());

                response.setSuccess(false);
                response.setData(9);
                response.setMsg("文件已存在！是否覆盖？");
            }

        } catch (Exception e) {
            response.setMsg(e.getMessage());
            response.setSuccess(false);
            LoggerHandler.error(new Object(), MessageFormat.format("移动文件失败:{0}", response.getMsg()), e);
        }

        return response;
    }

    /**
     * 将文件保存到临时文件夹中
     *
     * @param file            上传的文件
     * @param fileRelativeDir 相对路径
     * @param filename        文件名（不包含文件类型）
     * @param appDomain       应用域名
     * @param toThum          是否生成缩略图
     * @return 操作结果响应
     */
    public static BaseResponse saveFile(MultipartFile file, String fileRelativeDir, String filename,
                                        String appDomain, boolean toThum) {
        BaseResponse response = new BaseResponse();
        response.setSuccess(false);

        // 处理文件名中的反斜杠
        if (filename != null && !filename.isEmpty()) {
            if (filename.indexOf("\\") > -1) {
                String[] parts = filename.split("\\\\");
                filename = parts[parts.length - 1];
            }
        }

        // 处理相对路径
        boolean isAbsPath = fileRelativeDir != null && fileRelativeDir.startsWith("/");
        if (isAbsPath) {
            fileRelativeDir = fileRelativeDir.substring(1);
        }

        // 图片文件特殊处理（添加_high标识）
        if (filename != null && ImageUtil.isImg(filename)) {
            if (fileRelativeDir != null && !fileRelativeDir.isEmpty()
                    && !fileRelativeDir.endsWith("/")) {
                fileRelativeDir += "_high/";
            } else {
                fileRelativeDir = "_high/";
            }
        }

        try {
            // 获取保存目录
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String saveDir;
            if (isAbsPath) {
                saveDir = Paths.get(request.getServletContext().getRealPath("/"), fileRelativeDir).toString();
            } else {
                saveDir = Paths.get(WebConfigUtil_web.getFilePath(), fileRelativeDir).toString();
            }

            // 创建目录
            File dirFile = new File(saveDir);
            if (!dirFile.exists()) {
                boolean mkdirs = dirFile.mkdirs();
                if (!mkdirs) {
                    response.setMsg("创建目录失败");
                    return response;
                }
            }

            // 验证文件类型
            String fileType = getFileExtension(filename).toLowerCase().replace(".", "");
            if (!checkFileType(fileType)) {
                response.setSuccess(false);
                response.setMsg("非法文件！");
                return response;
            }

            // 生成保存文件名
            StringBuilder fiName = new StringBuilder();
            if (filename == null || filename.isEmpty()) {
                fiName = new StringBuilder(UUID.randomUUID() + "." + fileType);
            } else {
                fiName = new StringBuilder(filename);
            }

            // 构建保存路径
            String savePath = Paths.get(saveDir, fiName.toString()).toString();

            // 检查路径长度
            String[] msg = new String[1];
            if (!checkSaveViewFolderLength(savePath, msg)) {
                response.setSuccess(false);
                response.setMsg(msg[0]);
                return response;
            }
            String fileRelativePath = Paths.get(fileRelativeDir, fiName.toString()).toString();
            // 处理文件已存在情况
            File targetFile = new File(savePath);
            if (targetFile.exists()) {
                boolean delete = targetFile.delete();
                if (!delete) {
                    response.setMsg("文件已存在且无法删除");
                    return response;
                }
            }

            // 保存文件
            file.transferTo(targetFile.toPath());

            // 处理图片旋转
            ImageUtil.translateImageByTag(savePath);

            // 处理缩略图
            boolean compThum = Boolean.parseBoolean(WebConfigUtil.get("ComprAttImg"));
            if (toThum || compThum) {
                toThumImg(savePath, toThum, compThum);
            }

            // 处理视频转换
            convertVideo(savePath, fileType, fiName);
            response.setOther(true);
            // 构建响应数据
            String webPath;
            if (appDomain != null && !appDomain.isEmpty()) {
                webPath = appDomain + "/" + (isAbsPath ? "" : WebConfigUtil.getFileVPath()) + "/" + fileRelativePath;
            } else {
                webPath = WebUtil.getRequestDomain() + "/" + (isAbsPath ? "" : WebConfigUtil.getFileVPath()) + "/" + fileRelativePath;
            }

            String webRelPath = "/" + (isAbsPath ? "" : WebConfigUtil.getFileVPath()) + "/" + fileRelativePath;
            response.setSuccess(true);
            FileUtil.PathInfo pathInfo = new FileUtil.PathInfo();
            pathInfo.WebPath = (webPath);
            pathInfo.WebRelPath = (webRelPath).replace("//", "/");
            pathInfo.RelativePath = (fileRelativePath).replace("//", "/");
            pathInfo.OldFileName = (filename);
            pathInfo.FileName = (fiName.toString());
            response.setData(pathInfo);

        } catch (IOException e) {
            response.setMsg("文件保存失败：" + e.getMessage());
        }

        return response;
    }

    /**
     * 等价于C#的DownLoadFile方法：从指定URL下载文件到本地路径
     *
     * @param downUrl  下载URL
     * @param savePath 本地保存路径（含文件名）
     * @return 下载成功返回true，失败（含异常）返回false
     */
    public static boolean DownLoadFile(String downUrl, String savePath) {
        if (downUrl == null || downUrl.trim().isEmpty() || savePath == null || savePath.trim().isEmpty()) {
            return false;
        }

        HttpClient client = HttpClient.newBuilder()
                .followRedirects(HttpClient.Redirect.NEVER)
                .build();

        try {
            URI currentUri = RemoteDownloadGuard.requireAllowedHttpUri(downUrl);
            long maxBytes = RemoteDownloadGuard.maxDownloadBytes();
            HttpResponse<InputStream> response = null;

            for (int redirects = 0; redirects <= RemoteDownloadGuard.MAX_REDIRECTS; redirects++) {
                HttpRequest request = HttpRequest.newBuilder()
                        .uri(currentUri)
                        .GET()
                        .build();

                response = client.send(request, HttpResponse.BodyHandlers.ofInputStream());
                int statusCode = response.statusCode();
                if (statusCode >= 300 && statusCode < 400) {
                    try (InputStream ignored = response.body()) {
                        // Close redirect response body before following the next hop.
                    }
                    Optional<String> location = response.headers().firstValue("Location");
                    if (location.isEmpty() || redirects == RemoteDownloadGuard.MAX_REDIRECTS) {
                        return false;
                    }
                    currentUri = RemoteDownloadGuard.requireAllowedRedirect(currentUri, location.get());
                    continue;
                }

                break;
            }

            if (response == null || response.statusCode() < 200 || response.statusCode() >= 300) {
                if (response != null) {
                    try (InputStream ignored = response.body()) {
                        // Close failed response body.
                    }
                }
                return false;
            }

            OptionalLong contentLength = response.headers().firstValueAsLong("Content-Length");
            if (contentLength.isPresent() && contentLength.getAsLong() > maxBytes) {
                try (InputStream ignored = response.body()) {
                    // Close oversized response body.
                }
                return false;
            }

            Path saveFilePath = Paths.get(savePath);
            Path parentDir = saveFilePath.getParent();
            if (parentDir != null && !Files.exists(parentDir)) {
                Files.createDirectories(parentDir);
            }

            // 核心修正：使用 StandardOpenOption 替代 StandardCopyOption
            try (InputStream in = response.body();
                 OutputStream out = Files.newOutputStream(
                         saveFilePath,
                         StandardOpenOption.CREATE,    // 不存在则创建文件
                         StandardOpenOption.TRUNCATE_EXISTING  // 存在则清空覆盖（等价C#的覆盖行为）
                 )) {
                copyWithMaxBytes(in, out, maxBytes);
            }

            return true;
        } catch (IOException e) {
            return false;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

    private static void copyWithMaxBytes(InputStream in, OutputStream out, long maxBytes) throws IOException {
        byte[] buffer = new byte[4096];
        long totalRead = 0;
        int bytesRead;
        while ((bytesRead = in.read(buffer)) != -1) {
            totalRead += bytesRead;
            if (totalRead > maxBytes) {
                throw new IOException("Remote download exceeds configured size limit");
            }
            out.write(buffer, 0, bytesRead);
        }
    }

    /**
     * 保存文件（从字节数组）
     */
    public static BaseResponse saveFile_noCN(byte[] buffer, long position, long tot, String fileRelativeDir,
                                             String filename, String appDomain, boolean toThum, int confirm, String absVPath, String downLoadUrl) {
        BaseResponse response = new BaseResponse();
        response.setSuccess(false);
        try {
            if (!isNullOrEmpty(downLoadUrl) && isNullOrEmpty(filename)) {
                filename = downLoadUrl;
            }
            if (!isNullOrEmpty(filename)) {
                // 检查是否包含反斜杠（本地路径分隔符）
                if (filename.indexOf("\\") > -1) {
                    // Java中反斜杠需要双重转义（\ → \\）
                    String[] pathParts = filename.split("\\\\");
                    // 取最后一个元素（对应C#的 Last()），先判断数组长度避免越界
                    if (pathParts.length > 0) {
                        filename = pathParts[pathParts.length - 1];
                    }
                }

                // 检查是否是URL（以http开头）
                if (filename.startsWith("http")) {
                    // 按/分割URL，取最后一段
                    String[] urlParts = filename.split("/");
                    String lastUrlSegment = urlParts.length > 0 ? urlParts[urlParts.length - 1] : filename;

                    // 按?分割，去除URL参数（取第一个元素，对应C#的 First()）
                    String[] paramParts = lastUrlSegment.split("\\?");
                    if (paramParts.length > 0) {
                        filename = paramParts[0];
                    }
                }
            }

            if (ImageUtil.isImg(filename) && toThum) {
                //去除字符串末尾所有的/字符
                fileRelativeDir = fileRelativeDir.replaceAll("/+$", "") + "_high/";
            }

            // 处理文件名
//            if (filename != null && filename.contains("\\")) {
//                filename = filename.substring(filename.lastIndexOf("\\") + 1);
//            }
//
//            // 处理图片目录
//            if (filename != null && ImageUtil.isImg(filename)) {
//                fileRelativeDir = fileRelativeDir.trim() + "_high/";
//            }

            StringBuilder relPath = new StringBuilder();
            // 获取保存目录
            String saveDir = toAbsPath(fileRelativeDir, absVPath, relPath)[0];
            log.debug(String.valueOf("relPath : " + relPath));
            String utf8SaveDir = new String(saveDir.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);

            // 创建目录
//            File dir = new File(saveDir);
            File dir = new File(utf8SaveDir);
//            if (!dir.exists()) {
//                Files.createDirectories(dir.toPath());
//            }
            try {
                // 强制创建目录（包括父目录），并设置权限
                if (!dir.exists()) {
                    dir.mkdirs(); // 替代 Files.createDirectories，兼容性更好
                    dir.setWritable(true, false); // 设为可写
                    dir.setReadable(true, false); // 设为可读
                    dir.setExecutable(true, false);
                }
                // 检查权限
                if (!dir.canWrite()) {
                    throw new RuntimeException("目录无写入权限：" + saveDir);
                }
            } catch (Exception e) {
                throw new RuntimeException("创建目录失败：" + saveDir, e);
            }

            // 验证文件类型
            String fileType = getFileExtension(filename).toLowerCase();
            if (!checkFileType(fileType)) {
                response.setSuccess(false);
                response.setMsg("非法文件！");
                return response;
            }

            // 处理文件名
//            StringBuilder fiName = new StringBuilder((filename == null || filename.isEmpty())
//                    ? UUID.randomUUID() + "." + fileType
//                    : filename);
            StringBuilder fiName = new StringBuilder();
            fiName.append(filename);

            String utf8FileName = new String(fiName.toString().getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
            // 保存路径
//            Path savePath = Paths.get(saveDir, fiName.toString());
            Path savePath = Paths.get(utf8SaveDir, utf8FileName);
            // 处理文件已存在情况
            if (position == 0 && Files.exists(savePath)) {
                if (confirm == 1) {
                    Files.delete(savePath);
                    confirm = 0;
                } else {
                    response.setSuccess(false);
                    response.setMsg("文件已存在，是否覆盖？");
                    response.setData(9);
                    return response;
                }
            }

            // 检查路径长度
            String[] msg = new String[1];
            if (position == 0 && !checkSaveViewFolderLength(savePath.toString(), msg)) {
                response.setSuccess(false);
                response.setMsg(msg[0]);
                return response;
            }

            // 检查权限
            if (position == 0 && !checkFileAuthory(savePath.toString(), absVPath)) {
                response.setMsg("非法路径！");
                return response;
            }

            // 写入文件
            boolean isFileProcessed = false;
            if (buffer != null && buffer.length > 0) { // 修正：增加buffer非空判断，避免空指针
                // 修正点4：文件写入逻辑与C#等价
                boolean append = Files.exists(savePath) && position != 0;
                try (FileOutputStream fos = new FileOutputStream(savePath.toString(), append)) {
                    fos.write(buffer);
                }
                isFileProcessed = true;
                File targetFile = savePath.toFile();
                // 1. Linux下强制设置文件权限（所有用户可读，所有者可写，保证下载权限）
                // - 644：所有者可读写，其他用户只读（生产推荐）
                // - 777：所有用户可读可写可执行（测试推荐，最宽松）
                targetFile.setReadable(true, false);  // 所有用户可读（下载核心权限）
                targetFile.setWritable(true, false);   // 仅所有者可写（保证安全性）
                targetFile.setExecutable(false);      // 非可执行文件，关闭执行权限

                // 文件写入完成判断
                if (position + buffer.length >= tot) {
                    // 修正点5：ToThumImg调用逻辑与C#一致
                    boolean comprAttImg = toBoolean(WebConfigUtil.get("ComprAttImg"));
                    toThumImg(savePath.toString(), toThum, comprAttImg);
                    convertVideo(savePath.toString(), fileType, fiName);
                    response.setOther(true);
                }
            }
            // C#原逻辑：buffer为空但downLoadUrl有值时下载文件
            else if (!isNullOrEmpty(downLoadUrl) && FileUtil.DownLoadFile(downLoadUrl, savePath.toString())) {
                boolean comprAttImg = toBoolean(WebConfigUtil.get("ComprAttImg"));
                toThumImg(savePath.toString(), toThum, comprAttImg);
                convertVideo(savePath.toString(), fileType, fiName);
                response.setOther(true);
                isFileProcessed = true;
            }
            // 构建返回路径信息
            String fileRelativePath = Paths.get(relPath.toString(), fiName.toString()).toString().replace("\\", "/");
            String webPath = buildWebPath(fileRelativePath, appDomain);
            String webRelPath = fileRelativePath;

            PathInfo pathInfo = new PathInfo();
            pathInfo.WebPath = webPath;
            pathInfo.WebRelPath = webRelPath.replace("//", "/");
            pathInfo.RelativePath = fileRelativePath.replace("//", "/");
            pathInfo.SavePath = savePath.toString();
            pathInfo.OldFileName = filename;
            pathInfo.FileName = fiName.toString();

            response.setSuccess(true);
            response.setData(pathInfo);
        } catch (Exception e) {
            response.setMsg(e.getMessage());
        }
        return response;
    }

    public static BaseResponse saveFile(byte[] buffer, long position, long tot, String fileRelativeDir,
                                        String filename, String appDomain, boolean toThum, int confirm, String absVPath, String downLoadUrl) {
        BaseResponse response = new BaseResponse();
        response.setSuccess(false);
        try {
            if (!isNullOrEmpty(downLoadUrl) && isNullOrEmpty(filename)) {
                filename = downLoadUrl;
            }
            if (!isNullOrEmpty(filename)) {
                if (filename.indexOf("\\") > -1) {
                    String[] pathParts = filename.split("\\\\");
                    if (pathParts.length > 0) {
                        filename = pathParts[pathParts.length - 1];
                    }
                }

                if (filename.startsWith("http")) {
                    String[] urlParts = filename.split("/");
                    String lastUrlSegment = urlParts.length > 0 ? urlParts[urlParts.length - 1] : filename;
                    String[] paramParts = lastUrlSegment.split("\\?");
                    if (paramParts.length > 0) {
                        filename = paramParts[0];
                    }
                }
            }

            filename = sanitizeFileName(filename);

            if (ImageUtil.isImg(filename) && toThum) {
                fileRelativeDir = fileRelativeDir.replaceAll("/+$", "") + "_high/";
            }

            StringBuilder relPath = new StringBuilder();
            String saveDir = toAbsPath(fileRelativeDir, absVPath, relPath)[0];

            String utf8SaveDir = saveDir;
//            String utf8FileName = ensureUtf8String(filename);
            String utf8FileName = (filename);

            File dir = new File(utf8SaveDir);
            try {
                if (!dir.exists()) {
                    dir.mkdirs();
                    dir.setWritable(true, false);
                    dir.setReadable(true, false);
                    dir.setExecutable(true, false);
                }
                if (!dir.canWrite()) {
                    throw new RuntimeException("目录无写入权限：" + saveDir);
                }
            } catch (Exception e) {
                throw new RuntimeException("创建目录失败：" + saveDir, e);
            }

            String fileType = getFileExtension(filename).toLowerCase();
            if (!checkFileType(fileType)) {
                response.setSuccess(false);
                response.setMsg("非法文件！");
                return response;
            }

            StringBuilder fiName = new StringBuilder();
            fiName.append(utf8FileName);


//            Path savePath = Paths.get(utf8SaveDir, fiName.toString());
            String savePath = String.format("%s/%s", utf8SaveDir, filename);
            String savef = String.format("%s/%s", utf8SaveDir, filename);
            log.debug(String.format("savePath: %s\n,savef : %s\n", savePath, savef));
            File saveFile = new File(savef);
//            if (position == 0 && Files.exists(savePath)) {
            if (position == 0 && saveFile.exists()) {
                if (confirm == 1) {
                    saveFile.delete();
                    confirm = 0;
                } else {
                    response.setSuccess(false);
                    response.setMsg("文件已存在，是否覆盖？");
                    response.setData(9);
                    return response;
                }
            }

            String[] msg = new String[1];
            if (position == 0 && !checkSaveViewFolderLength(savePath, msg)) {
                response.setSuccess(false);
                response.setMsg(msg[0]);
                return response;
            }

            if (position == 0 && !checkFileAuthory(savePath, absVPath)) {
                response.setMsg("非法路径！");
                return response;
            }

            boolean isFileProcessed = false;
            if (buffer != null && buffer.length > 0) {
//                boolean append = Files.exists(savePath) && position != 0;
                boolean append = saveFile.exists() && position != 0;
                try (FileOutputStream fos = new FileOutputStream(savePath, append)) {
                    fos.write(buffer);
                }
                isFileProcessed = true;
                File targetFile = saveFile;
                targetFile.setReadable(true, false);
                targetFile.setWritable(true, false);
                targetFile.setExecutable(false);

                if (position + buffer.length >= tot) {
                    boolean comprAttImg = toBoolean(WebConfigUtil.get("ComprAttImg"));
                    toThumImg(savePath, toThum, comprAttImg);
                    convertVideo(savePath, fileType, fiName);
                    response.setOther(true);
                }
            } else if (!isNullOrEmpty(downLoadUrl) && FileUtil.DownLoadFile(downLoadUrl, savePath)) {
                boolean comprAttImg = toBoolean(WebConfigUtil.get("ComprAttImg"));
                toThumImg(savePath, toThum, comprAttImg);
                convertVideo(savePath, fileType, fiName);
                response.setOther(true);
                isFileProcessed = true;
            }

//            String fileRelativePath = String.format(relPath.toString(), fiName).replace("\\", "/");
            String fileRelativePath = Paths.get(relPath.toString(), fiName.toString()).toString().replace("\\", "/");
            String webPath = buildWebPath(fileRelativePath, appDomain);
            String webRelPath = fileRelativePath;

            PathInfo pathInfo = new PathInfo();
            pathInfo.WebPath = webPath;
            pathInfo.WebRelPath = webRelPath.replace("//", "/");
            pathInfo.RelativePath = fileRelativePath.replace("//", "/");
            pathInfo.SavePath = savePath;
            pathInfo.OldFileName = filename;
            pathInfo.FileName = fiName.toString();

            response.setSuccess(true);
            response.setData(pathInfo);
        } catch (Exception e) {
            LoggerHandler.error(new Object(), "保存文件失败：" + e.getMessage() + ", filename=" + filename, e);
            response.setMsg("文件保存失败：" + getErrMsg(e));
        }
        return response;
    }

    // 文件名清理
    private static String sanitizeFileName(String filename) {
        if (filename == null || filename.isEmpty()) {
            return filename;
        }

        String sanitized = filename.trim();

        if (sanitized.contains("%")) {
            sanitized = URLDecoder.decode(sanitized, StandardCharsets.UTF_8);
        }

        sanitized = sanitized.replaceAll("[\\\\/:*?\"<>|]", "_");

        while (sanitized.contains("__")) {
            sanitized = sanitized.replace("__", "_");
        }

        return sanitized;
    }

    // UTF-8 编码转换
    // ... existing code ...

    public static String ensureUtf8String(String str) {
        if (str == null || str.isEmpty()) {
            return str;
        }

        String processed = str.trim();

        try {
            // 【第一步】先处理 URL 编码（如 %E4%B8%AD → 中）
            if (processed.contains("%")) {
                boolean hasUrlEncoding = false;
                // 检查是否真的是 URL 编码（%后跟两个十六进制字符）
                for (int i = 0; i < processed.length() - 2; i++) {
                    if (processed.charAt(i) == '%' &&
                            isHexDigit(processed.charAt(i + 1)) &&
                            isHexDigit(processed.charAt(i + 2))) {
                        hasUrlEncoding = true;
                        break;
                    }
                }

                if (hasUrlEncoding) {
                    processed = URLDecoder.decode(processed, StandardCharsets.UTF_8);
                }
            }

            // 【第二步】再处理可能的 ISO-8859-1 编码问题
            byte[] bytes = processed.getBytes(StandardCharsets.ISO_8859_1);
            String testUtf8 = new String(bytes, StandardCharsets.UTF_8);

            // 如果包含问号或明显是乱码，说明是 ISO-8859-1 编码错误
            if (testUtf8.contains("?") || (!testUtf8.equals(processed) && !looksLikeValidText(testUtf8))) {
                processed = testUtf8;
            }

            return processed;

        } catch (Exception e) {
            LoggerHandler.error(new Object(), "UTF-8 编码转换失败：" + e.getMessage() + ", original=" + str);
            return str;
        }
    }

    /**
     * 辅助方法：判断字符串是否看起来像有效的文本（非乱码）
     */
    private static boolean looksLikeValidText(String str) {
        if (str == null || str.isEmpty()) {
            return false;
        }

        // 检查是否包含常见的有效字符（中文、英文、数字、常见符号）
        int validCharCount = 0;
        for (char c : str.toCharArray()) {
            // 中文、英文、数字、常见标点
            if (Character.isLetterOrDigit(c) ||
                    "._-()[] ".indexOf(c) >= 0 ||
                    (c >= 0x4e00 && c <= 0x9fff)) {  // 常用汉字范围
                validCharCount++;
            }
        }

        // 如果超过 70% 的字符是有效的，认为是合法文本
        return (double) validCharCount / str.length() > 0.7;
    }


    //  修改后（兼容所有 Java 版本）
    private static boolean isHexDigit(char c) {
        return (c >= '0' && c <= '9') ||
                (c >= 'A' && c <= 'F') ||
                (c >= 'a' && c <= 'f');
    }


    private static String getErrMsg(Exception e) {
        if (e == null) {
            return "";
        }
        String msg = e.getMessage();
        if (msg == null) {
            return e.getClass().getSimpleName();
        }
        return msg;
    }


    public static String CreateAttcFileName(String localPath, String fileName) {
        return CreateAttcFileName(localPath, fileName, 1);
    }

    public static String CreateAttcFileName(String localPath, String fileName, int index) {
        File dir = new File(localPath);
        // 保留初始传入的index，后续循环修改这个变量
        int finalIndex = index;

        if (dir.exists() && dir.isDirectory()) {
            try {
                // 关键修改1：过滤仅保留文件（排除子目录），对齐C#的Directory.GetFiles
                File[] files = dir.listFiles(File::isFile);

                if (files != null && files.length > 0) {
                    // 索引提取逻辑保持不变（和C#一致，无需修改）
                    int[] fileIndex = Arrays.stream(files)
                            .map(File::getPath)
                            .map(fname -> {
                                // 统一路径分隔符，拆分取文件名
                                String name = fname.replace("/", "\\");
                                String[] pathParts = name.split("\\\\");
                                name = pathParts.length > 0 ? pathParts[pathParts.length - 1] : "";

                                // 按_拆分，取第一段
                                String[] nameParts = name.split("_");
                                name = nameParts.length > 0 ? nameParts[0] : "";

                                // 去掉开头所有0
                                name = name.replaceAll("^0+", "");

                                // 长度>4取最后4位
                                if (name.length() > 4) {
                                    name = name.substring(name.length() - 4);
                                }

                                // 转为整数（替换为你实际的NativeExtensionUtils.ToInt32）
                                return ToInt32(name);
                            })
                            .mapToInt(Integer::intValue)
                            .toArray();

                    // 关键修改2：替换为C#的循环检查逻辑（核心对齐点）
                    // 用HashSet提升包含判断的效率（数组contains是O(n)，HashSet是O(1)）
                    Set<Integer> indexSet = new HashSet<>();
                    for (int idx : fileIndex) {
                        indexSet.add(idx);
                    }
                    // 循环检查：如果当前索引已存在，就+1，直到找到不存在的索引
                    while (indexSet.contains(finalIndex)) {
                        finalIndex += 1;
                    }
                }
            } catch (Exception e) {
                // 保持和原逻辑一致的空catch
            }
        }

        // 生成最终文件名（和C#格式一致）
        return String.format("00%d_%s", finalIndex, fileName);
    }

    /**
     * 获取文件保存路径信息
     */
    public static String getFileSavePath_noCN(String fileRelativeDir, String fileType, String appDomain,
                                              String absPath, StringBuilder fileName, StringBuilder relativePath, StringBuilder webPath) {
        Path savePath = null;
        try {
            // 处理文件名
//            if (fileName != null && fileName.toString().contains("/")) {
//                fileName = new StringBuilder(fileName.toString().substring(fileName.toString().lastIndexOf("/") + 1));
//            }

            if (fileName != null && fileName.length() > 0) {
                // 1. 将 StringBuilder 转为字符串处理（反斜杠替换为斜杠）
//                String fileNameStr = fileName.toString().replace("\\", "/");
                String fileNameStr = new String(
                        fileName.toString().replace("\\", "/").getBytes(StandardCharsets.UTF_8),
                        StandardCharsets.UTF_8
                );
                // 2. 检查是否包含斜杠，若有则截取最后一部分
                if (fileNameStr.contains("/")) {
                    // 按斜杠分割，取最后一段
                    fileNameStr = fileNameStr.split("/")[fileNameStr.split("/").length - 1];
                }

                // 3. 清空原 StringBuilder 并填入处理后的结果（避免创建新对象）
                fileName.setLength(0);
                fileName.append(fileNameStr);
            }

            StringBuilder relPath = new StringBuilder();
//            2026.3.9 修改中文路径
            String utf8FileRelativeDir = new String(fileRelativeDir.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
            String utf8AbsPath = new String(absPath.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
            String saveDir = toAbsPath(utf8FileRelativeDir, utf8AbsPath, relPath)[0];

            saveDir = new String(saveDir.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
//            // 获取绝对路径和相对路径
//            String saveDir = toAbsPath(fileRelativeDir, absPath, relPath)[0];
//            out.println("relPath(getFileSavePath) : " + relPath);

            // 创建目录
            File dir = new File(saveDir);
            if (!dir.exists()) {
                Files.createDirectories(dir.toPath());
                // 关键：Linux下给中文目录加全权限（必须有x权限才能进入）
                dir.setReadable(true, false);
                dir.setWritable(true, false);
                dir.setExecutable(true, false);
            }

            // 处理文件类型和文件名
            if (fileType == null || fileType.isEmpty()) {
                fileType = getFileExtension(String.valueOf(fileName));
            }
            String fiName = (fileName == null || fileName.isEmpty())
                    ? UUID.randomUUID() + "." + fileType
                    : String.valueOf(fileName);
//            fileName = new StringBuilder(fiName);
            fiName = new String(fiName.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
            log.debug(String.valueOf("fiName: " + fiName));
            // 构建路径
            savePath = Paths.get(saveDir, fiName);
//            relativePath.append(Paths.get(relPath.toString(), fiName).toString().replace("//", "/"));
            relativePath.append(Paths.get(relPath.toString(), fiName).toString()
                    .replace("//", "/")
                    .replace("\\", "/"));
            log.debug(String.valueOf("relativePath(getFileSavePath) : " + relativePath));
            webPath.append(buildWebPath(relativePath.toString(), appDomain));

            return savePath.toString();
        } catch (Exception e) {
            //throw new RuntimeException("获取文件保存路径失败", e);

            throw new RuntimeException("getFileSavePath生成路径失败！relativeDir=" + savePath + "fileRelativeDir, absPath " + fileRelativeDir + absPath + ", filename=" + fileName, e);
        }
    }

    public static String getFileSavePath(String fileRelativeDir, String fileType, String appDomain,
                                         String absPath, StringBuilder fileName, StringBuilder relativePath, StringBuilder webPath) {
        Path savePath = null;
        try {
            if (fileName != null && !fileName.isEmpty()) {
                String fileNameStr = fileName.toString().replace("\\", "/");

                if (fileNameStr.contains("%")) {
                    fileNameStr = URLDecoder.decode(fileNameStr, StandardCharsets.UTF_8);
                }

                if (fileNameStr.contains("/")) {
                    fileNameStr = fileNameStr.split("/")[fileNameStr.split("/").length - 1];
                }

                fileNameStr = sanitizeFileName(fileNameStr);

                fileName.setLength(0);
                fileName.append(fileNameStr);
            }

            StringBuilder relPath = new StringBuilder();
//            String utf8FileRelativeDir = ensureUtf8String(fileRelativeDir);
//            String utf8AbsPath = ensureUtf8String(absPath);
            String saveDir = toAbsPath(fileRelativeDir, absPath, relPath)[0];

//            saveDir = ensureUtf8String(saveDir);

            File dir = new File(saveDir);
            if (!dir.exists()) {
                Files.createDirectories(dir.toPath());
                dir.setReadable(true, false);
                dir.setWritable(true, false);
                dir.setExecutable(true, false);
            }

            if (fileType == null || fileType.isEmpty()) {
                fileType = getFileExtension(String.valueOf(fileName));
            }
            String fiName = (fileName == null || fileName.isEmpty())
                    ? UUID.randomUUID() + "." + fileType
                    : String.valueOf(fileName);

//            fiName = ensureUtf8String(fiName);

            log.debug(String.valueOf("fiName: " + fiName));

            savePath = Paths.get(saveDir, fiName);

            relativePath.append(Paths.get(relPath.toString(), fiName).toString()
                    .replace("//", "/")
                    .replace("\\", "/"));
            log.debug(String.valueOf("relativePath(getFileSavePath) : " + relativePath));
            webPath.append(buildWebPath(relativePath.toString(), appDomain));

            return savePath.toString();
        } catch (Exception e) {
            LoggerHandler.error(new Object(), "getFileSavePath 生成路径失败！fileRelativeDir=" + fileRelativeDir
                    + ", absPath=" + absPath + ", filename=" + fileName, e);
            throw new RuntimeException("getFileSavePath 生成路径失败！relativeDir=" + savePath
                    + ", fileRelativeDir=" + fileRelativeDir + ", absPath=" + absPath
                    + ", filename=" + fileName, e);
        }
    }

    /**
     * 检查保存路径长度是否合法
     */
    public static boolean checkSaveViewFolderLength(String filePath, String[] msg) {
        int length = checkFolderLength(filePath);
        if (length > 260) {
            msg[0] = (StringFormat.format("文件夹及文件名过长,不得超过260个字符，当前总长度{0}！", length));
            return false;
        }

        Path dir = Paths.get(filePath);
        filePath = dir.toString();
        String dirPath = filePath.substring(0, filePath.lastIndexOf(File.separator));
        length = checkFolderLength(dirPath);
        if (length > 248) {
            msg[0] = (String.format("文件夹过长,目录名必须少于248个字符，当前总长度{0}！", length));
            return false;
        }

        // 检查预览文件路径
        StringBuilder vHtmlPath = new StringBuilder();
        String htmlPath = WebConfigUtil_web.getViewDocHtmlPath(filePath, vHtmlPath);
        length = checkFolderLength(htmlPath);
        if (length > 260) {
            msg[0] = (String.format("文件预览路径过长,不得超过260个字符，当前总长度{0}！", length));
            return false;
        }

        String htmlDirPath = htmlPath.substring(0, htmlPath.lastIndexOf("/"));
//        String htmlDirPath = htmlPath.substring(0, htmlPath.lastIndexOf('/'));
        length = checkFolderLength(htmlDirPath);
        if (length > 248) {
            msg[0] = (String.format("文件预览目录过长,必须少于248个字符，当前总长度%s！", length));
            return false;
        }
        return true;
    }

    /**
     * 检查文件类型是否合法
     */
    public static boolean checkFileType(String fileType) {
        if (fileType == null || fileType.isEmpty()) {
            return false;
        }
        fileType = fileType.toLowerCase().trim();
        if (EXCLUDE_TYPES.contains(fileType)) {
            return false;
        }

        String ftype = "." + fileType;
        if (ALLOW_FILE_TYPES.contains(ftype)) {
            return true;
        }

        // 检查配置的允许类型
        String allowUpload = WebConfigUtil_web.get("allowUploadFile", "");
        if (!allowUpload.isEmpty() && Arrays.asList(allowUpload.toLowerCase().split(",")).contains(ftype)) {
            return true;
        }

        // 检查配置的禁止类型
        String notUpload = WebConfigUtil_web.get("notUploadFile", "");
        if (!notUpload.isEmpty() && Arrays.asList(notUpload.toLowerCase().split(",")).contains(ftype)) {
            return false;
        }

        return false;
    }

    /**
     * 检查文件夹路径长度
     */
    public static int checkFolderLength(String folder) {
        String cleanFolder = folder.replace("\\", "").replace("/", "").replace(":", "");
        Pattern pattern = Pattern.compile("[^\\x00-\\xFF]");
        Matcher matcher = pattern.matcher(cleanFolder);
        int chineseCount = cleanFolder.length() - matcher.replaceAll("").length();
        int asciiCount = cleanFolder.length() - chineseCount;
        return chineseCount * 2 + asciiCount + 1;
    }

    /**
     * 检查文件是否存在
     */
    public static BaseResponse checkExistsFile(String fileRelativeDir, String filename) {
        BaseResponse response = new BaseResponse();
        response.setSuccess(false);

        try {
            if (filename != null && filename.contains("\\")) {
                filename = filename.substring(filename.lastIndexOf("\\") + 1);
            }

            boolean isAbsPath = fileRelativeDir.startsWith("/");
            if (isAbsPath) {
                fileRelativeDir = fileRelativeDir.substring(1);
            }

            String saveDir = Paths.get(
                    isAbsPath ? getRequest().getServletContext().getRealPath("/") : WebConfigUtil_web.getFilePath(),
                    fileRelativeDir
            ).toString();

            Path savePath = Paths.get(saveDir, filename);
            if (Files.exists(savePath)) {
                response.setOther(1);
                response.setMsg(String.format("文件已存在: %s", filename));
            } else {
                response.setSuccess(true);
            }
        } catch (Exception e) {
            response.setMsg(e.getMessage());
        }

        return response;
    }

    /**
     * 完全对齐 C# 的 ToAbsPath 方法逻辑
     *
     * @param filePath 输入文件路径（可能是相对/绝对路径，含编码）
     * @param absPath  基础绝对路径（可为 null，null 时使用配置的 FilePath）
     * @param vPath    输出参数：虚拟路径（通过 StringBuilder 传递，对应 C# 的 out 参数）
     * @return 处理后的绝对路径（反斜杠分隔，尾部无斜杠）
     */
//    public static String[] toAbsPath(String filePath, String absPath, StringBuilder vPath) {
//        // 1. 初始化虚拟路径 vPath（对应 C# 的 vPath = "/"）
//        // 注：Java 无 out 参数，通过重置 StringBuilder 内容实现输出
//        if (vPath == null) {
//            vPath = new StringBuilder();
//        }
//        vPath.setLength(0); // 清空原有内容
//        vPath.append("/");
//
//        try {
//            // 2. URL 解码（对应 C# 的 HttpUtility.UrlDecode）
//            if (filePath != null && filePath.contains("%")) {
//                filePath = URLDecoder.decode(filePath, StandardCharsets.UTF_8.name());
//            }
//
//            // 3. 修剪路径前缀（. ~ / \）+ 统一反斜杠为斜杠（完全复刻 C# 的 TrimStart 逻辑）
//            if (filePath != null) {
//                // 先修剪前缀：. ~ / \（顺序与 C# 一致：TrimStart('.').TrimStart('~').TrimStart('/').TrimStart('\\')）
//                String trimmed = filePath.trim();
//                trimmed = trimStart(trimmed, '.');
//                trimmed = trimStart(trimmed, '~');
//                trimmed = trimStart(trimmed, '/');
//                trimmed = trimStart(trimmed, '\\');
//                // 统一反斜杠为斜杠
//                filePath = trimmed.replace("\\", "/");
//            }
//
//            // 4. 处理相对路径（无盘符时，拼接基础路径）
//            if (filePath == null || filePath.indexOf(":") < 0) { // 无盘符（非绝对路径）
//                // 4.1 基础路径优先级：absPath 不为空则用 absPath，否则用配置的 FilePath（对应 C# 的 WebConfigUtil.FilePath）
//                if (absPath == null || absPath.isEmpty()) {
//                    absPath = WebConfigUtil.getFilePath();
//                }
//
//                // 4.2 读取配置的虚拟路径前缀（对应 C# 的 WebConfigUtil.FileVPath）
//                String fileVPath = WebConfigUtil.getFileVPath();
//                // 处理空的 fileVPath（避免后续 substring 异常）
//                if (fileVPath == null) {
//                    fileVPath = "";
//                }
//
//                // 4.3 大小写不敏感判断：filePath 是否以 fileVPath 开头，且 absPath 包含 fileVPath
//                boolean startsWithVPath = false;
//                boolean absContainsVPath = false;
//                if (!fileVPath.isEmpty() && filePath != null) {
//                    // 复刻 C# 的 StringComparison.OrdinalIgnoreCase（大小写不敏感）
//                    startsWithVPath = filePath.toLowerCase().startsWith(fileVPath.toLowerCase());
//                    absContainsVPath = absPath.toLowerCase().contains(fileVPath.toLowerCase());
//                }
//
//                // 4.4 拼接路径（对应 C# 的 Path.Combine）
//                Path combinedPath;
//                if (startsWithVPath && absContainsVPath) {
//                    // 截取 filePath 中 fileVPath 之后的部分，并修剪前缀 /
//                    String subPath = trimStart(filePath.substring(fileVPath.length()), '/');
//                    combinedPath = Paths.get(absPath, subPath);
//                } else {
//                    // 直接拼接基础路径和 filePath
//                    combinedPath = Paths.get(absPath, filePath);
//                }
//
//                // 转换为字符串路径（统一处理分隔符）
//                filePath = combinedPath.toString();
//            }
//
//            // 5. 计算虚拟路径 vPath（对应 C# 的 $"/{filePath.Substring(vIndex)}"）
//            String fileVPath = WebConfigUtil.getFileVPath();
//            if (fileVPath == null) {
//                fileVPath = "";
//            }
//            int vIndex = -1;
//            if (!fileVPath.isEmpty() && filePath != null) {
//                // 大小写不敏感查找最后一个 fileVPath（对应 C# 的 LastIndexOf(..., OrdinalIgnoreCase)）
//                vIndex = filePath.toLowerCase().lastIndexOf(fileVPath.toLowerCase());
//            }
//            out.println("vIndex: " + vIndex);
//            if (vIndex > -1) {
//                // 截取从 vIndex 开始的子串，拼接为 "/xxx" 格式，统一分隔符为 /
//                String vPathSub = filePath.substring(vIndex).replace("\\", "/");
//                vPath.setLength(0); // 清空原有内容
//                vPath.append("/").append(vPathSub);
//            }
////            out.println("vPath: " + vPath); //测试vpath是否正确
//            // 6. 处理返回路径：尾部无 / + 统一为反斜杠（对应 C# 的 TrimEnd('/').Replace("/","\\")）
//            String resultPath = filePath;
//            if (resultPath != null) {
//                // 修剪尾部 /（复刻 C# 的 TrimEnd('/')）
//                resultPath = trimEnd(resultPath, '/');
//                // 统一斜杠为反斜杠
//                resultPath = resultPath.replace("/", "\\");
//            }
//
//            // 返回结果：[0] 处理后的绝对路径，[1] 虚拟路径（辅助返回，vPath 已通过输出参数传递）
//            return new String[]{resultPath, vPath.toString()};
//
//        } catch (Exception e) {
//            // 异常时返回原始 filePath + 当前 vPath（对齐 C# 异常处理逻辑）
//            String errorPath = filePath != null ? filePath : "";
//            return new String[]{errorPath, vPath.toString()};
//        }
//    }
    public static String[] toAbsPath(String filePath, String absPath, StringBuilder vPath) {
        // 1. 虚拟路径初始化
        if (vPath == null) {
            vPath = new StringBuilder();
        }
        vPath.setLength(0);
        vPath.append("/");

        try {
            // 2. URL解码（处理%23等特殊字符）
            if (filePath != null && filePath.contains("%")) {
                filePath = URLDecoder.decode(filePath, StandardCharsets.UTF_8);
            }

            // 3. 路径修剪 + 统一分隔符为 /（Linux优先，Windows自动兼容）
            String cleanFilePath = filePath;
            if (cleanFilePath != null) {
                cleanFilePath = trimStart(cleanFilePath, '.');
                cleanFilePath = trimStart(cleanFilePath, '~');
                cleanFilePath = trimStart(cleanFilePath, '/');
                cleanFilePath = trimStart(cleanFilePath, '\\');
                // 核心：统一所有分隔符为 /（Linux标准，Windows也识别）
                cleanFilePath = cleanFilePath.replace("\\", "/");
            }

            // 4. 相对路径拼接（仅非Windows盘符路径执行）
            if (cleanFilePath != null && cleanFilePath.indexOf(":") < 0) {
                // 兜底获取基础路径
                if (absPath == null || absPath.isEmpty()) {
                    absPath = WebConfigUtil.getFilePath();
                }
                // 基础路径也统一为 / 分隔符
                String cleanAbsPath = absPath.replace("\\", "/");

                String fileVPath = WebConfigUtil.getFileVPath();
                fileVPath = (fileVPath == null ? "" : fileVPath.replace("\\", "/"));

                boolean startsWithVPath = false;
                boolean absContainsVPath = false;
                if (!fileVPath.isEmpty() && cleanFilePath != null) {
                    startsWithVPath = cleanFilePath.toLowerCase().startsWith(fileVPath.toLowerCase());
                    absContainsVPath = cleanAbsPath.toLowerCase().contains(fileVPath.toLowerCase());
                }

                Path combinedPath;
                if (startsWithVPath && absContainsVPath) {
                    // 正则匹配虚拟路径前缀（兼容带数字后缀的情况，如fileRoot_1）
                    String pattern = "^" + Pattern.quote(fileVPath) + "(?:_[0-9]+)?";
                    Pattern regex = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
                    Matcher match = regex.matcher(cleanFilePath);
                    int subLength = fileVPath.length();
                    if (match.find()) {
                        subLength = match.end();
                    }
                    String subPath = trimStart(cleanFilePath.substring(subLength), '/');
                    combinedPath = Paths.get(cleanAbsPath, subPath);
                } else {
                    combinedPath = Paths.get(cleanAbsPath, cleanFilePath);
                }

                // 标准化路径（移除../、//，统一为 / 分隔符）
                cleanFilePath = combinedPath.toAbsolutePath().normalize().toString().replace("\\", "/");
            }

            // 5. 虚拟路径计算（统一为 / 分隔符）
            String fileVPath = WebConfigUtil.getFileVPath();
            fileVPath = (fileVPath == null ? "" : fileVPath.replace("\\", "/"));
            int vIndex = -1;
            if (!fileVPath.isEmpty() && cleanFilePath != null) {
                vIndex = cleanFilePath.toLowerCase().lastIndexOf(fileVPath.toLowerCase());
            }
            if (vIndex > -1) {
                String vPathSub = cleanFilePath.substring(vIndex).replace("\\", "/");
                vPath.setLength(0);
                vPath.append("/").append(trimStart(vPathSub, '/'));
            } else {
                // 匹配失败时，使用完整路径作为虚拟路径
                vPath.append(cleanFilePath == null ? "/" : cleanFilePath);
            }

            // 6. 递归处理HTTP路径（递归时也统一分隔符）
            if (cleanFilePath != null && cleanFilePath.startsWith("http")) {
                String[] recursiveResult = toAbsPath(vPath.toString(), absPath, vPath);
                // 递归结果也统一为 / 分隔符
                recursiveResult[0] = recursiveResult[0].replace("\\", "/");
                recursiveResult[1] = recursiveResult[1].replace("\\", "/");
                return recursiveResult;
            }

            // 7. 最终路径处理（根据系统适配分隔符，Linux用/，Windows用\）
            String resultPath = cleanFilePath;
            if (resultPath != null) {
                resultPath = trimEnd(resultPath, '/');
                // 核心：根据系统自动适配分隔符，不再强制转\
                if (System.getProperty("os.name").toLowerCase().contains("win")) {
                    resultPath = resultPath.replace("/", "\\"); // Windows才转\
                } else {
                    resultPath = resultPath.replace("\\", "/"); // Linux保留/
                }
            }

            // 8. 最终返回（确保虚拟路径也是标准分隔符）
            String vPathStr = vPath.toString().replace("\\", "/");
            return new String[]{resultPath, vPathStr};

        } catch (Exception e) {
            String errorPath = filePath != null ? filePath.replace("\\", "/") : "";
            return new String[]{errorPath, vPath.toString().replace("\\", "/")};
        }
    }

    public static String[] toAbsPath_old(String filePath, String absPath, StringBuilder vPath) {
        // 初始化虚拟路径，避免空指针
        if (vPath == null) {
            vPath = new StringBuilder();
        }
        vPath.setLength(0);
        vPath.append("/");

        try {
            // 1. URL解码（处理特殊字符，如%20）
            if (filePath != null && filePath.contains("%")) {
                filePath = URLDecoder.decode(filePath, StandardCharsets.UTF_8.name());
            }

            // 2. 清理路径前缀 + 统一分隔符为 /（彻底抛弃\，避免Linux/Docker识别错误）
            String cleanFilePath = filePath;
            if (cleanFilePath != null) {
                String trimmed = cleanFilePath.trim();
                // 依次修剪前缀：. ~ / \（避免路径开头的无效字符）
                trimmed = trimStart(trimmed, '.');
                trimmed = trimStart(trimmed, '~');
                trimmed = trimStart(trimmed, '/');
                trimmed = trimStart(trimmed, '\\');
                // 强制统一所有分隔符为 /（Linux/Docker 唯一正确分隔符）
                cleanFilePath = trimmed.replace("\\", "/");
            }

            // 3. 拼接绝对路径（核心：避免截断、保证路径完整）
            if (cleanFilePath == null || !cleanFilePath.contains(":")) { // 非Windows盘符路径
                // 获取基础路径（兜底为配置的文件路径）
                if (absPath == null || absPath.isEmpty()) {
                    absPath = WebConfigUtil.getFilePath(); // 确保该方法返回Linux路径，如 /home/Lserp/fileRoot
                }
                // 统一基础路径分隔符为 /
                String cleanAbsPath = absPath.replace("\\", "/");

                // 处理虚拟路径前缀（避免因fileVPath匹配错误截断路径）
                String fileVPath = WebConfigUtil.getFileVPath();
                fileVPath = (fileVPath == null ? "" : fileVPath.trim().replaceAll("^/+|/+$", ""));

                boolean startsWithVPath = false;
                boolean absContainsVPath = false;
                String subPath = cleanFilePath; // 默认使用完整路径，不截断
                if (!fileVPath.isEmpty() && cleanFilePath != null) {
                    // 大小写不敏感匹配（仅匹配开头，避免中间匹配导致截断）
                    String lowerFilePath = cleanFilePath.toLowerCase();
                    String lowerVPath = fileVPath.toLowerCase();
                    startsWithVPath = lowerFilePath.startsWith(lowerVPath);
                    absContainsVPath = cleanAbsPath.toLowerCase().contains(lowerVPath);

                    // 仅当开头匹配时才截取（避免中间匹配导致路径截丢）
                    if (startsWithVPath) {
                        subPath = cleanFilePath.substring(fileVPath.length());
                        subPath = trimStart(subPath, '/'); // 移除开头多余/，避免//
                    }
                }

                // 拼接路径（关键：用Paths.normalize保证路径完整，避免../或//）
                Path combinedPath = Paths.get(cleanAbsPath, subPath == null ? "" : subPath)
                        .toAbsolutePath() // 强制转为绝对路径
                        .normalize(); // 标准化路径（修复../、//等问题）
                cleanFilePath = combinedPath.toString().replace("\\", "/");
            }

            // 4. 生成虚拟路径（核心修复：避免vIndex错误导致路径截断）
            String fileVPath = WebConfigUtil.getFileVPath();
            fileVPath = (fileVPath == null ? "" : fileVPath.trim().replaceAll("^/+|/+$", ""));
            int vIndex = -1;
            if (!fileVPath.isEmpty() && cleanFilePath != null) {
                // 仅查找fileVPath在路径中的最后一次出现（且仅匹配完整目录名）
                String lowerFilePath = cleanFilePath.toLowerCase();
                String lowerVPath = fileVPath.toLowerCase();
                vIndex = lowerFilePath.lastIndexOf(lowerVPath);
                // 关键：校验匹配位置是否合法（避免匹配到字符串中间，如Lserp3中的3）
                if (vIndex > 0) {
                    // 确保匹配的是完整目录（前后是/或开头/结尾）
                    char prevChar = lowerFilePath.charAt(vIndex - 1);
                    boolean isWholeDir = prevChar == '/' || vIndex == 0;
                    if (!isWholeDir) {
                        vIndex = -1; // 非完整目录匹配，放弃截取
                    }
                }
            }
            log.debug(String.valueOf("vIndex: " + vIndex)); // 可注释，仅调试用

            // 生成虚拟路径（仅当vIndex合法时才截取，否则用完整路径）
            vPath.setLength(0);
            if (vIndex > -1 && cleanFilePath != null) {
                String vPathSub = cleanFilePath.substring(vIndex + fileVPath.length()).replace("\\", "/");
                vPath.append("/").append(trimStart(vPathSub, '/'));
            } else {
                // 匹配失败时，使用完整路径作为虚拟路径（避免截丢）
                vPath.append(cleanFilePath == null ? "/" : cleanFilePath.replace("\\", "/"));
            }

            // 5. 最终路径处理（保证Linux/Docker格式，无截断）
            String resultPath = cleanFilePath;
            if (resultPath != null) {
                resultPath = trimEnd(resultPath, '/'); // 移除末尾/
                resultPath = resultPath.replace("\\", "/"); // 最终统一为/
            }

            // 返回：[0]物理绝对路径 [1]虚拟路径
            return new String[]{resultPath, vPath.toString()};

        } catch (Exception e) {
            // 异常时返回原始路径（避免空路径）
            String errorPath = filePath != null ? filePath.replace("\\", "/") : "";
            vPath.setLength(0);
            vPath.append("/").append(errorPath);
            return new String[]{errorPath, vPath.toString()};
        }
    }


    /**
     * 检查文件权限（防止路径穿越）
     */
    public static boolean checkFileAuthory(String path, String attcPath) {
        if (path == null || path.isEmpty()) {
            return false;
        }
        try {
            String fullPath = normalizeCanonicalPath(path);
            return isUnderDirectory(fullPath, WebConfigUtil_web.getServerPath())
                    || isUnderDirectory(fullPath, WebConfigUtil_web.getFilePath())
                    || isUnderDirectory(fullPath, attcPath);
        } catch (Exception e) {
            return false;
        }
    }

    private static boolean isUnderDirectory(String fullPath, String allowedRoot) throws IOException {
        if (allowedRoot == null || allowedRoot.trim().isEmpty()) {
            return false;
        }
        String rootPath = normalizeCanonicalPath(allowedRoot);
        return fullPath.equals(rootPath) || fullPath.startsWith(rootPath + "/");
    }

    private static String normalizeCanonicalPath(String path) throws IOException {
        return new File(path).getCanonicalPath()
                .replace("\\", "/")
                .replaceAll("/+", "/")
                .trim();
    }

    // 线程池：用于异步执行空目录删除（对应 C# 的 Task.Start()）
    /**
     * @param filePath 文件路径（可为虚拟路径或绝对路径）
     * @param absPath  若 filePath 为虚拟路径，传递绝对盘符路径（如 "D:/data"）
     * @return BaseResponse 包含操作结果（success：是否成功，other：1 表示已删除）
     */
    public static BaseResponse deleteFile(String filePath, String absPath) {
        BaseResponse response = new BaseResponse();
        response.setSuccess(false); // 初始化为失败
        StringBuilder vPath = new StringBuilder(); // 对应 C# 的 out string vPath

        try {
            // 1. 调用之前实现的 toAbsPath 方法，转换为绝对路径（对齐 C# 的 ToAbsPath）
            String[] pathResult = toAbsPath(filePath, absPath, vPath);
            filePath = pathResult[0]; // 处理后的绝对路径（C# 方法返回值）

            // 2. 解析文件信息（对应 C# 的 FileInfo）
            File file = new File(filePath);
            String dir = file.getParent(); // 文件夹路径（对应 C# 的 info.DirectoryName）
            String filename = file.getName(); // 文件名（对应 C# 的 info.Name）
            String savePath = file.getAbsolutePath(); // 完整路径（对应 C# 的 info.FullName）

            // 3. 图片文件特殊处理：若为图片且文件夹不含 "_high"，则拼接 "_high" 后缀（原始图文件夹逻辑）
            if (ImageUtil.isImg(filename) && (dir == null || dir.indexOf("_high") < 0)) {
                // 修剪尾部 "/" 并拼接 "_high/"（对齐 C# 的 TrimEnd('/') + "_high/"）
                dir = dir == null ? "_high/" : dir.replaceAll("/$", "") + "_high/";
                // 拼接新的保存路径（对应 C# 的 Path.Combine）
                savePath = Paths.get(dir, filename).toString();
            }

            // 4. 文件权限校验（对应 C# 的 CheckFileAuthory，需自行实现权限逻辑）
            if (!checkFileAuthory(savePath, absPath)) {
                return response; // 权限不足，返回失败
            }

            // 5. 处理文件删除逻辑
            File saveFile = new File(savePath);
            if (saveFile.exists()) {
                // 5.1 图片文件：同时删除缩略图（_thum 文件夹下的 .png 文件）
                if (ImageUtil.isImg(file.getAbsolutePath().substring(file.getAbsolutePath().lastIndexOf(".")))) {
                    // 构建缩略图路径：_high 替换为 _thum，扩展名改为 .png（对齐 C# 的 Replace）
                    String thumSavePath = savePath.replace("_high", "_thum")
                            .replace(getFileExtension(file), ".png");
                    File thumFile = new File(thumSavePath);
                    if (thumFile.exists()) {
                        Files.delete(thumFile.toPath()); // 删除缩略图
                        // 异步删除空的缩略图目录（对应 C# 的 DeleteEmptyDir 异步调用）
                        String thumDir = dir == null ? "" : dir.replace("_high", "_thum");
                        ResourceExecutors.submitFileCleanup(() -> deleteEmptyDir(new File(thumDir).getName()));
                    }
                }
                // 5.2 非图片文件：删除关联的 HTML、CSS、图片目录或 PDF
                else {
                    StringBuilder vPathHtml = new StringBuilder(); // 对应 C# 的 out vpath
                    // 获取 HTML 文件路径（对应 C# 的 WebConfigUtil.GetViewDocHtmlPath）
                    String htmlPath = WebConfigUtil.getViewDocHtmlPath(savePath, vPathHtml);
                    String newFileName = filename; // 原逻辑保留文件名（可根据需求调整过滤字符）

                    // 替换 HTML 文件名（对齐 C# 的 Replace 最后一段路径为 {newFileName}.html）
                    if (htmlPath != null && !htmlPath.isEmpty()) {
                        String[] htmlPathParts = htmlPath.split("/");
                        if (htmlPathParts.length > 0) {
                            htmlPathParts[htmlPathParts.length - 1] = newFileName + ".html";
                            htmlPath = String.join("/", htmlPathParts);
                        }
                    }

                    File htmlFile = new File(htmlPath);
                    if (htmlFile.exists()) {
                        Files.delete(htmlFile.toPath()); // 删除 HTML 文件

                        // 删除关联的 CSS 文件（.html 替换为 _styles.css）
                        String cssPath = htmlPath.replace(".html", "_styles.css");
                        File cssFile = new File(cssPath);
                        if (cssFile.exists()) {
                            Files.delete(cssFile.toPath());
                        }

                        // 删除关联的图片目录（.html 替换为 _images）
                        String imgDirPath = htmlPath.replace(".html", "_images");
                        File imgDir = new File(imgDirPath);
                        if (imgDir.exists() && imgDir.isDirectory()) {
                            deleteDirRecursively(imgDir); // 递归删除目录及内容
                        }
                    }
                    // 若 HTML 不存在，检查并删除 PDF 文件（.html 替换为 .pdf）
                    else {
                        htmlPath = htmlPath.replace(".html", ".pdf");
                        File pdfFile = new File(htmlPath);
                        if (pdfFile.exists()) {
                            Files.delete(pdfFile.toPath());
                        }
                    }
                }

                // 5.3 删除主文件
                Files.delete(saveFile.toPath());
                // 异步删除空的主目录（对应 C# 的 DeleteEmptyDir）
                String finalDir = dir;
                ResourceExecutors.submitFileCleanup(() -> deleteEmptyDir(new File(finalDir).getName()));
                response.setOther(1); // 标记已删除
            }
            // 6. 若主文件不存在，尝试删除无 "_high" 后缀的文件
            else {
                savePath = savePath.replace("_high", "");
                File noHighFile = new File(savePath);
                if (noHighFile.exists()) {
                    Files.delete(noHighFile.toPath());
                    response.setOther(1);
                }
                // 7. 若仍不存在，尝试删除 WMV 转 MP4 的文件（小写匹配）
                else {
                    savePath = savePath.toLowerCase().replace("_wmv", "mp4");
                    File mp4File = new File(savePath);
                    if (mp4File.exists()) {
                        Files.delete(mp4File.toPath());
                        response.setOther(1);
                    }
                }
            }

            // 8. 异步删除无 "_high" 后缀的空目录
            String noHighDir = dir == null ? "" : dir.replace("_high", "");
            ResourceExecutors.submitFileCleanup(() -> deleteEmptyDir(new File(noHighDir).getName()));

            // 9. 操作成功
            response.setSuccess(true);

        } catch (Exception e) {
            // 异常时保持 success=false（不抛出，避免崩溃，对齐 C# 静默处理）
            log.error("Exception caught", e); // 建议生产环境替换为日志记录（如 log.error）
        }

        return response;
    }

    /**
     * 获取文件扩展名（不含 "."，如 "png"、"txt"）
     *
     * @param file 文件对象
     * @return 扩展名（空字符串表示无扩展名）
     */
    private static String getFileExtension(File file) {
        String fileName = file.getName();
        int dotIndex = fileName.lastIndexOf(".");
        return dotIndex > 0 ? fileName.substring(dotIndex) : "";
    }

    // 辅助方法：获取文件扩展名
    public static String getFileExtension(String fileName) {
        // 1. null 保护 + 修剪首尾空格（对齐 C# 的 Trim()）
        if (fileName == null) {
            return "";
        }
        String trimmedFileName = fileName.trim();

        // 2. 无点则返回空
        if (!trimmedFileName.contains(".")) {
            return "";
        }

        // 3. 取最后一个点后的字符（纯扩展名）
        return trimmedFileName.substring(trimmedFileName.lastIndexOf(".") + 1);
    }

    /**
     * 递归删除目录及所有子内容（用于删除非空目录，如 _images）
     *
     * @param dir 待删除的目录
     * @throws IOException 目录删除异常
     */
    private static void deleteDirRecursively(File dir) throws IOException {
        if (!dir.exists()) {
            return;
        }
        // 递归删除子文件和子目录
        File[] files = dir.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    deleteDirRecursively(file);
                } else {
                    Files.delete(file.toPath());
                }
            }
        }
        // 删除空目录
        Files.delete(dir.toPath());
    }

    // ------------------------------
    // 资源释放：关闭线程池（建议在应用关闭时调用）
    // ------------------------------
    public static void shutdownExecutor() {
        ResourceExecutors.shutdownFileCleanup();
    }

    /**
     * 对齐 C# 的 DeleteEmptyDir(string dir)：异步删除空目录
     *
     * @param dir 目录路径字符串
     */
    public static void deleteEmptyDir(String dir) {
        if (dir == null || dir.isEmpty()) {
            return;
        }
        File dirFile = new File(dir);
        // 异步执行（对应 C# 的 Task.Start()）
        ResourceExecutors.submitFileCleanup(() -> deleteEmptyDir(dirFile, true));
    }


    /**
     * 对齐 C# 的 DeleteEmptyDir(DirectoryInfo dirInfo, bool delParent)：递归删除空目录
     *
     * @param dirFile   目录 File 对象（对应 C# 的 DirectoryInfo）
     * @param delParent 是否删除父目录（递归向上清理）
     */
    public static void deleteEmptyDir(File dirFile, boolean delParent) {
        try {
            // 目录不存在，直接返回
            if (!dirFile.exists() || !dirFile.isDirectory()) {
                return;
            }

            // 1. 递归清理子目录（先处理所有子目录）
            File[] childDirs = dirFile.listFiles(File::isDirectory);
            if (childDirs != null && childDirs.length > 0) {
                for (File childDir : childDirs) {
                    deleteEmptyDir(childDir, false); // 子目录不递归删除父目录
                }
            }

            // 2. 检查当前目录是否为空（无文件 + 无子目录）
            File[] files = dirFile.listFiles();
            if (files == null || files.length == 0) {
                // 3. 删除当前空目录
                Files.delete(dirFile.toPath());
                // 4. 若允许删除父目录，递归清理父目录
                if (delParent) {
                    File parentDir = dirFile.getParentFile();
                    if (parentDir != null) {
                        deleteEmptyDir(parentDir, true);
                    }
                }
            }

        } catch (Exception e) {
            // 对齐 C# 的 catch 静默处理（建议生产环境加日志）
            log.error("Exception caught", e);
        }
    }

    /**
     * 生成缩略图
     */
    public static String toThumImg(String filePath, boolean toThum, boolean comp) {
        if (!ImageUtil.isImg(filePath)) {
            return filePath;
        }

        try {
            File file = new File(filePath);
            String fileDir = file.getParent();

            // 压缩图片
            if (comp) {
                long fileSizeKb = file.length() / 1024;
                int maxSize = Math.max(100, Integer.parseInt(WebConfigUtil_web.get("ComprAttImgMaxKB", "100")));
                if (fileSizeKb > maxSize) {
                    int width = Math.max(300, Integer.parseInt(WebConfigUtil_web.get("ComprAttImgWidth", "300")));
                    ImageUtil.format(filePath, filePath, "png", ImageUtil.ImgDeep.D8, width, width);
                }
            }

            // 生成缩略图
            if (toThum) {
                String thumDir = fileDir.replace("_high", "") + "_thum";
                File thumDirFile = new File(thumDir);
                if (!thumDirFile.exists()) {
                    thumDirFile.mkdirs();
                }

                String thumPath = Paths.get(thumDir,
                        file.getName().replace(getFileExtension(file.getName()), ".png")).toString();
                ImageUtil.format(filePath, thumPath, "png", ImageUtil.ImgDeep.D8, 300, 300);
            }

        } catch (Exception e) {
            // 记录日志
        }

        return filePath;
    }

    /**
     * 根据匹配规则删除目录
     *
     * @param dir   父目录路径
     * @param match 目录匹配规则（返回true则删除该目录）
     */
    public static void deleteFloder(String dir, Predicate<File> match) {
        try {
            // 获取目录下的所有子目录
            File[] subDirs = new File(dir).listFiles(File::isDirectory);
            if (subDirs == null) {
                return; // 目录不存在或不是目录
            }

            for (File subDir : subDirs) {
                try {
                    // 匹配规则为空或满足匹配条件时删除目录
                    if (match == null || match.test(subDir)) { // 现在可以正常解析test方法
                        deleteDir(subDir.getAbsolutePath());
                    }
                } catch (Exception e) {
                    // 忽略单个目录删除失败的异常
                }
            }
        } catch (Exception e) {
            // 忽略整体遍历异常
        }
    }

    /**
     * 递归删除目录及其包含的所有文件和子目录
     *
     * @param srcPath 要删除的目录路径
     */
    public static void deleteDir(String srcPath) {
        File dir = new File(srcPath);
        if (!dir.exists()) {
            return;
        }

        try {
            // 递归删除所有子文件和子目录
            deleteDirectoryContents(dir);
            // 删除空目录
            if (!dir.delete()) {
                throw new IOException("无法删除目录: " + srcPath);
            }
        } catch (Exception e) {
            throw new RuntimeException("删除目录失败: " + srcPath, e);
        }
    }

    /**
     * 递归删除目录下的所有内容（文件和子目录）
     *
     * @param dir 要清理的目录
     */
    private static void deleteDirectoryContents(File dir) throws IOException {
        File[] files = dir.listFiles();
        if (files == null) {
            throw new IOException("无法列出目录内容: " + dir.getAbsolutePath());
        }

        for (File file : files) {
            if (file.isDirectory()) {
                // 递归删除子目录
                deleteDirectoryContents(file);
                // 删除空的子目录
                if (!file.delete()) {
                    throw new IOException("无法删除子目录: " + file.getAbsolutePath());
                }
            } else {
                // 删除文件
                if (!file.delete()) {
                    throw new IOException("无法删除文件: " + file.getAbsolutePath());
                }
            }
        }
    }

    /**
     * 转换视频格式
     */
    public static String convertVideo(String sourceFile, String fileType, StringBuilder fileName) {
        if ("wmv".equalsIgnoreCase(fileType)) {
            String mp4Path = sourceFile.replace(fileType, "mp4");
            FormatFactoryUtil.convertVideo(sourceFile, mp4Path, false, true);
            fileName = new StringBuilder(fileName.toString().replace(fileType, "mp4"));
            return mp4Path;
        }
        return sourceFile;
    }


    /**
     * 获取文件编码格式
     *
     * @param filePath 文件路径
     * @return 检测到的编码格式，默认返回系统默认编码
     */
    public static Charset getFileEncoding(String filePath) {
        // 默认使用系统默认编码
        Charset encoding = Charset.defaultCharset();
        File file = new File(filePath);

        try (FileInputStream fis = new FileInputStream(file)) {
            // 创建编码检测器
            UniversalDetector detector = new UniversalDetector(null);
            byte[] buffer = new byte[4096];
            int bytesRead;

            // 读取文件内容进行检测
            while ((bytesRead = fis.read(buffer)) > 0 && !detector.isDone()) {
                detector.handleData(buffer, 0, bytesRead);
            }

            // 完成检测
            detector.dataEnd();

            // 获取检测到的编码名称
            String charsetName = detector.getDetectedCharset();
            if (charsetName != null) {
                encoding = Charset.forName(charsetName);
            }

            // 重置检测器状态（可选，用于重复使用）
            detector.reset();

        } catch (IOException e) {
            // 处理文件读取异常，这里简单打印异常信息，实际应用中可根据需要处理
            log.error("Exception caught", e);
        }

        return encoding;
    }

    /**
     * 对齐 C# 的 GetTextFileEncodingType 方法：检测文本文件的编码格式
     *
     * @param filePath 文件路径（绝对路径）
     * @return 检测到的编码（Charset 对象，对应 C# 的 Encoding）
     * @throws IOException      文件读取异常（如文件不存在、无读取权限）
     * @throws RuntimeException 非预期字节格式异常（对应 C# 的 Exception）
     */
    public static Charset getTextFileEncodingType(String filePath) throws IOException {
        // 1. 默认编码：对应 C# 的 Encoding.Default（Java 用系统默认编码）
        Charset encoding = Charset.defaultCharset();

        // 2. 读取文件字节流（对应 C# 的 FileStream + BinaryReader）
        // 注：Java 无 BinaryReader，用 FileInputStream 结合 BufferedInputStream 实现字节读取
        try (InputStream fs = new FileInputStream(filePath);
             BufferedInputStream bufferedIn = new BufferedInputStream(fs)) {

            // 读取文件所有字节（对应 C# 的 binaryReader.ReadBytes((int)fs.Length)）
            byte[] buffer = readAllBytes(bufferedIn);

            // 3. BOM 检测：按 C# 逻辑顺序判断（UTF-8 BOM → UTF-16 BE → UTF-16 LE）
            // 3.1 检测 UTF-8 BOM（头部字节：0xEF 0xBB 0xBF）
            if (buffer.length >= 3 && buffer[0] == (byte) 0xEF && buffer[1] == (byte) 0xBB && buffer[2] == (byte) 0xBF) {
                encoding = StandardCharsets.UTF_8;
            }
            // 3.2 检测 BigEndianUnicode（UTF-16 BE，头部字节：0xFE 0xFF 0x00，对应 C# 的 Encoding.BigEndianUnicode）
            else if (buffer.length >= 3 && buffer[0] == (byte) 0xFE && buffer[1] == (byte) 0xFF && buffer[2] == (byte) 0x00) {
                encoding = StandardCharsets.UTF_16BE;
            }
            // 3.3 检测 Unicode（UTF-16 LE，头部字节：0xFF 0xFE 0x41（即字符 'A'），对应 C# 的 Encoding.Unicode）
            else if (buffer.length >= 3 && buffer[0] == (byte) 0xFF && buffer[1] == (byte) 0xFE && buffer[2] == (byte) 0x41) {
                encoding = StandardCharsets.UTF_16LE;
            }
            // 3.4 无 BOM 时，检测是否为 UTF-8（对应 C# 的 IsUTF8Bytes）
            else if (isUTF8Bytes(buffer)) {
                encoding = StandardCharsets.UTF_8;
            }

            return encoding;
        }
    }

    /**
     * 对齐 C# 的 IsUTF8Bytes 方法：判断字节数组是否为无 BOM 的 UTF-8 格式
     * 核心逻辑：遵循 UTF-8 多字节编码规则（RFC 3629）
     *
     * @param data 待检测的字节数组
     * @return true：是无 BOM UTF-8，false：不是
     * @throws RuntimeException 非预期的字节格式（对应 C# 的 throw new Exception）
     */
    private static boolean isUTF8Bytes(byte[] data) {
        int charByteCounter = 1; // 记录当前字符还需读取的后续字节数（UTF-8 多字节字符的后续字节数）
        byte curByte; // 当前分析的字节

        for (int i = 0; i < data.length; i++) {
            curByte = data[i];

            // 情况1：当前是多字节字符的「起始字节」（需判断后续应跟随的字节数）
            if (charByteCounter == 1) {
                // UTF-8 单字节字符：0xxxxxxx（首位为 0），无需处理；多字节字符：1xxxxxxx（首位为 1）
                if (curByte >= 0x80) {
                    // 计算后续应跟随的字节数：通过「起始字节的连续 1 的个数」判断（如 110xxxxx → 后续1字节，1110xxxx → 后续2字节）
                    while (((curByte <<= 1) & 0x80) != 0) {
                        charByteCounter++;
                    }

                    // UTF-8 规则：起始字节的连续 1 个数需在 2~6 之间（对应 2~6 字节字符，实际常用 2~3 字节）
                    if (charByteCounter == 1 || charByteCounter > 6) {
                        return false;
                    }
                }
            }
            // 情况2：当前是多字节字符的「后续字节」（必须满足 10xxxxxx 格式）
            else {
                // 后续字节规则：首位必须是 10（即 (curByte & 0xC0) == 0x80，0xC0 是 11000000，0x80 是 10000000）
                if ((curByte & 0xC0) != 0x80) {
                    return false;
                }
                charByteCounter--; // 后续字节数减 1，直到回到 1（表示当前字符处理完成）
            }
        }

        // 遍历结束后，若仍有未处理完的后续字节（charByteCounter > 1），说明字节格式不完整
        if (charByteCounter > 1) {
            throw new RuntimeException("非预期的byte格式");
        }

        return true;
    }

    // ------------------------------
    // 辅助方法：读取 InputStream 所有字节（对应 C# 的 BinaryReader.ReadBytes((int)fs.Length)）
    // ------------------------------
    private static byte[] readAllBytes(InputStream inputStream) throws IOException {
        // 用 ByteArrayOutputStream 缓存所有字节
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            byte[] tempBuffer = new byte[1024]; // 1KB 缓冲区（平衡性能与内存）
            int bytesRead;
            // 循环读取直到流结束
            while ((bytesRead = inputStream.read(tempBuffer)) != -1) {
                outputStream.write(tempBuffer, 0, bytesRead);
            }
            outputStream.flush();
            return outputStream.toByteArray();
        }
    }

    /**
     * 下载远程文件到本地
     *
     * @param downUrl  远程文件URL
     * @param savePath 本地保存路径
     * @return 下载成功返回true，失败返回false
     */
    public static boolean downloadFile(String downUrl, String savePath) {
        return DownLoadFile(downUrl, savePath);
    }


    /**
     * 异步下载文件
     *
     * @param url        下载地址
     * @param savePath   保存路径
     * @param onProgress 进度回调（0-100）
     * @param onFinish   完成回调
     * @param onFail     失败回调
     */
    public static void asyncDownload(String url, String savePath,
                                     Consumer<Integer> onProgress,
                                     Consumer<String> onFinish,
                                     Consumer<Exception> onFail) {
        // 创建RestTemplate并设置超时
        if (startGuardedAsyncDownload(url, savePath, onProgress, onFinish, onFail)) {
            return;
        }
        // 异步执行下载
        /*
        RestTemplate restTemplate = new RestTemplate();
        new Thread(() -> {
            try {
                URI uri = new URI(url);
                HttpHeaders headers = new HttpHeaders();
                headers.add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");

                // 请求回调：设置请求头
                RequestCallback requestCallback = request -> {
                    request.getHeaders().putAll(headers);
                };

                // 响应提取器：处理文件流并计算进度
                ResponseExtractor<Void> responseExtractor = response -> {
                    try (InputStream in = response.getBody();
                         OutputStream out = new FileOutputStream(savePath)) {

                        long contentLength = response.getHeaders().getContentLength();
                        byte[] buffer = new byte[4096];
                        int bytesRead;
                        long totalRead = 0;

                        while ((bytesRead = in.read(buffer)) != -1) {
                            out.write(buffer, 0, bytesRead);
                            totalRead += bytesRead;

                            // 计算并回调进度
                            if (contentLength > 0 && onProgress != null) {
                                int progress = (int) (totalRead * 100 / contentLength);
                                onProgress.accept(progress);
                            }
                        }

                        // 确保最后进度为100%
                        if (contentLength > 0 && onProgress != null) {
                            onProgress.accept(100);
                        }

                        return null;
                    } catch (IOException e) {
                        if (onFail != null) {
                            onFail.accept(e);
                        }
                        throw e;
                    }
                };

                // 执行下载
                restTemplate.execute(uri, HttpMethod.GET, requestCallback, responseExtractor);

                // 下载完成回调
                if (onFinish != null) {
                    onFinish.accept(savePath);
                }

            } catch (Exception e) {
                // 失败回调
                if (onFail != null) {
                    onFail.accept(e);
                }
            }
        }).start();
        */
    }


    // 辅助方法：构建Web路径
    private static boolean startGuardedAsyncDownload(String url, String savePath,
                                                     Consumer<Integer> onProgress,
                                                     Consumer<String> onFinish,
                                                     Consumer<Exception> onFail) {
        new Thread(() -> {
            try {
                if (onProgress != null) {
                    onProgress.accept(0);
                }
                if (!DownLoadFile(url, savePath)) {
                    throw new IOException("Remote download failed or was blocked");
                }
                if (onProgress != null) {
                    onProgress.accept(100);
                }
                if (onFinish != null) {
                    onFinish.accept(savePath);
                }
            } catch (Exception e) {
                if (onFail != null) {
                    onFail.accept(e);
                }
            }
        }).start();
        return true;
    }

    private static String buildWebPath(String relativePath, String appDomain) throws UnsupportedEncodingException {
        // 1. 处理域名：确保结尾无 /
        String domain = (appDomain != null && !appDomain.isEmpty())
                ? appDomain.trim().replaceAll("/$", "") // 移除结尾的 /
                : WebConfigUtil_web.getFileDomain().trim().replaceAll("/$", "");

        // 2. 处理相对路径编码（保持以 / 开头）
        String encodedPath = (appDomain != null && !appDomain.isEmpty())
                ? urlEncode(relativePath, false).replace("//", "/")
                : urlEncode(relativePath, false, true).replace("//", "/");

        // 3. 直接拼接（domain 无 / 结尾 + encodedPath 以 / 开头 → 自动形成单 / 分隔）
        return domain + encodedPath;
    }

    // 辅助方法：URL编码
    public static String urlEncode(String content, boolean encodeChinese, boolean toPathStr) throws UnsupportedEncodingException {
        String ret = content;
        if (encodeChinese) {
            ret = URLEncoder.encode(content, StandardCharsets.UTF_8);

        } else {
            StringBuilder sb = new StringBuilder();
            for (char c : content.toCharArray()) {
                if (c == ' ') {
                    sb.append("%20");
                } else if (!Pattern.matches("[\\u4e00-\\u9fa5]", String.valueOf(c))) {
                    sb.append(URLEncoder.encode(String.valueOf(c), StandardCharsets.UTF_8));
                } else {
                    sb.append(c);
                }
            }
            ret = sb.toString();
        }
        if (toPathStr) return ret.replace("%5C", "/").replace("%2F", "/").replace("//", "/");
        return ret;
    }

    public static String urlEncode(String content, boolean encodeChinese) throws UnsupportedEncodingException {
        return urlEncode(content, encodeChinese, true);
    }

    // 辅助方法：获取当前请求
    private static HttpServletRequest getRequest() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return attributes != null ? attributes.getRequest() : null;
    }

    private static Path getUtf8Path(String first, String... more) {
        try {
            // 强制将路径字符串按UTF-8编码为字节，再转回字符串（绕过系统locale）
            String utf8First = new String(first.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
            String[] utf8More = new String[more.length];
            for (int i = 0; i < more.length; i++) {
                utf8More[i] = new String(more[i].getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8);
            }
            return Paths.get(utf8First, utf8More);
        } catch (Exception e) {
            return Paths.get(first, more);
        }
    }


}
