当前位置:首页 > 技术 > 正文内容

基于Apache POI的Excel物流订单解析方案

访客 技术 2026年7月1日 1

问题背景

在实际物流订单处理场景中,Excel模板常包含合并单元格、内嵌图片等复杂结构。本文针对以下三个核心问题提供解决方案:合并单元格导致同一箱组生成不同BOX编号、箱数读取异常、合并组标记不一致。

一、核心实现代码

// 图片缓存容器(键:图片ID,值:二进制数据)
private static Map<String, byte[]> imageCache = new HashMap<>();

/**
 * 主入口:解析物流Excel文件
 */
public static LogisticsOrderDTO parseLogisticsFile(MultipartFile multipartFile) throws IOException {
    validateFile(multipartFile);
    InputStream stream = multipartFile.getInputStream();
    XSSFWorkbook workbook = null;
    try {
        workbook = new XSSFWorkbook(stream);
        XSSFSheet targetSheet = locateSheet(workbook, "单票下单");
        if (targetSheet == null) {
            throw new IllegalArgumentException("Excel文件中缺少【单票下单】工作表");
        }
        preloadImages(workbook);
        SheetParseResult parseResult = extractSheetContent(targetSheet);
        LogisticsOrderDTO order = buildMainOrder(parseResult.getRawData());
        List<P0ProductInfoDTO> products = buildProductList(parseResult);
        order.setProductList(products);
        return order;
    } finally {
        if (stream != null) stream.close();
        if (workbook != null) workbook.close();
    }
}

/**
 * 预加载所有工作表中的图片
 */
private static void preloadImages(Workbook workbook) {
    imageCache.clear();
    if (!(workbook instanceof XSSFWorkbook)) return;
    XSSFWorkbook xw = (XSSFWorkbook) workbook;
    for (int i = 0; i < xw.getNumberOfSheets(); i++) {
        Sheet sheet = xw.getSheetAt(i);
        if (sheet instanceof XSSFSheet) {
            XSSFDrawing drawing = ((XSSFSheet) sheet).getDrawingPatriarch();
            if (drawing != null) {
                for (XSSFShape shape : drawing.getShapes()) {
                    if (shape instanceof XSSFPicture) {
                        XSSFPicture pic = (XSSFPicture) shape;
                        String imageId = parseImageId(pic.getPictureData().getPackagePart().getPartName().toString());
                        if (imageId != null) {
                            imageCache.put(imageId, pic.getPictureData().getData());
                        }
                    }
                }
            }
        }
    }
}

/**
 * 从路径中提取图片ID
 */
private static String parseImageId(String partName) {
    if (partName.contains("ID_")) {
        return partName.substring(partName.indexOf("ID_"), partName.lastIndexOf("."));
    }
    return partName.substring(partName.lastIndexOf("/") + 1, partName.lastIndexOf("."));
}

/**
 * 从公式 =DISPIMG("ID_XXX") 中提取ID
 */
private static String extractImageId(String formula) {
    Pattern pattern = Pattern.compile("\"(ID_[A-F0-9]+)\"");
    Matcher matcher = pattern.matcher(formula);
    if (matcher.find()) {
        return matcher.group(1);
    }
    return null;
}

/**
 * 安全获取单元格值
 */
private static String getCellValueSafely(Row row, int col) {
    Cell cell = row.getCell(col);
    if (cell == null) return "";
    cell.setCellType(CellType.STRING);
    return cell.getStringCellValue().trim();
}

/**
 * 文件校验
 */
private static void validateFile(MultipartFile file) {
    if (file == null || file.isEmpty()) {
        throw new IllegalArgumentException("上传文件不能为空");
    }
    String filename = file.getOriginalFilename();
    if (filename == null || !filename.toLowerCase().endsWith(".xlsx")) {
        throw new IllegalArgumentException("仅支持.xlsx格式,当前文件:" + filename);
    }
    if (file.getSize() > 10 * 1024 * 1024) {
        throw new IllegalArgumentException("文件大小不能超过10MB");
    }
}

/**
 * 模糊查找目标工作表
 */
private static XSSFSheet locateSheet(XSSFWorkbook workbook, String name) {
    for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
        XSSFSheet sheet = workbook.getSheetAt(i);
        if (name.equalsIgnoreCase(sheet.getSheetName())) {
            return sheet;
        }
    }
    return null;
}

/**
 * 核心提取:按合并范围分组处理箱子信息
 */
