feat: 检验模块新增纸质单据导出模板(原材料/出厂/过程检验),过程检验新增技术参数等字段并改造判定提交逻辑
已添加4个文件
已修改3个文件
1272 ■■■■■ 文件已修改
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/utils/FinalInspectExportUtil.java 384 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/utils/ProcessInspectExportUtil.java 381 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/utils/RawMaterialInspectExportUtil.java 372 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/quality/QualityInspectMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/static/junge-logo.png 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
@@ -96,6 +96,18 @@
    private String model;
    /**
     * æè´¨
     */
    @Excel(name = "材质")
    private String material;
    /**
     * éªŒæ”¶ä¾æ®
     */
    @Excel(name = "验收依据")
    private String acceptanceBasis;
    /**
     * å•位
     */
    @Excel(name = "单位")
@@ -107,6 +119,71 @@
    @Excel(name = "总数量")
    private BigDecimal quantity;
    /**
     * æŠ½æ£€æ•°
     */
    @Excel(name = "抽检数")
    private BigDecimal samplingCount;
    /**
     * å›¾çº¸å…³é”®éƒ¨ä½å°ºå¯¸
     */
    @Excel(name = "图纸关键部位尺寸")
    private String drawingKeyDimensions;
    /**
     * å®žæµ‹å…³é”®éƒ¨ä½å°ºå¯¸
     */
    @Excel(name = "实测关键部位尺寸")
    private String measuredKeyDimensions;
    /**
     * ä¸»è¦æŠ€æœ¯å‚æ•°(过程检验)
     */
    @Excel(name = "主要技术参数")
    private String mainTechnicalParams;
    /**
     * å®žé™…技术参数(过程检验)
     */
    @Excel(name = "实际技术参数")
    private String actualTechnicalParams;
    /**
     * è¯´æ˜Žä¹¦(过程检验)
     */
    @Excel(name = "说明书")
    private String specificationManual;
    /**
     * å…¥åº“编号
     */
    @Excel(name = "入库编号")
    private String warehouseCode;
    /**
     * æ‰€å±žè®¾å¤‡åç§°
     */
    @Excel(name = "所属设备名称")
    private String equipmentName;
    /**
     * é¡¹ç›®åç§°
     */
    @Excel(name = "项目名称")
    private String projectName;
    /**
     * åˆ¤å®š(合格/不合格)
     */
    @Excel(name = "判定")
    private String verdict;
    /**
     * å¼‚常备注(原材料检验)
     */
    private String abnormalRemark;
    @Excel(name = "合格数量")
    @TableField("qualified_quantity")
    private BigDecimal qualifiedQuantity;
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -21,6 +21,9 @@
import com.ruoyi.procurementrecord.service.ProcurementRecordService;
import com.ruoyi.procurementrecord.utils.StockUtils;
import com.ruoyi.quality.dto.QualityInspectDto;
import com.ruoyi.quality.utils.FinalInspectExportUtil;
import com.ruoyi.quality.utils.ProcessInspectExportUtil;
import com.ruoyi.quality.utils.RawMaterialInspectExportUtil;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.mapper.QualityTestStandardMapper;
import com.ruoyi.quality.mapper.QualityUnqualifiedMapper;
@@ -82,12 +85,15 @@
    public int add(QualityInspectDto qualityInspectDto) {
        QualityInspect qualityInspect = new QualityInspect();
        BeanUtils.copyProperties(qualityInspectDto, qualityInspect);
        qualityInspect.setId(null);//强制走数据库自增,避免前端误传id导致主键冲突
        qualityInspect.setInspectState(0);//默认未提交
        qualityInspectMapper.insert(qualityInspect);
        if (ObjectUtils.isNotEmpty(qualityInspectDto.getQualityInspectParams())) {
        for (QualityInspectParam qualityInspectParam : qualityInspectDto.getQualityInspectParams()) {
            qualityInspectParam.setInspectId(qualityInspect.getId());
        }
        qualityInspectParamService.saveBatch(qualityInspectDto.getQualityInspectParams());
        }
        // ä¿å­˜é™„ä»¶
        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.QUALITY_INSPECT, qualityInspect.getId(), qualityInspectDto.getStorageBlobDTOs());
        return 0;