private static SheetParseResult extractSheetContent(XSSFSheet sheet) {
    List<List<String>> rawData = new ArrayList<>();
    List<RowGroup> groups = new ArrayList<>();
    int boxSeq = 1;
    final int PRODUCT_START = 18;

    // 收集合并行映射
    Map<Integer, Integer> mergeMap = new HashMap<>();
    for (int rowIdx = PRODUCT_START; rowIdx < sheet.getPhysicalNumberOfRows(); rowIdx++) {
        XSSFRow row = sheet.getRow(rowIdx);
        if (row == null) continue;
        XSSFCell skuCell = row.getCell(0);
        if (skuCell == null || StringUtils.isBlank(getCellString(skuCell))) continue;

        Integer mergeEnd = getMergeEndRow(sheet, rowIdx, 1);
        if (mergeEnd == null) {
            mergeMap.put(rowIdx, rowIdx);
            continue;
        }
        boolean consistent = mergeEnd.equals(getMergeEndRow(sheet, rowIdx, 2))
                && mergeEnd.equals(getMergeEndRow(sheet, rowIdx, 3))
                && mergeEnd.equals(getMergeEndRow(sheet, rowIdx, 4));
        if (consistent) {
            mergeMap.put(rowIdx, mergeEnd);
            for (int i = rowIdx + 1; i <= mergeEnd; i++) {
                mergeMap.put(i, mergeEnd);
            }
        } else {
            mergeMap.put(rowIdx, rowIdx);
        }
    }

    // 生成合并组
    Set<String> rangeSet = new HashSet<>();
    for (Map.Entry<Integer, Integer> entry : mergeMap.entrySet()) {
        int start = entry.getKey();
        int end = entry.getValue();
        if (start != end) rangeSet.add(start + "_" + end);
    }
    for (String range : rangeSet) {
        String[] parts = range.split("_");
        int start = Integer.parseInt(parts[0]);
        int end = Integer.parseInt(parts[1]);
        Integer boxCount = getFirstBoxCount(sheet, start, end);
        groups.add(new RowGroup(start, end, boxSeq++, boxCount));
    }

    // 读取所有行数据
    int dataIdx = 0;
    Map<Integer, Integer> excelToData = new HashMap<>();
    for (int rowIdx = 0; rowIdx < sheet.getPhysicalNumberOfRows(); rowIdx++) {
        XSSFRow row = sheet.getRow(rowIdx);
        if (row == null) continue;
        List<String> rowData = readRow(sheet, row, rowIdx);
        if (rowData.stream().allMatch(StringUtils::isBlank)) continue;
        rawData.add(rowData);
        excelToData.put(rowIdx, dataIdx++);
    }

    // 构建合并区域映射
    Map<Integer, MergeArea> areaMap = new HashMap<>();
    for (RowGroup group : groups) {
        Integer startData = excelToData.get(group.startRow);
        if (startData == null) continue;
        for (int rowIdx = group.startRow; rowIdx <= group.endRow; rowIdx++) {
            Integer dataRow = excelToData.get(rowIdx);
            if (dataRow == null) continue;
            areaMap.put(dataRow, new MergeArea(
                    group.startRow, group.endRow,
                    1, 4,
                    group.boxNo, group.mergeSku,
                    group.boxCount, startData
            ));
        }
    }
    return new SheetParseResult(rawData, areaMap);
}

/**
 * 获取合并范围内第一个有效箱数
 */
private static Integer getFirstBoxCount(XSSFSheet sheet, int start, int end) {
    for (int rowIdx = start; rowIdx <= end; rowIdx++) {
        XSSFRow row = sheet.getRow(rowIdx);
        if (row == null) continue;
        XSSFCell cell = row.getCell(4);
        if (cell == null || cell.getCellType() == CellType.BLANK.getCode()) continue;
        try {
            if (cell.getCellType() == CellType.FORMULA.getCode()) {
                if (cell.getCachedFormulaResultType() == CellType.NUMERIC.getCode()) {
                    return (int) cell.getNumericCellValue();
                } else if (cell.getCachedFormulaResultType() == CellType.STRING.getCode()) {
                    String cleaned = cell.getStringCellValue().trim().replaceAll("[^0-9]", "");
                    return cleaned.isEmpty() ? 0 : Integer.parseInt(cleaned);
                }
            } else if (cell.getCellType() == CellType.NUMERIC.getCode()) {
                return (int) cell.getNumericCellValue();
            } else if (cell.getCellType() == CellType.STRING.getCode()) {
                String cleaned = cell.getStringCellValue().trim().replaceAll("[^0-9]", "");
                return cleaned.isEmpty() ? 0 : Integer.parseInt(cleaned);
            }
        } catch (Exception e) {
            System.err.println("行" + (rowIdx + 1) + "箱数解析失败:" + e.getMessage());
        }
    }
    return 0;
}

/**
 * 获取合并区域的结束行
 */
private static Integer getMergeEndRow(XSSFSheet sheet, int row, int col) {
    for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
        CellRangeAddress region = sheet.getMergedRegion(i);
        if (region.isInRange(row, col)) return region.getLastRow();
    }
    return null;
}

/**
 * 读取一行数据(合并单元格处理)
 */
private static List<String> readRow(XSSFSheet sheet, XSSFRow row, int rowIdx) {
    List<String> data = new ArrayList<>();
    int lastCol = row.getLastCellNum();
    for (int col = 0; col <= lastCol; col++) {
        XSSFCell cell = row.getCell(col);
        CellRangeAddress mergeRegion = findMergeRegion(sheet, rowIdx, col);
        if (mergeRegion != null) {
            XSSFRow mergeRow = sheet.getRow(mergeRegion.getFirstRow());
            cell = mergeRow.getCell(mergeRegion.getFirstColumn());
        }
        data.add(getCellString(cell));
    }
    return data;
}

/**
 * 查找合并区域
 */
private static CellRangeAddress findMergeRegion(XSSFSheet sheet, int row, int col) {
    for (int i = 0; i < sheet.getNumMergedRegions(); i++) {
        CellRangeAddress region = sheet.getMergedRegion(i);
        if (region.isInRange(row, col)) return region;
    }
    return null;
}

/**
 * 获取单元格字符串值
 */
private static String getCellString(XSSFCell cell) {
    if (cell == null) return "";
    switch (cell.getCellTypeEnum()) {
        case STRING:
            return StringUtils.trim(cell.getStringCellValue());
        case NUMERIC:
            if (DateUtil.isCellDateFormatted(cell)) {
                return DateUtil.getJavaDate(cell.getNumericCellValue()).toString().replace("T", " ");
            } else {
                cell.setCellType(CellType.STRING);
                return StringUtils.trim(cell.getStringCellValue());
            }
        case BOOLEAN:
            return String.valueOf(cell.getBooleanCellValue());
        case FORMULA:
            try {
                String formula = cell.getCellFormula();
                if (formula != null && formula.startsWith("DISPIMG")) {
                    return "=" + formula;
                }
                if (cell.getCachedFormulaResultType() == CellType.STRING.getCode()) {
                    return StringUtils.trim(cell.getStringCellValue());
                } else if (cell.getCachedFormulaResultType() == CellType.NUMERIC.getCode()) {
                    return String.valueOf((int) cell.getNumericCellValue());
                }
                return StringUtils.trim(cell.getStringCellValue());
            } catch (Exception e) {
                return String.valueOf(cell.getNumericCellValue());
            }
        default:
            return "";
    }
}

/**
 * 解析订单主信息
 */
private static LogisticsOrderDTO buildMainOrder(List<List<String>> rawData) {
    LogisticsOrderDTO order = new LogisticsOrderDTO();
    int maxRow = Math.min(16, rawData.size());
    for (int row = 0; row < maxRow; row++) {
        List<String> rowData = rawData.get(row);
        if (rowData.isEmpty()) continue;
        String key = safeGet(rowData, 0);
        if (StringUtils.isBlank(key)) continue;
        switch (key) {
            case "客户单号:":
                order.setCustomerOrderNo(safeGet(rowData, 1));
                order.setLogisticsChannel(safeGet(rowData, 7));
                break;
            case "*仓库代码:":
                order.setWarehouseCode(safeGet(rowData, 1));
                order.setTaxIncluded(safeGet(rowData, 7));
                break;
            case "收件人公司":
                order.setRecipientCompany(safeGet(rowData, 1));
                order.setCustomsDeclaration(safeGet(rowData, 7));
                break;
            case "收件人姓名":
                order.setRecipientName(safeGet(rowData, 1));
                order.setCustomsClearanceMethod(safeGet(rowData, 7));
                break;
            case "*收件人电话":
                order.setRecipientPhone(safeGet(rowData, 1));
                break;
            case "*收件人地址":
                order.setRecipientAddress(safeGet(rowData, 1));
                order.setWithElectricity(safeGet(rowData, 7));
                break;
            case "*收件人城市":
                order.setRecipientCity(safeGet(rowData, 1));
                order.setWithMagnetism(safeGet(rowData, 7));
                break;
            case "*收件人省份":
                order.setRecipientProvince(safeGet(rowData, 1));
                order.setDeclareCurrency(safeGet(rowData, 7));
                break;
            case "*收件人邮编":
                order.setRecipientPostalCode(safeGet(rowData, 1));
                order.setEinCbpNumber(safeGet(rowData, 7));
                break;
            case "*Country国家/区域:":
                order.setCountryRegion(safeGet(rowData, 1));
                order.setCompanyName(safeGet(rowData, 7));
                break;
            case "*FBA Shipment ID":
                order.setFbaShipmentId(safeGet(rowData, 1));
                break;
            case "*Amazon Reference ID":
                order.setAmazonReferenceId(safeGet(rowData, 1));
                break;
            case "*所在货站":
                order.setShipmentStation(safeGet(rowData, 1));
                break;
            default:
                break;
        }
    }
    validateOrderFields(order);
    return order;
}