@@ -110,9 +116,36 @@
    public int submit(QualityInspect inspect) {
        QualityInspect qualityInspect = qualityInspectMapper.selectById(inspect.getId());
        if (Integer.valueOf(1).equals(qualityInspect.getInspectState())) {
            throw new ServiceException("该记录已提交,不能重复提交");
        }
        // åŽŸææ–™æ£€éªŒ/过程检验/出厂检验:表单不再填写合格/不合格数量,改用「判定」字段
        boolean isVerdictInspect = qualityInspect.getInspectType() != null
                && (qualityInspect.getInspectType() == 0 || qualityInspect.getInspectType() == 1 || qualityInspect.getInspectType() == 2);
        //提交前必须判断是否合格
        if (ObjectUtils.isNull(qualityInspect.getCheckResult())) {
            if (isVerdictInspect) {
                // æ”¹ç”¨ã€Œåˆ¤å®šã€å­—段
                if (ObjectUtils.isNull(qualityInspect.getVerdict())) {
                    throw new ServiceException("请先判定是否合格");
                }
            } else {
            throw new ServiceException("请先判断是否合格");
            }
        }
        // ç”±ã€Œåˆ¤å®šã€æŽ¨å¯¼åˆæ ¼/不合格数量
        if (isVerdictInspect) {
            BigDecimal total = qualityInspect.getQuantity() == null ? BigDecimal.ZERO : qualityInspect.getQuantity();
            if ("合格".equals(qualityInspect.getVerdict())) {
                qualityInspect.setQualifiedQuantity(total);
                qualityInspect.setUnqualifiedQuantity(BigDecimal.ZERO);
            } else if ("不合格".equals(qualityInspect.getVerdict())) {
                qualityInspect.setQualifiedQuantity(BigDecimal.ZERO);
                qualityInspect.setUnqualifiedQuantity(total);
            }
        }
        if (ObjectUtils.isNull(qualityInspect.getQualifiedQuantity())) {
@@ -166,7 +199,10 @@
            qualityUnqualified.setProductModelId(qualityInspect.getProductModelId());
            List<QualityInspectParam> inspectParams = qualityInspectParamService.list(Wrappers.<QualityInspectParam>lambdaQuery().eq(QualityInspectParam::getInspectId, inspect.getId()));
            String text = inspectParams.stream().map(QualityInspectParam::getParameterItem).collect(Collectors.joining(","));
            qualityUnqualified.setDefectivePhenomena(text + "这些指标中存在不合格");//不合格现象
            if (text == null || text.isEmpty()) {
                text = qualityInspect.getAbnormalRemark();
            }
            qualityUnqualified.setDefectivePhenomena((text == null || text.isEmpty()) ? "检验不合格" : text + "这些指标中存在不合格");//不合格现象
            qualityUnqualified.setInspectId(qualityInspect.getId());
            qualityUnqualifiedMapper.insert(qualityUnqualified);
@@ -208,6 +244,9 @@
            return R.ok("检验单已提交");
        }
        if (ObjectUtils.isNull(qualityInspect.getVerdict())) {
            qualityInspect.setVerdict("合格");
        }
        if (ObjectUtils.isNull(qualityInspect.getCheckResult())) {
            qualityInspect.setCheckResult("合格");
        }
@@ -336,6 +375,21 @@
    @Override
    public void qualityInspectExport(HttpServletResponse response, QualityInspect qualityInspect) {
        List<QualityInspect> qualityInspects = qualityInspectMapper.qualityInspectExport(qualityInspect);
        // åŽŸææ–™æ£€éªŒï¼šæŒ‰çº¸è´¨å•æ®æ ·å¼å¯¼å‡º
        if (qualityInspect.getInspectType() != null && qualityInspect.getInspectType() == 0) {
            RawMaterialInspectExportUtil.export(response, qualityInspects);
            return;
        }
        // è¿‡ç¨‹æ£€éªŒï¼šæŒ‰çº¸è´¨å•据样式导出
        if (qualityInspect.getInspectType() != null && qualityInspect.getInspectType() == 1) {
            ProcessInspectExportUtil.export(response, qualityInspects);
            return;
        }
        // å‡ºåŽ‚æ£€éªŒï¼šæŒ‰çº¸è´¨å•æ®æ ·å¼å¯¼å‡º
        if (qualityInspect.getInspectType() != null && qualityInspect.getInspectType() == 2) {
            FinalInspectExportUtil.export(response, qualityInspects);
            return;
        }
        ExcelUtil<QualityInspect> util = new ExcelUtil<QualityInspect>(QualityInspect.class);
        switch (qualityInspect.getInspectType()) {
            case 0:
src/main/java/com/ruoyi/quality/utils/FinalInspectExportUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,384 @@
package com.ruoyi.quality.utils;
import com.ruoyi.quality.pojo.QualityInspect;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.ClientAnchor;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.PrintSetup;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.RegionUtil;
import org.apache.poi.util.Units;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFDrawing;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.List;
/**
 * å‡ºåŽ‚æ£€éªŒå¯¼å‡ºï¼ˆæŒ‰çº¸è´¨å•æ®æ ·å¼ç”Ÿæˆ Excel)
 */
public class FinalInspectExportUtil {
    // ==================== å¯å®šåˆ¶åŒºåŸŸï¼ˆæ”¹è¿™é‡Œå³å¯è°ƒæ•´æ¨¡æ¿ï¼‰ ====================
    /** LOGO å›¾ç‰‡ä¸Žå•元格边框之间的留白(像素) */
    private static final float LOGO_PADDING_PX = 6f;
    /** å·¦ä¸Šè§’ LOGO å›¾ç‰‡ï¼ˆclasspath è·¯å¾„,放在 resources/static ä¸‹ï¼‰ */
    private static final String LOGO_IMAGE_PATH = "/static/junge-logo.png";
    /** æœ¬åœ°å¼€å‘环境 LOGO ç»å¯¹è·¯å¾„(优先读取) */
    private static final String LOGO_ABS_PATH = "D:/develop/XingRuan/product-inventory-management-junge/product-inventory-management/multiple/assets/logo/JunGeJiTuan-logo.png";
    /** å±…中大标题 */
    private static final String TITLE = "出厂检验记录表";
    /** å›ºå®šæ•°æ®è¡Œæ•°ï¼ˆæ•°æ®ä¸è¶³è¡¥ç©ºè¡Œï¼Œè¶…过自动追加) */
    private static final int FIXED_ROW_COUNT = 14;
    /** è¡¨å¤´åˆ—(从左到右) */
    private static final String[] HEADERS = {
            "序号", "名称", "图号", "材质", "检验依据", "数量", "抽检数",
            "图纸关键部位尺寸", "实测关键部位尺寸", "检验日期", "入库编号", "所属设备名称", "项目名字", "判定"
    };
    /** åˆ—宽自适应:每列在内容宽度基础上额外加的宽度(约等于一个汉字宽度) */
    private static final int COL_WIDTH_PADDING = 2;
    /** å•列最大宽度(约等于汉字个数),防止个别超长内容把列撑得过宽 */
    private static final int COL_WIDTH_MAX = 40;
    /** æ­£æ–‡/标题字号 */
    private static final float BODY_FONT_SIZE = 12f;
    private static final float TITLE_FONT_SIZE = 16f;
    /** æ˜¯å¦æ¨ªå‘打印 */
    private static final boolean LANDSCAPE = true;
    // ========================================================================
    public static void export(HttpServletResponse response, List<QualityInspect> list) {
        try (XSSFWorkbook workbook = new XSSFWorkbook()) {
            Sheet sheet = workbook.createSheet(TITLE);
            int colCount = HEADERS.length;
            // æ ·å¼
            CellStyle titleStyle = createStyle(workbook, true, TITLE_FONT_SIZE, HorizontalAlignment.CENTER);
            CellStyle labelStyle = createStyle(workbook, false, BODY_FONT_SIZE, HorizontalAlignment.LEFT);
            CellStyle headerStyle = createStyle(workbook, true, BODY_FONT_SIZE, HorizontalAlignment.CENTER);
            CellStyle centerStyle = createStyle(workbook, false, BODY_FONT_SIZE, HorizontalAlignment.CENTER);
            CellStyle leftStyle = createStyle(workbook, false, BODY_FONT_SIZE, HorizontalAlignment.LEFT);
            leftStyle.setWrapText(true);
            // æ•°æ®åŒºæ ·å¼åŠ è¾¹æ¡†
            addThinBorder(headerStyle);
            addThinBorder(centerStyle);
            addThinBorder(leftStyle);
            // ---- ç¬¬1~2行:页眉 ----
            Row row1 = sheet.createRow(0);
            Row row2 = sheet.createRow(1);
            row1.setHeightInPoints(45);
            row2.setHeightInPoints(20);
            // å·¦ä¸Šï¼šå…¬å¸ LOGO å›¾ç‰‡ï¼ˆA1:C2 åˆå¹¶å•元格)
            CellRangeAddress logoRegion = new CellRangeAddress(0, 1, 0, 2);
            sheet.addMergedRegion(logoRegion);
            Cell logoCell = row1.createCell(0);
            logoCell.setCellStyle(createStyle(workbook, false, 12f, HorizontalAlignment.CENTER));
            applyRegionBorder(sheet, logoRegion);
            // å±…中大标题(D~K)
            Cell titleCell = row1.createCell(3);
            titleCell.setCellValue(TITLE);
            titleCell.setCellStyle(titleStyle);
            CellRangeAddress titleRegion = new CellRangeAddress(0, 1, 3, colCount - 4);
            sheet.addMergedRegion(titleRegion);
            applyRegionBorder(sheet, titleRegion);
            // å³ä¸Šï¼šæ–‡ä»¶ç¼–号 / é¡µæ•°ï¼ˆé å·¦ï¼‰
            Cell fileNoCell = row1.createCell(colCount - 3);
            fileNoCell.setCellValue("文件编号:");
            fileNoCell.setCellStyle(labelStyle);
            CellRangeAddress fileNoRegion = new CellRangeAddress(0, 0, colCount - 3, colCount - 1);
            sheet.addMergedRegion(fileNoRegion);
            applyRegionBorder(sheet, fileNoRegion);
            Cell pageCell = row2.createCell(colCount - 3);
            pageCell.setCellValue("页数 / No.:");
            pageCell.setCellStyle(labelStyle);
            CellRangeAddress pageRegion = new CellRangeAddress(1, 1, colCount - 3, colCount - 1);
            sheet.addMergedRegion(pageRegion);
            applyRegionBorder(sheet, pageRegion);
            // ---- ç¬¬3行:表头 ----
            int headerRowIndex = 2;
            Row headerRow = sheet.createRow(headerRowIndex);
            headerRow.setHeightInPoints(24);
            for (int i = 0; i < colCount; i++) {
                Cell cell = headerRow.createCell(i);
                cell.setCellValue(HEADERS[i]);
                cell.setCellStyle(headerStyle);
            }
            // ---- æ•°æ®è¡Œï¼šå›ºå®šè¡Œæ•°ï¼Œä¸è¶³è¡¥ç©ºè¡Œ ----
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            int dataRows = Math.max(FIXED_ROW_COUNT, list.size());
            for (int r = 0; r < dataRows; r++) {
                Row dataRow = sheet.createRow(headerRowIndex + 1 + r);
                for (int c = 0; c < colCount; c++) {
                    Cell cell = dataRow.createCell(c);
                    // æ–‡æœ¬ç±»åˆ—左对齐,其余居中(序号/数量/抽检数/检验日期/判定居中)
                    boolean isTextColumn = c == 1 || c == 2 || c == 3 || c == 4 || c == 7 || c == 8
                            || c == 10 || c == 11 || c == 12;
                    cell.setCellStyle(isTextColumn ? leftStyle : centerStyle);
                }
                // åºå·å›ºå®šç¼–号 1~N
                dataRow.getCell(0).setCellValue(r + 1);
                if (r < list.size()) {
                    QualityInspect item = list.get(r);
                    setText(dataRow.getCell(1), item.getProductName());                    // åç§°
                    setText(dataRow.getCell(2), item.getModel());                          // å›¾å·
                    setText(dataRow.getCell(3), item.getMaterial());                       // æè´¨
                    setText(dataRow.getCell(4), item.getAcceptanceBasis());                // æ£€éªŒä¾æ®
                    setText(dataRow.getCell(5), plain(item.getQuantity()));                // æ•°é‡
                    setText(dataRow.getCell(6), plain(item.getSamplingCount()));           // æŠ½æ£€æ•°
                    setText(dataRow.getCell(7), item.getDrawingKeyDimensions());           // å›¾çº¸å…³é”®éƒ¨ä½å°ºå¯¸
                    setText(dataRow.getCell(8), item.getMeasuredKeyDimensions());          // å®žæµ‹å…³é”®éƒ¨ä½å°ºå¯¸
                    setText(dataRow.getCell(9), item.getCheckTime() == null ? "" : sdf.format(item.getCheckTime())); // æ£€éªŒæ—¥æœŸ
                    setText(dataRow.getCell(10), item.getWarehouseCode());                 // å…¥åº“编号
                    setText(dataRow.getCell(11), item.getEquipmentName());                 // æ‰€å±žè®¾å¤‡åç§°
                    setText(dataRow.getCell(12), item.getProjectName());                   // é¡¹ç›®åå­—
                    setText(dataRow.getCell(13), item.getVerdict());                       // åˆ¤å®š
                }
                // æ ¹æ®æ–‡æœ¬åˆ—换行行数估算行高
                dataRow.setHeightInPoints(estimateRowHeight(dataRow, colCount));
            }
            // åˆ—宽按内容自适应
            autoSizeColumns(sheet, colCount, headerRowIndex, dataRows);
            // åˆ—宽确定后再嵌入 LOGO
            addLogoImage(workbook, sheet);
            // ---- é¡µé¢è®¾ç½®ï¼šA4 ----
            PrintSetup ps = sheet.getPrintSetup();
            ps.setPaperSize(PrintSetup.A4_PAPERSIZE);
            ps.setLandscape(LANDSCAPE);
            sheet.setMargin(Sheet.TopMargin, 0.4);
            sheet.setMargin(Sheet.BottomMargin, 0.4);
            sheet.setMargin(Sheet.LeftMargin, 0.3);
            sheet.setMargin(Sheet.RightMargin, 0.3);
            sheet.setFitToPage(true);
            ps.setFitWidth((short) 1);
            ps.setFitHeight((short) 0);
            // ---- è¾“出 ----
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            String fileName = URLEncoder.encode(TITLE, "UTF-8");
            response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
            OutputStream os = response.getOutputStream();
            workbook.write(os);
            os.flush();
        } catch (Exception e) {
            throw new RuntimeException("导出失败", e);
        }
    }
    /** åœ¨ A1:C2 åˆå¹¶åŒºåµŒå…¥å…¬å¸ LOGO å›¾ç‰‡ */
    private static void addLogoImage(XSSFWorkbook workbook, Sheet sheet) {
        byte[] bytes = loadLogoBytes();
        if (bytes == null) {
            System.err.println("[FinalInspectExport] LOGO å›¾ç‰‡æœªæ‰¾åˆ°ï¼Œå¯¼å‡ºå°†ä¸å¸¦ LOGO(资源路径: " + LOGO_IMAGE_PATH + ")");
            return;
        }
        try {
            int pictureIdx = workbook.addPicture(bytes, XSSFWorkbook.PICTURE_TYPE_PNG);
            XSSFDrawing drawing = (XSSFDrawing) sheet.createDrawingPatriarch();
            // å›¾ç‰‡å®šä½ï¼šA1:C1 åŒºåŸŸå†…
            float colAPx = sheet.getColumnWidthInPixels(0);
            float colBPx = sheet.getColumnWidthInPixels(1);
            float colCPx = sheet.getColumnWidthInPixels(2);
            float blockWidthPx = colAPx + colBPx + colCPx;
            float row0Px = sheet.getRow(0).getHeightInPoints() * 96f / 72f;
            float row1Px = sheet.getRow(1).getHeightInPoints() * 96f / 72f;
            float cellHPx = row0Px + row1Px;
            // è¯»å–图片真实宽高
            int imgW = 236;
            int imgH = 190;
            if (bytes.length > 24) {
                imgW = readInt(bytes, 16);
                imgH = readInt(bytes, 20);
            }
            // æŒ‰å•元格大小等比缩放(四周留白),整体居中
            float availW = blockWidthPx - LOGO_PADDING_PX * 2;
            float availH = cellHPx - LOGO_PADDING_PX * 2;
            float scale = Math.min(availW / imgW, availH / imgH);
            float dispW = imgW * scale;
            float dispH = imgH * scale;
            float absLeft = LOGO_PADDING_PX + (availW - dispW) / 2f;
            float absTop = LOGO_PADDING_PX + (availH - dispH) / 2f;
            float absRight = absLeft + dispW;
            float absBottom = absTop + dispH;
            // è®¡ç®— from/to é”šç‚¹ï¼šæ‰€æœ‰åç§»é‡ä¿æŒåœ¨æ‰€åœ¨åˆ—/行的合法范围内
            int fromCol;
            int fromOffPx;
            if (absLeft <= colAPx) {
                fromCol = 0;
                fromOffPx = (int) absLeft;
            } else if (absLeft <= colAPx + colBPx) {
                fromCol = 1;
                fromOffPx = (int) (absLeft - colAPx);
            } else {
                fromCol = 2;
                fromOffPx = (int) (absLeft - colAPx - colBPx);
            }
            int toCol;
            int toOffPx;
            if (absRight <= colAPx) {
                toCol = 0;
                toOffPx = (int) absRight;
            } else if (absRight <= colAPx + colBPx) {
                toCol = 1;
                toOffPx = (int) (absRight - colAPx);
            } else {
                toCol = 2;
                toOffPx = (int) (absRight - colAPx - colBPx);
            }
            int fromRow = 0;
            float fromRowOff = absTop;
            if (absTop > row0Px) {
                fromRow = 1;
                fromRowOff = absTop - row0Px;
            }
            int toRow = 0;
            float toRowOff = absBottom;
            if (absBottom > row0Px) {
                toRow = 1;
                toRowOff = absBottom - row0Px;
            }
            XSSFClientAnchor anchor = new XSSFClientAnchor(
                    fromOffPx * Units.EMU_PER_PIXEL, (int) (fromRowOff * Units.EMU_PER_PIXEL),
                    toOffPx * Units.EMU_PER_PIXEL, (int) (toRowOff * Units.EMU_PER_PIXEL),
                    fromCol, fromRow, toCol, toRow);
            anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
            drawing.createPicture(anchor, pictureIdx);
        } catch (Exception e) {
            // LOGO åŠ è½½å¤±è´¥ä¸å½±å“å¯¼å‡ºï¼Œä½†æ‰“å°é”™è¯¯ä¾¿äºŽæŽ’æŸ¥
            System.err.println("[FinalInspectExport] LOGO åµŒå…¥å¤±è´¥: " + e.getMessage());
        }
    }
    /** è¯»å– LOGO:本地开发优先读绝对路径,其次 classpath,最后项目相对路径 */
    private static byte[] loadLogoBytes() {
        try (InputStream is = new java.io.FileInputStream(LOGO_ABS_PATH)) {
            return is.readAllBytes();
        } catch (Exception ignored) {
        }
        try (InputStream is = FinalInspectExportUtil.class.getResourceAsStream(LOGO_IMAGE_PATH)) {
            if (is != null) {
                return is.readAllBytes();
            }
        } catch (Exception ignored) {
        }
        try (InputStream is = new java.io.FileInputStream("src/main/resources" + LOGO_IMAGE_PATH)) {
            return is.readAllBytes();
        } catch (Exception e) {
            return null;
        }
    }
    /** è¯»å– PNG å¤´éƒ¨çš„宽/高(大端整数) */
    private static int readInt(byte[] b, int off) {
        return ((b[off] & 0xFF) << 24) | ((b[off + 1] & 0xFF) << 16) | ((b[off + 2] & 0xFF) << 8) | (b[off + 3] & 0xFF);
    }
    private static CellStyle createStyle(XSSFWorkbook workbook, boolean bold, float fontSize,
                                         HorizontalAlignment align) {
        CellStyle style = workbook.createCellStyle();
        Font font = workbook.createFont();
        font.setFontName("宋体");
        font.setBold(bold);
        font.setFontHeightInPoints((short) fontSize);
        style.setFont(font);
        style.setAlignment(align);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        return style;
    }
    private static void addThinBorder(CellStyle style) {
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
    }
    private static void applyRegionBorder(Sheet sheet, CellRangeAddress region) {
        RegionUtil.setBorderTop(BorderStyle.THIN, region, sheet);
        RegionUtil.setBorderBottom(BorderStyle.THIN, region, sheet);
        RegionUtil.setBorderLeft(BorderStyle.THIN, region, sheet);
        RegionUtil.setBorderRight(BorderStyle.THIN, region, sheet);
    }
    private static void setText(Cell cell, String value) {
        cell.setCellValue(value == null ? "" : value);
    }
    private static String plain(BigDecimal value) {
        return value == null ? "" : value.toPlainString();
    }
    /** æŒ‰æ–‡æœ¬åˆ—内容与列宽估算换行后的行高(点数) */
    private static float estimateRowHeight(Row row, int colCount) {
        int maxLines = 1;
        for (int c = 1; c < colCount; c++) {
            boolean isTextColumn = c == 1 || c == 2 || c == 3 || c == 4 || c == 7 || c == 8
                    || c == 10 || c == 11 || c == 12;
            if (!isTextColumn) {
                continue;
            }
            Cell cell = row.getCell(c);
            String value = cell == null ? null : cell.getStringCellValue();
            if (value == null || value.isEmpty()) {
                continue;
            }
            int lines = (int) Math.ceil(displayLen(value) / (COL_WIDTH_MAX * 2d));
            maxLines = Math.max(maxLines, lines);
        }
        return Math.max(22f, maxLines * 15f + 6f);
    }
    /** åˆ—宽自适应:按表头与数据内容计算每列宽度 */
    private static void autoSizeColumns(Sheet sheet, int colCount, int headerRowIndex, int dataRows) {
        for (int c = 0; c < colCount; c++) {
            double maxLen = displayLen(HEADERS[c]);
            for (int r = 0; r < dataRows; r++) {
                Row row = sheet.getRow(headerRowIndex + 1 + r);
                Cell cell = row == null ? null : row.getCell(c);
                if (cell != null && cell.getCellType() == CellType.STRING) {
                    maxLen = Math.max(maxLen, displayLen(cell.getStringCellValue()));
                }
            }
            double widthChars = Math.min(maxLen + COL_WIDTH_PADDING, COL_WIDTH_MAX + COL_WIDTH_PADDING);
            // ä¸­æ–‡æŒ‰ä¸¤ä¸ªåŠè§’字符宽度计算
            sheet.setColumnWidth(c, (int) (widthChars * 256));
        }
    }
    /** è®¡ç®—显示宽度:中文等全角字符按 2 ä¸ªåŠè§’宽度计 */
    private static double displayLen(String s) {
        if (s == null) {
            return 0;
        }
        double len = 0;
        for (char ch : s.toCharArray()) {
            if ((ch >= 0x2E80 && ch <= 0x9FFF) || (ch >= 0x3000 && ch <= 0x303F) || ch >= 0xFF00) {
                len += 2;
            } else {
                len += 1;
            }
        }
        return len;
    }
}
src/main/java/com/ruoyi/quality/utils/ProcessInspectExportUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,381 @@
package com.ruoyi.quality.utils;
import com.ruoyi.quality.pojo.QualityInspect;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.ClientAnchor;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.PrintSetup;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.RegionUtil;
import org.apache.poi.util.Units;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFDrawing;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.List;
/**
 * è¿‡ç¨‹æ£€éªŒå¯¼å‡ºï¼ˆæŒ‰çº¸è´¨å•据样式生成 Excel)
 */
public class ProcessInspectExportUtil {
    // ==================== å¯å®šåˆ¶åŒºåŸŸï¼ˆæ”¹è¿™é‡Œå³å¯è°ƒæ•´æ¨¡æ¿ï¼‰ ====================
    /** LOGO å›¾ç‰‡ä¸Žå•元格边框之间的留白(像素) */
    private static final float LOGO_PADDING_PX = 6f;
    /** å·¦ä¸Šè§’ LOGO å›¾ç‰‡ï¼ˆclasspath è·¯å¾„,放在 resources/static ä¸‹ï¼‰ */
    private static final String LOGO_IMAGE_PATH = "/static/junge-logo.png";
    /** æœ¬åœ°å¼€å‘环境 LOGO ç»å¯¹è·¯å¾„(优先读取) */
    private static final String LOGO_ABS_PATH = "D:/develop/XingRuan/product-inventory-management-junge/product-inventory-management/multiple/assets/logo/JunGeJiTuan-logo.png";
    /** å±…中大标题 */
    private static final String TITLE = "过程检验记录表";
    /** å›ºå®šæ•°æ®è¡Œæ•°ï¼ˆæ•°æ®ä¸è¶³è¡¥ç©ºè¡Œï¼Œè¶…过自动追加) */
    private static final int FIXED_ROW_COUNT = 14;
    /** è¡¨å¤´åˆ—(从左到右) */
    private static final String[] HEADERS = {
            "序号", "名称", "规格型号", "材质", "验收依据", "数量", "抽检数",
            "主要技术参数", "实际技术参数", "说明书", "检验日期", "入库编号",
            "所属设备名称", "项目名称", "判定"
    };
    /** åˆ—宽自适应:每列在内容宽度基础上额外加的宽度(约等于一个汉字宽度) */
    private static final int COL_WIDTH_PADDING = 2;
    /** å•列最大宽度(约等于汉字个数),防止个别超长内容把列撑得过宽 */
    private static final int COL_WIDTH_MAX = 40;
    /** æ­£æ–‡/标题字号 */
    private static final float BODY_FONT_SIZE = 12f;
    private static final float TITLE_FONT_SIZE = 16f;
    /** æ˜¯å¦æ¨ªå‘打印 */
    private static final boolean LANDSCAPE = true;
    // ========================================================================
    public static void export(HttpServletResponse response, List<QualityInspect> list) {
        try (XSSFWorkbook workbook = new XSSFWorkbook()) {
            Sheet sheet = workbook.createSheet(TITLE);
            int colCount = HEADERS.length;
            // æ ·å¼
            CellStyle titleStyle = createStyle(workbook, true, TITLE_FONT_SIZE, HorizontalAlignment.CENTER);
            CellStyle labelStyle = createStyle(workbook, false, BODY_FONT_SIZE, HorizontalAlignment.LEFT);
            CellStyle headerStyle = createStyle(workbook, true, BODY_FONT_SIZE, HorizontalAlignment.CENTER);
            CellStyle centerStyle = createStyle(workbook, false, BODY_FONT_SIZE, HorizontalAlignment.CENTER);
            centerStyle.setWrapText(true);
            // æ•°æ®åŒºæ ·å¼åŠ è¾¹æ¡†
            addThinBorder(headerStyle);
            addThinBorder(centerStyle);
            // ---- ç¬¬1~2行:页眉 ----
            Row row1 = sheet.createRow(0);
            Row row2 = sheet.createRow(1);
            row1.setHeightInPoints(36);
            row2.setHeightInPoints(16);
            // å·¦ä¸Šï¼šå…¬å¸ LOGO å›¾ç‰‡ï¼ˆA1:C2 åˆå¹¶å•元格)
            CellRangeAddress logoRegion = new CellRangeAddress(0, 1, 0, 2);
            sheet.addMergedRegion(logoRegion);
            Cell logoCell = row1.createCell(0);
            logoCell.setCellStyle(createStyle(workbook, false, 12f, HorizontalAlignment.CENTER));
            applyRegionBorder(sheet, logoRegion);
            // å±…中大标题
            Cell titleCell = row1.createCell(3);
            titleCell.setCellValue(TITLE);
            titleCell.setCellStyle(titleStyle);
            CellRangeAddress titleRegion = new CellRangeAddress(0, 1, 3, colCount - 4);
            sheet.addMergedRegion(titleRegion);
            applyRegionBorder(sheet, titleRegion);
            // å³ä¸Šï¼šæ–‡ä»¶ç¼–号 / é¡µæ•°ï¼ˆé å·¦ï¼‰
            Cell fileNoCell = row1.createCell(colCount - 3);
            fileNoCell.setCellValue("文件编号:");
            fileNoCell.setCellStyle(labelStyle);
            CellRangeAddress fileNoRegion = new CellRangeAddress(0, 0, colCount - 3, colCount - 1);
            sheet.addMergedRegion(fileNoRegion);
            applyRegionBorder(sheet, fileNoRegion);
            Cell pageCell = row2.createCell(colCount - 3);
            pageCell.setCellValue("页数 / No.:");
            pageCell.setCellStyle(labelStyle);
            CellRangeAddress pageRegion = new CellRangeAddress(1, 1, colCount - 3, colCount - 1);
            sheet.addMergedRegion(pageRegion);
            applyRegionBorder(sheet, pageRegion);
            // ---- ç¬¬3行:表头 ----
            int headerRowIndex = 2;
            Row headerRow = sheet.createRow(headerRowIndex);
            headerRow.setHeightInPoints(24);
            for (int i = 0; i < colCount; i++) {
                Cell cell = headerRow.createCell(i);
                cell.setCellValue(HEADERS[i]);
                cell.setCellStyle(headerStyle);
            }
            // ---- æ•°æ®è¡Œï¼šå›ºå®šè¡Œæ•°ï¼Œä¸è¶³è¡¥ç©ºè¡Œï¼Œæ•°æ®å…¨éƒ¨å±…中 ----
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            int dataRows = Math.max(FIXED_ROW_COUNT, list.size());
            for (int r = 0; r < dataRows; r++) {
                Row dataRow = sheet.createRow(headerRowIndex + 1 + r);
                for (int c = 0; c < colCount; c++) {
                    Cell cell = dataRow.createCell(c);
                    cell.setCellStyle(centerStyle);
                }
                // åºå·å›ºå®šç¼–号 1~N
                dataRow.getCell(0).setCellValue(r + 1);
                if (r < list.size()) {
                    QualityInspect item = list.get(r);
                    setText(dataRow.getCell(1), item.getProductName());                 // åç§°
                    setText(dataRow.getCell(2), item.getModel());                       // è§„格型号
                    setText(dataRow.getCell(3), item.getMaterial());                    // æè´¨
                    setText(dataRow.getCell(4), item.getAcceptanceBasis());             // éªŒæ”¶ä¾æ®
                    setText(dataRow.getCell(5), plain(item.getQuantity()));             // æ•°é‡
                    setText(dataRow.getCell(6), plain(item.getSamplingCount()));        // æŠ½æ£€æ•°
                    setText(dataRow.getCell(7), item.getMainTechnicalParams());         // ä¸»è¦æŠ€æœ¯å‚æ•°
                    setText(dataRow.getCell(8), item.getActualTechnicalParams());       // å®žé™…技术参数
                    setText(dataRow.getCell(9), item.getSpecificationManual());         // è¯´æ˜Žä¹¦
                    setText(dataRow.getCell(10), item.getCheckTime() == null ? "" : sdf.format(item.getCheckTime())); // æ£€éªŒæ—¥æœŸ
                    setText(dataRow.getCell(11), item.getWarehouseCode());              // å…¥åº“编号
                    setText(dataRow.getCell(12), item.getEquipmentName());              // æ‰€å±žè®¾å¤‡åç§°
                    setText(dataRow.getCell(13), item.getProjectName());                // é¡¹ç›®åç§°
                    setText(dataRow.getCell(14), item.getVerdict());                    // åˆ¤å®š
                }
                // æ ¹æ®æ–‡æœ¬åˆ—换行行数估算行高
                dataRow.setHeightInPoints(estimateRowHeight(dataRow, colCount));
            }
            // åˆ—宽按内容自适应
            autoSizeColumns(sheet, colCount, headerRowIndex, dataRows);
            // åˆ—宽确定后再嵌入 LOGO
            addLogoImage(workbook, sheet);
            // ---- é¡µé¢è®¾ç½®ï¼šA4 ----
            PrintSetup ps = sheet.getPrintSetup();
            ps.setPaperSize(PrintSetup.A4_PAPERSIZE);
            ps.setLandscape(LANDSCAPE);
            sheet.setMargin(Sheet.TopMargin, 0.4);
            sheet.setMargin(Sheet.BottomMargin, 0.4);
            sheet.setMargin(Sheet.LeftMargin, 0.3);
            sheet.setMargin(Sheet.RightMargin, 0.3);
            sheet.setFitToPage(true);
            ps.setFitWidth((short) 1);
            ps.setFitHeight((short) 0);
            // ---- è¾“出 ----
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            String fileName = URLEncoder.encode(TITLE, "UTF-8");
            response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
            OutputStream os = response.getOutputStream();
            workbook.write(os);
            os.flush();
        } catch (Exception e) {
            throw new RuntimeException("导出失败", e);
        }
    }
    /** åœ¨ A1:C2 åˆå¹¶åŒºåµŒå…¥å…¬å¸ LOGO å›¾ç‰‡ï¼ŒæŒ‰å•元格自适应、整体居中 */
    private static void addLogoImage(XSSFWorkbook workbook, Sheet sheet) {
        byte[] bytes = loadLogoBytes();
        if (bytes == null) {
            System.err.println("[ProcessInspectExport] LOGO å›¾ç‰‡æœªæ‰¾åˆ°ï¼Œå¯¼å‡ºå°†ä¸å¸¦ LOGO(资源路径: " + LOGO_IMAGE_PATH + ")");
            return;
        }
        try {
            int pictureIdx = workbook.addPicture(bytes, XSSFWorkbook.PICTURE_TYPE_PNG);
            XSSFDrawing drawing = (XSSFDrawing) sheet.createDrawingPatriarch();
            // å›¾ç‰‡å®šä½ï¼šA1:C2 åŒºåŸŸå†…
            float colAPx = sheet.getColumnWidthInPixels(0);
            float colBPx = sheet.getColumnWidthInPixels(1);
            float colCPx = sheet.getColumnWidthInPixels(2);
            float blockWidthPx = colAPx + colBPx + colCPx;
            float row0Px = sheet.getRow(0).getHeightInPoints() * 96f / 72f;
            float row1Px = sheet.getRow(1).getHeightInPoints() * 96f / 72f;
            float cellHPx = row0Px + row1Px;
            // è¯»å–图片真实宽高
            int imgW = 236;
            int imgH = 190;
            if (bytes.length > 24) {
                imgW = readInt(bytes, 16);
                imgH = readInt(bytes, 20);
            }
            // æŒ‰å•元格大小等比缩放(四周留白),整体居中
            float availW = blockWidthPx - LOGO_PADDING_PX * 2;
            float availH = cellHPx - LOGO_PADDING_PX * 2;
            float scale = Math.min(availW / imgW, availH / imgH);
            float dispW = imgW * scale;
            float dispH = imgH * scale;
            float absLeft = LOGO_PADDING_PX + (availW - dispW) / 2f;
            float absTop = LOGO_PADDING_PX + (availH - dispH) / 2f;
            float absRight = absLeft + dispW;
            float absBottom = absTop + dispH;
            // è®¡ç®— from/to é”šç‚¹ï¼šæ‰€æœ‰åç§»é‡ä¿æŒåœ¨æ‰€åœ¨åˆ—/行的合法范围内
            int fromCol;
            int fromOffPx;
            if (absLeft <= colAPx) {
                fromCol = 0;
                fromOffPx = (int) absLeft;
            } else if (absLeft <= colAPx + colBPx) {
                fromCol = 1;
                fromOffPx = (int) (absLeft - colAPx);
            } else {
                fromCol = 2;
                fromOffPx = (int) (absLeft - colAPx - colBPx);
            }
            int toCol;
            int toOffPx;
            if (absRight <= colAPx) {
                toCol = 0;
                toOffPx = (int) absRight;
            } else if (absRight <= colAPx + colBPx) {
                toCol = 1;
                toOffPx = (int) (absRight - colAPx);
            } else {
                toCol = 2;
                toOffPx = (int) (absRight - colAPx - colBPx);
            }
            int fromRow = 0;
            float fromRowOff = absTop;
            if (absTop > row0Px) {
                fromRow = 1;
                fromRowOff = absTop - row0Px;
            }
            int toRow = 0;
            float toRowOff = absBottom;
            if (absBottom > row0Px) {
                toRow = 1;
                toRowOff = absBottom - row0Px;
            }
            XSSFClientAnchor anchor = new XSSFClientAnchor(
                    fromOffPx * Units.EMU_PER_PIXEL, (int) (fromRowOff * Units.EMU_PER_PIXEL),
                    toOffPx * Units.EMU_PER_PIXEL, (int) (toRowOff * Units.EMU_PER_PIXEL),
                    fromCol, fromRow, toCol, toRow);
            anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
            drawing.createPicture(anchor, pictureIdx);
        } catch (Exception e) {
            // LOGO åŠ è½½å¤±è´¥ä¸å½±å“å¯¼å‡ºï¼Œä½†æ‰“å°é”™è¯¯ä¾¿äºŽæŽ’æŸ¥
            System.err.println("[ProcessInspectExport] LOGO åµŒå…¥å¤±è´¥: " + e.getMessage());
        }
    }
    /** è¯»å– LOGO:本地开发优先读绝对路径,其次 classpath,最后项目相对路径 */
    private static byte[] loadLogoBytes() {
        try (InputStream is = new java.io.FileInputStream(LOGO_ABS_PATH)) {
            return is.readAllBytes();
        } catch (Exception ignored) {
        }
        try (InputStream is = ProcessInspectExportUtil.class.getResourceAsStream(LOGO_IMAGE_PATH)) {
            if (is != null) {
                return is.readAllBytes();
            }
        } catch (Exception ignored) {
        }
        try (InputStream is = new java.io.FileInputStream("src/main/resources" + LOGO_IMAGE_PATH)) {
            return is.readAllBytes();
        } catch (Exception e) {
            return null;
        }
    }
    /** æŒ‰æ–‡æœ¬åˆ—内容与列宽估算换行后的行高(点数) */
    private static float estimateRowHeight(Row row, int colCount) {
        int maxLines = 1;
        for (int c = 1; c < colCount; c++) {
            boolean isTextColumn = c == 1 || c == 2 || c == 3 || c == 4 || c == 7 || c == 8 || c == 9
                    || c == 11 || c == 12 || c == 13;
            if (!isTextColumn) {
                continue;
            }
            Cell cell = row.getCell(c);
            String value = cell == null ? null : cell.getStringCellValue();
            if (value == null || value.isEmpty()) {
                continue;
            }
            int lines = (int) Math.ceil(displayLen(value) / (COL_WIDTH_MAX * 2d));
            maxLines = Math.max(maxLines, lines);
        }
        return Math.max(22f, maxLines * 15f + 6f);
    }
    /** åˆ—宽自适应:按表头与数据内容计算每列宽度 */
    private static void autoSizeColumns(Sheet sheet, int colCount, int headerRowIndex, int dataRows) {
        for (int c = 0; c < colCount; c++) {
            double maxLen = displayLen(HEADERS[c]);
            for (int r = 0; r < dataRows; r++) {
                Row row = sheet.getRow(headerRowIndex + 1 + r);
                Cell cell = row == null ? null : row.getCell(c);
                if (cell != null && cell.getCellType() == CellType.STRING) {
                    maxLen = Math.max(maxLen, displayLen(cell.getStringCellValue()));
                }
            }
            double widthChars = Math.min(maxLen + COL_WIDTH_PADDING, COL_WIDTH_MAX + COL_WIDTH_PADDING);
            // ä¸­æ–‡æŒ‰ä¸¤ä¸ªåŠè§’字符宽度计算
            sheet.setColumnWidth(c, (int) (widthChars * 256));
        }
    }
    /** è®¡ç®—显示宽度:中文等全角字符按 2 ä¸ªåŠè§’宽度计 */
    private static double displayLen(String s) {
        if (s == null) {
            return 0;
        }
        double len = 0;
        for (char ch : s.toCharArray()) {
            if ((ch >= 0x2E80 && ch <= 0x9FFF) || (ch >= 0x3000 && ch <= 0x303F) || ch >= 0xFF00) {
                len += 2;
            } else {
                len += 1;
            }
        }
        return len;
    }
    /** è¯»å– PNG å¤´éƒ¨çš„宽/高(大端整数) */
    private static int readInt(byte[] b, int off) {
        return ((b[off] & 0xFF) << 24) | ((b[off + 1] & 0xFF) << 16) | ((b[off + 2] & 0xFF) << 8) | (b[off + 3] & 0xFF);
    }
    private static CellStyle createStyle(XSSFWorkbook workbook, boolean bold, float fontSize,
                                         HorizontalAlignment align) {
        CellStyle style = workbook.createCellStyle();
        Font font = workbook.createFont();
        font.setFontName("宋体");
        font.setBold(bold);
        font.setFontHeightInPoints((short) fontSize);
        style.setFont(font);
        style.setAlignment(align);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        return style;
    }
    private static void addThinBorder(CellStyle style) {
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
    }
    private static void applyRegionBorder(Sheet sheet, CellRangeAddress region) {
        RegionUtil.setBorderTop(BorderStyle.THIN, region, sheet);
        RegionUtil.setBorderBottom(BorderStyle.THIN, region, sheet);
        RegionUtil.setBorderLeft(BorderStyle.THIN, region, sheet);
        RegionUtil.setBorderRight(BorderStyle.THIN, region, sheet);
    }
    private static void setText(Cell cell, String value) {
        cell.setCellValue(value == null ? "" : value);
    }
    private static String plain(BigDecimal value) {
        return value == null ? "" : value.toPlainString();
    }
}
src/main/java/com/ruoyi/quality/utils/RawMaterialInspectExportUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,372 @@
package com.ruoyi.quality.utils;
import com.ruoyi.quality.pojo.QualityInspect;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.ClientAnchor;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.PrintSetup;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.RegionUtil;
import org.apache.poi.util.Units;
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
import org.apache.poi.xssf.usermodel.XSSFDrawing;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.List;
/**
 * åŽŸææ–™æ£€éªŒå¯¼å‡ºï¼ˆæŒ‰çº¸è´¨å•æ®æ ·å¼ç”Ÿæˆ Excel)
 */
public class RawMaterialInspectExportUtil {
    // ==================== å¯å®šåˆ¶åŒºåŸŸï¼ˆæ”¹è¿™é‡Œå³å¯è°ƒæ•´æ¨¡æ¿ï¼‰ ====================
    /** LOGO å›¾ç‰‡ä¸Žå•元格边框之间的留白(像素) */
    private static final float LOGO_PADDING_PX = 6f;
    /** å·¦ä¸Šè§’ LOGO å›¾ç‰‡ï¼ˆclasspath è·¯å¾„,放在 resources/static ä¸‹ï¼‰ */
    private static final String LOGO_IMAGE_PATH = "/static/junge-logo.png";
    /** æœ¬åœ°å¼€å‘环境 LOGO ç»å¯¹è·¯å¾„(优先读取) */
    private static final String LOGO_ABS_PATH = "D:/develop/XingRuan/product-inventory-management-junge/product-inventory-management/multiple/assets/logo/JunGeJiTuan-logo.png";
    /** å±…中大标题 */
    private static final String TITLE = "来料件入库检验记录";
    /** å›ºå®šæ•°æ®è¡Œæ•°ï¼ˆæ•°æ®ä¸è¶³è¡¥ç©ºè¡Œï¼Œè¶…过自动追加) */
    private static final int FIXED_ROW_COUNT = 14;
    /** è¡¨å¤´åˆ—(从左到右) */
    private static final String[] HEADERS = {
            "序号", "项目", "物料名称", "检验依据", "数量", "抽检数量", "检验日期", "异常备注", "判定"
    };
    /** åˆ—宽自适应:每列在内容宽度基础上额外加的宽度(约等于一个汉字宽度) */
    private static final int COL_WIDTH_PADDING = 2;
    /** å•列最大宽度(约等于汉字个数),防止个别超长内容把列撑得过宽 */
    private static final int COL_WIDTH_MAX = 40;
    /** æ­£æ–‡/标题字号 */
    private static final float BODY_FONT_SIZE = 12f;
    private static final float TITLE_FONT_SIZE = 16f;
    /** æ˜¯å¦æ¨ªå‘打印 */
    private static final boolean LANDSCAPE = true;
    // ========================================================================
    public static void export(HttpServletResponse response, List<QualityInspect> list) {
        try (XSSFWorkbook workbook = new XSSFWorkbook()) {
            Sheet sheet = workbook.createSheet(TITLE);
            int colCount = HEADERS.length;
            // æ ·å¼
            CellStyle titleStyle = createStyle(workbook, true, TITLE_FONT_SIZE, HorizontalAlignment.CENTER);
            CellStyle labelStyle = createStyle(workbook, false, BODY_FONT_SIZE, HorizontalAlignment.LEFT);
            CellStyle headerStyle = createStyle(workbook, true, BODY_FONT_SIZE, HorizontalAlignment.CENTER);
            CellStyle centerStyle = createStyle(workbook, false, BODY_FONT_SIZE, HorizontalAlignment.CENTER);
            centerStyle.setWrapText(true);
            // æ•°æ®åŒºæ ·å¼åŠ è¾¹æ¡†
            addThinBorder(headerStyle);
            addThinBorder(centerStyle);
            // ---- ç¬¬1~2行:页眉 ----
            Row row1 = sheet.createRow(0);
            Row row2 = sheet.createRow(1);
            row1.setHeightInPoints(36);
            row2.setHeightInPoints(16);
            // å·¦ä¸Šï¼šå…¬å¸ LOGO å›¾ç‰‡ï¼ˆA1:C2 åˆå¹¶å•元格)
            CellRangeAddress logoRegion = new CellRangeAddress(0, 1, 0, 2);
            sheet.addMergedRegion(logoRegion);
            Cell logoCell = row1.createCell(0);
            logoCell.setCellStyle(createStyle(workbook, false, 12f, HorizontalAlignment.CENTER));
            applyRegionBorder(sheet, logoRegion);
            // å±…中大标题(D~F)
            Cell titleCell = row1.createCell(3);
            titleCell.setCellValue(TITLE);
            titleCell.setCellStyle(titleStyle);
            CellRangeAddress titleRegion = new CellRangeAddress(0, 1, 3, colCount - 4);
            sheet.addMergedRegion(titleRegion);
            applyRegionBorder(sheet, titleRegion);
            // å³ä¸Šï¼šæ–‡ä»¶ç¼–号 / é¡µæ•°ï¼ˆé å·¦ï¼‰
            Cell fileNoCell = row1.createCell(colCount - 3);
            fileNoCell.setCellValue("文件编号:");
            fileNoCell.setCellStyle(labelStyle);
            CellRangeAddress fileNoRegion = new CellRangeAddress(0, 0, colCount - 3, colCount - 1);
            sheet.addMergedRegion(fileNoRegion);
            applyRegionBorder(sheet, fileNoRegion);
            Cell pageCell = row2.createCell(colCount - 3);
            pageCell.setCellValue("页数 / No.:");
            pageCell.setCellStyle(labelStyle);
            CellRangeAddress pageRegion = new CellRangeAddress(1, 1, colCount - 3, colCount - 1);
            sheet.addMergedRegion(pageRegion);
            applyRegionBorder(sheet, pageRegion);
            // ---- ç¬¬3行:表头 ----
            int headerRowIndex = 2;
            Row headerRow = sheet.createRow(headerRowIndex);
            headerRow.setHeightInPoints(24);
            for (int i = 0; i < colCount; i++) {
                Cell cell = headerRow.createCell(i);
                cell.setCellValue(HEADERS[i]);
                cell.setCellStyle(headerStyle);
            }
            // ---- æ•°æ®è¡Œï¼šå›ºå®šè¡Œæ•°ï¼Œä¸è¶³è¡¥ç©ºè¡Œ ----
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            int dataRows = Math.max(FIXED_ROW_COUNT, list.size());
            for (int r = 0; r < dataRows; r++) {
                Row dataRow = sheet.createRow(headerRowIndex + 1 + r);
                for (int c = 0; c < colCount; c++) {
                    Cell cell = dataRow.createCell(c);
                    cell.setCellStyle(centerStyle);
                }
                // åºå·å›ºå®šç¼–号 1~N
                dataRow.getCell(0).setCellValue(r + 1);
                if (r < list.size()) {
                    QualityInspect item = list.get(r);
                    setText(dataRow.getCell(1), item.getProjectName());                   // é¡¹ç›®
                    setText(dataRow.getCell(2), item.getProductName());                   // ç‰©æ–™åç§°
                    setText(dataRow.getCell(3), item.getAcceptanceBasis());               // æ£€éªŒä¾æ®
                    setText(dataRow.getCell(4), plain(item.getQuantity()));               // æ•°é‡
                    setText(dataRow.getCell(5), plain(item.getSamplingCount()));          // æŠ½æ£€æ•°é‡
                    setText(dataRow.getCell(6), item.getCheckTime() == null ? "" : sdf.format(item.getCheckTime())); // æ£€éªŒæ—¥æœŸ
                    setText(dataRow.getCell(7), item.getAbnormalRemark());                // å¼‚常备注
                    setText(dataRow.getCell(8), item.getVerdict());                       // åˆ¤å®š
                }
                // æ ¹æ®æ–‡æœ¬åˆ—换行行数估算行高
                dataRow.setHeightInPoints(estimateRowHeight(dataRow, colCount));
            }
            // åˆ—宽按内容自适应
            autoSizeColumns(sheet, colCount, headerRowIndex, dataRows);
            // åˆ—宽确定后再嵌入 LOGO
            addLogoImage(workbook, sheet);
            // ---- é¡µé¢è®¾ç½®ï¼šA4 ----
            PrintSetup ps = sheet.getPrintSetup();
            ps.setPaperSize(PrintSetup.A4_PAPERSIZE);
            ps.setLandscape(LANDSCAPE);
            sheet.setMargin(Sheet.TopMargin, 0.4);
            sheet.setMargin(Sheet.BottomMargin, 0.4);
            sheet.setMargin(Sheet.LeftMargin, 0.3);
            sheet.setMargin(Sheet.RightMargin, 0.3);
            sheet.setFitToPage(true);
            ps.setFitWidth((short) 1);
            ps.setFitHeight((short) 0);
            // ---- è¾“出 ----
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            String fileName = URLEncoder.encode(TITLE, "UTF-8");
            response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
            response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx");
            OutputStream os = response.getOutputStream();
            workbook.write(os);
            os.flush();
        } catch (Exception e) {
            throw new RuntimeException("导出失败", e);
        }
    }
    /** åœ¨ A1:C2 åˆå¹¶åŒºåµŒå…¥å…¬å¸ LOGO å›¾ç‰‡ï¼ŒæŒ‰å•元格自适应、整体居中 */
    private static void addLogoImage(XSSFWorkbook workbook, Sheet sheet) {
        byte[] bytes = loadLogoBytes();
        if (bytes == null) {
            System.err.println("[RawMaterialInspectExport] LOGO å›¾ç‰‡æœªæ‰¾åˆ°ï¼Œå¯¼å‡ºå°†ä¸å¸¦ LOGO(资源路径: " + LOGO_IMAGE_PATH + ")");
            return;
        }
        try {
            int pictureIdx = workbook.addPicture(bytes, XSSFWorkbook.PICTURE_TYPE_PNG);
            XSSFDrawing drawing = (XSSFDrawing) sheet.createDrawingPatriarch();
            // å›¾ç‰‡å®šä½ï¼šA1:C2 åŒºåŸŸå†…
            float colAPx = sheet.getColumnWidthInPixels(0);
            float colBPx = sheet.getColumnWidthInPixels(1);
            float colCPx = sheet.getColumnWidthInPixels(2);
            float blockWidthPx = colAPx + colBPx + colCPx;
            float row0Px = sheet.getRow(0).getHeightInPoints() * 96f / 72f;
            float row1Px = sheet.getRow(1).getHeightInPoints() * 96f / 72f;
            float cellHPx = row0Px + row1Px;
            // è¯»å–图片真实宽高
            int imgW = 236;
            int imgH = 190;
            if (bytes.length > 24) {
                imgW = readInt(bytes, 16);
                imgH = readInt(bytes, 20);
            }
            // æŒ‰å•元格大小等比缩放(四周留白),整体居中
            float availW = blockWidthPx - LOGO_PADDING_PX * 2;
            float availH = cellHPx - LOGO_PADDING_PX * 2;
            float scale = Math.min(availW / imgW, availH / imgH);
            float dispW = imgW * scale;
            float dispH = imgH * scale;
            float absLeft = LOGO_PADDING_PX + (availW - dispW) / 2f;
            float absTop = LOGO_PADDING_PX + (availH - dispH) / 2f;
            float absRight = absLeft + dispW;
            float absBottom = absTop + dispH;
            // è®¡ç®— from/to é”šç‚¹ï¼šæ‰€æœ‰åç§»é‡ä¿æŒåœ¨æ‰€åœ¨åˆ—/行的合法范围内
            int fromCol;
            int fromOffPx;
            if (absLeft <= colAPx) {
                fromCol = 0;
                fromOffPx = (int) absLeft;
            } else if (absLeft <= colAPx + colBPx) {
                fromCol = 1;
                fromOffPx = (int) (absLeft - colAPx);
            } else {
                fromCol = 2;
                fromOffPx = (int) (absLeft - colAPx - colBPx);
            }
            int toCol;
            int toOffPx;
            if (absRight <= colAPx) {
                toCol = 0;
                toOffPx = (int) absRight;
            } else if (absRight <= colAPx + colBPx) {
                toCol = 1;
                toOffPx = (int) (absRight - colAPx);
            } else {
                toCol = 2;
                toOffPx = (int) (absRight - colAPx - colBPx);
            }
            int fromRow = 0;
            float fromRowOff = absTop;
            if (absTop > row0Px) {
                fromRow = 1;
                fromRowOff = absTop - row0Px;
            }
            int toRow = 0;
            float toRowOff = absBottom;
            if (absBottom > row0Px) {
                toRow = 1;
                toRowOff = absBottom - row0Px;
            }
            XSSFClientAnchor anchor = new XSSFClientAnchor(
                    fromOffPx * Units.EMU_PER_PIXEL, (int) (fromRowOff * Units.EMU_PER_PIXEL),
                    toOffPx * Units.EMU_PER_PIXEL, (int) (toRowOff * Units.EMU_PER_PIXEL),
                    fromCol, fromRow, toCol, toRow);
            anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE);
            drawing.createPicture(anchor, pictureIdx);
        } catch (Exception e) {
            // LOGO åŠ è½½å¤±è´¥ä¸å½±å“å¯¼å‡ºï¼Œä½†æ‰“å°é”™è¯¯ä¾¿äºŽæŽ’æŸ¥
            System.err.println("[RawMaterialInspectExport] LOGO åµŒå…¥å¤±è´¥: " + e.getMessage());
        }
    }
    /** è¯»å– LOGO:本地开发优先读绝对路径,其次 classpath,最后项目相对路径 */
    private static byte[] loadLogoBytes() {
        try (InputStream is = new java.io.FileInputStream(LOGO_ABS_PATH)) {
            return is.readAllBytes();
        } catch (Exception ignored) {
        }
        try (InputStream is = RawMaterialInspectExportUtil.class.getResourceAsStream(LOGO_IMAGE_PATH)) {
            if (is != null) {
                return is.readAllBytes();
            }
        } catch (Exception ignored) {
        }
        try (InputStream is = new java.io.FileInputStream("src/main/resources" + LOGO_IMAGE_PATH)) {
            return is.readAllBytes();
        } catch (Exception e) {
            return null;
        }
    }
    /** æŒ‰æ–‡æœ¬åˆ—内容与列宽估算换行后的行高(点数) */
    private static float estimateRowHeight(Row row, int colCount) {
        int maxLines = 1;
        for (int c = 1; c < colCount; c++) {
            boolean isTextColumn = c == 1 || c == 2 || c == 3 || c == 7;
            if (!isTextColumn) {
                continue;
            }
            Cell cell = row.getCell(c);
            String value = cell == null ? null : cell.getStringCellValue();
            if (value == null || value.isEmpty()) {
                continue;
            }
            int lines = (int) Math.ceil(displayLen(value) / (COL_WIDTH_MAX * 2d));
            maxLines = Math.max(maxLines, lines);
        }
        return Math.max(22f, maxLines * 15f + 6f);
    }
    /** åˆ—宽自适应:按表头与数据内容计算每列宽度 */
    private static void autoSizeColumns(Sheet sheet, int colCount, int headerRowIndex, int dataRows) {
        for (int c = 0; c < colCount; c++) {
            double maxLen = displayLen(HEADERS[c]);
            for (int r = 0; r < dataRows; r++) {
                Row row = sheet.getRow(headerRowIndex + 1 + r);
                Cell cell = row == null ? null : row.getCell(c);
                if (cell != null && cell.getCellType() == CellType.STRING) {
                    maxLen = Math.max(maxLen, displayLen(cell.getStringCellValue()));
                }
            }
            double widthChars = Math.min(maxLen + COL_WIDTH_PADDING, COL_WIDTH_MAX + COL_WIDTH_PADDING);
            // ä¸­æ–‡æŒ‰ä¸¤ä¸ªåŠè§’字符宽度计算
            sheet.setColumnWidth(c, (int) (widthChars * 256));
        }
    }
    /** è®¡ç®—显示宽度:中文等全角字符按 2 ä¸ªåŠè§’宽度计 */
    private static double displayLen(String s) {
        if (s == null) {
            return 0;
        }
        double len = 0;
        for (char ch : s.toCharArray()) {
            if ((ch >= 0x2E80 && ch <= 0x9FFF) || (ch >= 0x3000 && ch <= 0x303F) || ch >= 0xFF00) {
                len += 2;
            } else {
                len += 1;
            }
        }
        return len;
    }
    /** è¯»å– PNG å¤´éƒ¨çš„宽/高(大端整数) */
    private static int readInt(byte[] b, int off) {
        return ((b[off] & 0xFF) << 24) | ((b[off + 1] & 0xFF) << 16) | ((b[off + 2] & 0xFF) << 8) | (b[off + 3] & 0xFF);
    }
    private static CellStyle createStyle(XSSFWorkbook workbook, boolean bold, float fontSize,
                                         HorizontalAlignment align) {
        CellStyle style = workbook.createCellStyle();
        Font font = workbook.createFont();
        font.setFontName("宋体");
        font.setBold(bold);
        font.setFontHeightInPoints((short) fontSize);
        style.setFont(font);
        style.setAlignment(align);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        return style;
    }
    private static void addThinBorder(CellStyle style) {
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
    }
    private static void applyRegionBorder(Sheet sheet, CellRangeAddress region) {
        RegionUtil.setBorderTop(BorderStyle.THIN, region, sheet);
        RegionUtil.setBorderBottom(BorderStyle.THIN, region, sheet);
        RegionUtil.setBorderLeft(BorderStyle.THIN, region, sheet);
        RegionUtil.setBorderRight(BorderStyle.THIN, region, sheet);
    }
    private static void setText(Cell cell, String value) {
        cell.setCellValue(value == null ? "" : value);
    }
    private static String plain(BigDecimal value) {
        return value == null ? "" : value.toPlainString();
    }
}
src/main/resources/mapper/quality/QualityInspectMapper.xml
@@ -63,7 +63,7 @@
        <if test="qualityInspect.salesContractNo != null and qualityInspect.salesContractNo != '' ">
            AND po_sales.sales_contract_no like concat('%',#{qualityInspect.salesContractNo},'%')
        </if>
        ORDER BY qi.check_time DESC
        ORDER BY qi.check_time ASC, qi.id ASC
    </select>
    <select id="qualityInspectExport" resultType="com.ruoyi.quality.pojo.QualityInspect">
src/main/resources/static/junge-logo.png