/**
 * 解析产品列表(合并组统一处理)
 */
private static List<P0ProductInfoDTO> buildProductList(SheetParseResult parseResult) {
    List<List<String>> rawData = parseResult.getRawData();
    Map<Integer, MergeArea> areaMap = parseResult.getAreaMap();
    List<P0ProductInfoDTO> products = new ArrayList<>();
    int start = 18;
    if (rawData.size() <= start) {
        throw new IllegalArgumentException("Excel中缺少产品数据(需从第19行开始)");
    }
    for (int dataIdx = start; dataIdx < rawData.size(); dataIdx++) {
        List<String> row = rawData.get(dataIdx);
        if (row.isEmpty()) continue;
        String sku = safeGet(row, 0);
        if (StringUtils.isBlank(sku)) continue;
        P0ProductInfoDTO product = new P0ProductInfoDTO();
        product.setSku(sku);
        MergeArea area = areaMap.get(dataIdx);
        boolean merged = area != null;
        if (merged) {
            product.setLengthCm(parseBigDecimal(safeGet(row, 1), "长度"));
            product.setWidthCm(parseBigDecimal(safeGet(row, 2), "宽度"));
            product.setHeightCm(parseBigDecimal(safeGet(row, 3), "高度"));
            product.setBoxCount(area.boxCount);
            product.setBoxNo(area.boxNo);
            product.setIsMerge(1);
            product.setMergeSku(area.mergeSku);
        } else {
            product.setLengthCm(parseBigDecimal(safeGet(row, 1), "长度"));
            product.setWidthCm(parseBigDecimal(safeGet(row, 2), "宽度"));
            product.setHeightCm(parseBigDecimal(safeGet(row, 3), "高度"));
            product.setBoxCount(parseInteger(safeGet(row, 4), "箱数"));
            product.setBoxNo(sku);
            product.setIsMerge(0);
            product.setMergeSku(sku);
        }
        product.setSingleGrossWeightKgs(parseBigDecimal(safeGet(row, 5), "单件毛重"));
        product.setDeclareQuantity(parseInteger(safeGet(row, 6), "申报数量"));
        product.setUnitPrice(parseBigDecimal(safeGet(row, 7), "单价"));
        product.setCustomsCode(safeGet(row, 8));
        product.setChineseProductName(safeGet(row, 9));
        product.setEnglishProductName(safeGet(row, 10));
        product.setProductMaterial(safeGet(row, 11));
        product.setProductUsage(safeGet(row, 12));
        product.setBrand(safeGet(row, 13));
        product.setSpecificationModel(safeGet(row, 14));
        String imageVal = safeGet(row, 15);
        if (StringUtils.isNotBlank(imageVal)) {
            if (imageVal.startsWith("http")) {
                product.setProductImage(imageVal);
            } else if (imageVal.startsWith("=DISPIMG")) {
                String imageId = extractImageId(imageVal);
                byte[] imgBytes = imageCache.get(imageId);
                if (imgBytes != null) {
                    try {
                        ShipmentHelper helper = new ShipmentHelper();
                        MultipartFile imgFile = new CustomMultipartFile(imageId + ".png", imageId + ".png", "image/png", imgBytes);
                        Map<String, Object> uploadResult = helper.uploadFile(imgFile);
                        product.setProductImage((String) uploadResult.get("url"));
                    } catch (Exception e) {
                        System.err.println("图片上传失败: " + imageId + ", 错误: " + e.getMessage());
                    }
                } else {
                    System.err.println("未找到图片数据: " + imageId);
                }
            } else {
                try {
                    ShipmentHelper helper = new ShipmentHelper();
                    Map<String, Object> uploadResult = helper.uploadFile(imageVal);
                    product.setProductImage((String) uploadResult.get("url"));
                } catch (Exception e) {
                    System.err.println("图片上传失败: " + imageVal);
                }
            }
        }
        validateProductFields(product, sku);
        products.add(product);
    }
    if (products.isEmpty()) {
        throw new IllegalArgumentException("未能解析到有效产品数据");
    }
    return products;
}

// ========== 辅助方法 ==========
private static String safeGet(List<String> data, int idx) {
    if (data == null || idx < 0 || idx >= data.size()) return "";
    return StringUtils.trim(data.get(idx));
}

private static BigDecimal parseBigDecimal(String val, String field) {
    if (StringUtils.isBlank(val)) throw new IllegalArgumentException(field + "不能为空");
    try { return new BigDecimal(val); } catch (NumberFormatException e) {
        throw new IllegalArgumentException(field + "格式错误:" + val);
    }
}

private static Integer parseInteger(String val, String field) {
    if (StringUtils.isBlank(val)) throw new IllegalArgumentException(field + "不能为空");
    try { return Integer.parseInt(val); } catch (NumberFormatException e) {
        throw new IllegalArgumentException(field + "格式错误:" + val);
    }
}

private static void validateOrderFields(LogisticsOrderDTO order) {
    List<String> missing = new ArrayList<>();
    if (StringUtils.isBlank(order.getLogisticsChannel())) missing.add("物流渠道");
    if (StringUtils.isBlank(order.getWarehouseCode())) missing.add("仓库代码");
    if (ShipmentHelper.isPostalCode(order.getWarehouseCode())) {
        if (StringUtils.isBlank(order.getRecipientCompany())) missing.add("收件人公司");
        if (StringUtils.isBlank(order.getRecipientName())) missing.add("收件人姓名");
        if (StringUtils.isBlank(order.getRecipientAddress())) missing.add("收件人地址");
        if (StringUtils.isBlank(order.getRecipientCity())) missing.add("收件人城市");
        if (StringUtils.isBlank(order.getRecipientProvince())) missing.add("收件人省份");
        if (StringUtils.isBlank(order.getRecipientPostalCode())) missing.add("收件人邮编");
        if (StringUtils.isBlank(order.getCountryRegion())) missing.add("Country国家/区域");
        if (StringUtils.isBlank(order.getRecipientPhone())) missing.add("收件人电话");
    }
    if (!missing.isEmpty()) {
        throw new IllegalArgumentException("订单必填字段缺失:" + String.join("、", missing));
    }
}

private static void validateProductFields(P0ProductInfoDTO product, String sku) {
    List<String> missing = new ArrayList<>();
    if (product.getLengthCm() == null) missing.add("长度");
    if (product.getWidthCm() == null) missing.add("宽度");
    if (product.getHeightCm() == null) missing.add("高度");
    if (product.getBoxCount() == null) missing.add("箱数");
    if (product.getSingleGrossWeightKgs() == null) missing.add("单件毛重");
    if (product.getDeclareQuantity() == null) missing.add("申报数量");
    if (product.getUnitPrice() == null) missing.add("单价");
    if (StringUtils.isBlank(product.getCustomsCode())) missing.add("海关编码");
    if (StringUtils.isBlank(product.getChineseProductName())) missing.add("中文品名");
    if (StringUtils.isBlank(product.getEnglishProductName())) missing.add("英文品名");
    if (StringUtils.isBlank(product.getProductMaterial())) missing.add("产品材质");
    if (StringUtils.isBlank(product.getProductUsage())) missing.add("产品用途");
    if (StringUtils.isBlank(product.getBrand())) missing.add("品牌");
    if (StringUtils.isBlank(product.getSpecificationModel())) missing.add("规格/型号");
    if (!missing.isEmpty()) {
        throw new IllegalArgumentException("SKU【" + sku + "】字段缺失:" + String.join("、", missing));
    }
}

// ========== 内部类 ==========
private static class RowGroup {
    final int startRow;
    final int endRow;
    final String boxNo;
    final String mergeSku;
    final Integer boxCount;

    RowGroup(int start, int end, int seq, Integer count) {
        this.startRow = start;
        this.endRow = end;
        this.boxNo = "BOX_" + seq;
        this.mergeSku = "MERGE_" + this.boxNo;
        this.boxCount = count;
    }
}

private static class MergeArea {
    final int firstExcelRow;
    final int lastExcelRow;
    final int firstCol;
    final int lastCol;
    final String boxNo;
    final String mergeSku;
    final Integer boxCount;
    final int firstDataRow;

    MergeArea(int firstExcelRow, int lastExcelRow, int firstCol, int lastCol,
              String boxNo, String mergeSku, Integer boxCount, int firstDataRow) {
        this.firstExcelRow = firstExcelRow;
        this.lastExcelRow = lastExcelRow;
        this.firstCol = firstCol;
        this.lastCol = lastCol;
        this.boxNo = boxNo;
        this.mergeSku = mergeSku;
        this.boxCount = boxCount;
        this.firstDataRow = firstDataRow;
    }
}

private static class SheetParseResult {
    final List<List<String>> rawData;
    final Map<Integer, MergeArea> areaMap;

    SheetParseResult(List<List<String>> data, Map<Integer, MergeArea> map) {
        this.rawData = data;
        this.areaMap = map;
    }
}

二、Maven依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>easyexcel</artifactId>
    <version>1.1.2-beta5</version>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>4.1.2</version>
</dependency>

三、相关导入

import com.ship.system.domain.p0Shipment.dto.LogisticsOrderDTO;
import com.ship.system.domain.p0Shipment.dto.P0ProductInfoDTO;
import com.ship.system.service.impl.p0Shipment.helper.CustomMultipartFile;
import com.ship.system.service.impl.p0Shipment.helper.ShipmentHelper;
import org.apache.commons.lang3.StringUtils;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigDecimal;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

通过 XSSFWorkbook 可操作 Excel 的 Sheet 和单元格内容,支持合并单元格和图片解析。

四、模板说明

Excel模板示例图片如下:

相关文章

Linux crontab 详解

1) crontab 是什么cron 是 Linux 的定时任务守护进程;crontab 是用来编辑/查看“按时间周期执行命令”的表(cron table)。常见两类:用户 crontab:每个用户一份(crontab -e 编辑)系统级 crontab / cron.d:可指定执行用户(/etc/crontab、/etc/cron.d/*)2) crontab 时间...

富文本里可以允许的 HTML 属性

一、所有标签默认允许的安全属性(极少)class        (可选)id           (通常建议禁用)title️ 注意:id 容易被滥用做锚点注入,很多系统直接禁用class 允许的话最好只允许固定前缀(如 editor-*)二、a 标签允许属性<a href="" t...

Mac 安装 Node.js 指南

方法一:通过官网安装包(最简单,适合初学者)如果你只是想快速安装并开始使用,这是最直接的方法。访问 Node.js 官网。页面会显示两个版本:LTS (Recommended For Most Users):长期支持版,最稳定。建议选这个。Current:最新特性版,包含最新功能但可能不够稳定。下载 .pkg 安装包并运行。按照安装向导点击“下一步”即可完成。方法二:使用 Homebrew 安装(...

Dom\HTML_NO_DEFAULT_NS 的副作用:自动加闭合标签

在使用Dom\HTMLDocument时,Dom\HTML_NO_DEFAULT_NS 将禁止在解析过程中设置元素的命名空间, 此设置是为了与DOMDocument向后兼容而存在的。当使用它时,已知的一个副作用就是:自动加闭合标签例如 </img> 为什么会这样?当你使用:Dom\HTML_NO_DEFAULT_NS文档会变成 无命名空间模式,此时内部更接近 XML...

Laravel 事件和监听器创建

在 Laravel 中,使用 Artisan 命令创建 Events(事件) 和 Listeners(监听器) 是非常高效的。你可以通过以下几种方式来实现:1. 手动创建单个 Event如果你只想创建一个事件类,可以使用 make:event 命令:Bashphp artisan make:event UserRegistered执行后,文件将生成在 app/Even...

自定义域名解析神器 dnsmasq

什么是 dnsmasq?dnsmasq 是一个轻量级、功能强大的网络服务工具,专为小型和中等规模网络设计。它是一个综合的网络基础设施解决方案[1]。dnsmasq 能做什么?功能说明应用场景DNS 转发与缓存将 DNS 查询转发到上游服务器(ISP、Google DNS 等),并在本地缓存结果加快 DNS 查询速度,减少外部 DNS 流量本地 DNS解析本地网络设备的主机名,无需编辑&n...

发表评论

访客

◎欢迎参与讨论,请在这里发表您的看法和观点。