当前位置:
解决生成的word合并单元格后预览pdf后错乱的问题
makesoft | 
 13人浏览
摘要:解决生成的word合并单元格后预览pdf后错乱的问题

解决问题的相关代码贴出来了

package com.doinner.csys.utils;

import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
import fr.opensagres.xdocreport.template.IContext;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.poi.xwpf.usermodel.*;
import org.openxmlformats.schemas.wordprocessingml.x2006.main.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileInputStream;
import java.io.InputStream;
import java.math.BigInteger;

public class WordUtil {

    private static final Logger log = LoggerFactory.getLogger(WordUtil.class);

    /**
     * 获取 Word 模板的两个操作对象 IXDocReport 和 IContext
     *
     * @param path 模板的绝对地址
     * @return 模板数据对象
     */
    public static ExportData creatExportData(String path) {
        try {
            IXDocReport report = createReport(path);
            IContext context = report.createContext();
            return new ExportData(report, context);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * 加载模板的方法,主要是指定模板的路径和选择渲染数据的模板
     *
     * @param url 模板相对于类路径的地址
     * @return word 文档操作类
     */
    private static IXDocReport createReport(String url) {
        try (InputStream in = new FileInputStream(url)) {
            IXDocReport ix = XDocReportRegistry.getRegistry().loadReport(in, TemplateEngineKind.Freemarker);
            return ix;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }

    public static String readTemplatePath(String courseType) {
        String dir;
        String path = ClassLoader.getSystemResource("template").getPath();
        if ("1".equals(courseType)) {
            dir = path + "/form_work_theory.docx";
        } else if ("2".equals(courseType)) {
            dir = path + "/form_work_practice.docx";
        } else {
            dir = path + "/form_work_theory&practice.docx";
        }
        return dir;
    }

    public static String readTemplatesPath() {
        String path = System.getProperty("user.dir");
        return path + "/template/form_work_list.docx";
    }

    /**
     * 创建文档标题
     *
     * @param document XWPFDocument 对象
     * @param title    标题文本
     */
    public static void createTitle(XWPFDocument document, String title) {
        XWPFParagraph titleParagraph = document.createParagraph();
        titleParagraph.setAlignment(ParagraphAlignment.CENTER);

        XWPFRun titleRun = titleParagraph.createRun();
        titleRun.setText(title);
        titleRun.setBold(true);
        titleRun.setFontSize(18);
        titleRun.setFontFamily("微软雅黑");

        // 添加空行
        document.createParagraph();
    }

    /**
     * 创建标题(1-6 级)
     *
     * @param document XWPFDocument 对象
     * @param text     标题文本
     * @param level    标题级别(1-6)
     */
    public static void createHeading(XWPFDocument document, String text, int level) {
        XWPFParagraph paragraph = document.createParagraph();
        paragraph.setAlignment(ParagraphAlignment.LEFT);

        // 设置标题样式(使用字符串,避免依赖 ParagraphStyles 枚举)
        String styleId = getHeadingStyleId(level);
        if (styleId != null) {
            paragraph.setStyle(styleId);
        }

        paragraph.getCTP().addNewPPr().addNewOutlineLvl().setVal(BigInteger.valueOf(level - 1));
        paragraph.getCTP().addNewPPr().addNewInd().setFirstLine(BigInteger.valueOf(120 * (level - 1)));
        XWPFRun run = paragraph.createRun();
        run.setText(text);
        run.setBold(true);

        // 根据级别设置字体大小
        int fontSize = getFontSizeByLevel(level);
        run.setFontSize(fontSize);
        run.setFontFamily("微软雅黑");
    }

    /**
     * 根据标题级别获取字体大小
     *
     * @param level 标题级别
     * @return 字体大小
     */
    private static int getFontSizeByLevel(int level) {
        switch (level) {
            case 1:
                return 16;
            case 2:
                return 14;
            case 3:
                return 12;
            case 4:
                return 10;
            case 5:
                return 9;
            case 6:
                return 8;
            default:
                return 7;
        }
    }

    /**
     * 获取标题样式 ID(使用字符串,不依赖枚举)
     *
     * @param level 标题级别
     * @return 样式 ID 字符串
     */
    private static String getHeadingStyleId(int level) {
        switch (level) {
            case 1:
                return "Heading1";
            case 2:
                return "Heading2";
            case 3:
                return "Heading3";
            case 4:
                return "Heading4";
            case 5:
                return "Heading5";
            case 6:
                return "Heading6";
            default:
                return null;
        }
    }

    /**
     * 创建普通段落
     *
     * @param document XWPFDocument 对象
     * @param text     段落文本
     * @param style    样式(可为 null)
     */
    public static void createParagraph(XWPFDocument document, String text, String style) {
        XWPFParagraph paragraph = document.createParagraph();
        paragraph.setAlignment(ParagraphAlignment.LEFT);

        if (style != null) {
            paragraph.setStyle(style);
        }
        paragraph.getCTP().addNewPPr().addNewInd().setFirstLine(BigInteger.valueOf(720));
        XWPFRun run = paragraph.createRun();
        run.setText(text);
        run.setFontSize(12);
        run.setFontFamily("宋体");
    }

    /**
     * 合并表格单元格(水平合并)
     *
     * @param table   XWPFTable 对象
     * @param rowIndex     行号(从0开始)
     * @param startCol 起始列号
     * @param endCol   结束列号
     */
    public static void mergeCellsHorizontal(XWPFTable table, int rowIndex, int startCol, int endCol) {
        XWPFTableRow tableRow = table.getRow(rowIndex);
        XWPFTableCell firstCell = tableRow.getCell(startCol);
        if (ObjectUtils.isEmpty(firstCell)) {
            return;
        }
        firstCell.getCTTc().addNewTcPr().addNewGridSpan().setVal(BigInteger.valueOf(endCol - startCol + 1));
        for (int i = endCol; i > startCol; i--) {
            table.getRow(rowIndex).removeCell(i);
        }

    }


    /**
     * 合并表格单元格(垂直合并)
     *
     * @param table   XWPFTable 对象
     * @param colIndex     列号(从0开始)
     * @param startRow 起始行号
     * @param endRow   结束行号
     */
    public static void mergeCellsVertical(XWPFTable table, int colIndex, int startRow, int endRow) {
        for (int r = startRow; r <= endRow; r++) {
            XWPFTableCell cell = table.getRow(r).getCell(colIndex);
            CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
            CTVMerge vMerge = tcPr.isSetVMerge() ? tcPr.getVMerge() : tcPr.addNewVMerge();

            if (r == startRow) {
                vMerge.setVal(STMerge.RESTART);
            } else {
                vMerge.setVal(STMerge.CONTINUE);
                // 清空内容
                while (cell.getParagraphs().size() > 0) {
                    cell.removeParagraph(0);
                }
                cell.addParagraph();
            }
        }
    }

    /**
     * 设置表格单元格内容(单元格内文字居中、加粗、设置字体大小)
     *
     * @param cell   XWPFTableCell 对象
     * @param text   单元格文本
     * @param isBold 是否加粗
     * @param width  单元格宽度
     */
    public static void setCellText(XWPFTableCell cell, String text, boolean isBold, String width) {
        // 清空单元格默认段落
        if (ObjectUtils.isEmpty(cell)) {
            return;
        }
        while (cell.getParagraphs() != null && !cell.getParagraphs().isEmpty()) {
            cell.removeParagraph(0);
        }
        XWPFParagraph paragraph = cell.addParagraph();
        // 水平居中
        paragraph.setAlignment(ParagraphAlignment.CENTER);
        // 垂直居中
        cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
        // 设置单元格宽度
        if (width != null && !width.isEmpty()) {
            cell.setWidth(width);
        }
        // 设置文本
        XWPFRun run = paragraph.createRun();
        run.setFontFamily("宋体");
        run.setText(text);
        run.setFontSize(9);
        if (isBold) {
            run.setBold(true);
        }
    }

    public static String addEndDot(String str, String dot){
        if(ObjectUtils.isEmpty(str)){
            return str;
        }
        String trim = str.trim();
        char last = trim.charAt(trim.length() - 1);
        String punc=".,、。,::?!“”‘’()【】{}《》——-?!\"'()[]";
        if(punc.indexOf(last)==-1){
            return str+dot;
        }
        return str;
    }


    public static void initTableGrid(XWPFTable table, int totalCols, int colWidthDxa) {
        CTTbl ctTbl = table.getCTTbl();

        // ========== 1. 表格属性 ==========
        CTTblPr tblPr = ctTbl.getTblPr()!=null ? ctTbl.getTblPr() : ctTbl.addNewTblPr();

        // 1.1 表格左缩进=0(解决整体缩进问题)
        CTTblWidth tblInd = tblPr.isSetTblInd() ? tblPr.getTblInd() : tblPr.addNewTblInd();
        tblInd.setType(STTblWidth.DXA);
        tblInd.setW(BigInteger.ZERO);

        // 1.2 表格总宽度(固定宽度,比百分比更可靠)
        int totalWidth = totalCols * colWidthDxa;
        CTTblWidth tblW = tblPr.isSetTblW() ? tblPr.getTblW() : tblPr.addNewTblW();
        tblW.setType(STTblWidth.DXA);
        tblW.setW(BigInteger.valueOf(totalWidth));

        // 1.3 【核心】固定布局,禁止LibreOffice自动重排
        CTTblLayoutType layout = tblPr.isSetTblLayout() ? tblPr.getTblLayout() : tblPr.addNewTblLayout();
        layout.setType(STTblLayoutType.FIXED);

        // 1.4 表格左对齐
        CTJcTable jc = tblPr.getJc()!=null ? tblPr.getJc() : tblPr.addNewJc();
        jc.setVal(STJcTable.LEFT);

        // 1.5 表格边框(无边框容易网格错乱)
        CTTblBorders borders = tblPr.isSetTblBorders() ? tblPr.getTblBorders() : tblPr.addNewTblBorders();
        setTableBorders(borders);

        // 1.6 单元格内边距(解决文字往里缩的问题)
        CTTblCellMar cellMar = tblPr.isSetTblCellMar() ? tblPr.getTblCellMar() : tblPr.addNewTblCellMar();
        setCellMargin(cellMar, 80, 80, 40, 40);

        // ========== 2. 【最核心】表格列网格 tblGrid ==========
        // LibreOffice靠这个识别真实列数,没有必错乱
        if (ctTbl.getTblGrid() != null) {
            ctTbl.setTblGrid(null);
        }
        CTTblGrid tblGrid = ctTbl.addNewTblGrid();
        for (int i = 0; i < totalCols; i++) {
            CTTblGridCol gridCol = tblGrid.addNewGridCol();
            gridCol.setW(BigInteger.valueOf(colWidthDxa));
        }

        // ========== 3. 统一每行单元格 ==========
        for (XWPFTableRow row : table.getRows()) {
            // 3.1 确保每行单元格数量 = 总列数
            while (row.getTableCells().size() < totalCols) {
                row.addNewTableCell();
            }
            while (row.getTableCells().size() > totalCols) {
                row.removeCell(totalCols);
            }

            // 3.2 每个单元格设置固定宽度
            for (XWPFTableCell cell : row.getTableCells()) {
                CTTcPr tcPr = cell.getCTTc().isSetTcPr() ? cell.getCTTc().getTcPr() : cell.getCTTc().addNewTcPr();
                CTTblWidth cellW = tcPr.isSetTcW() ? tcPr.getTcW() : tcPr.addNewTcW();
                cellW.setType(STTblWidth.DXA);
                cellW.setW(BigInteger.valueOf(colWidthDxa));

                // 3.3 单元格内段落缩进清零
                for (XWPFParagraph p : cell.getParagraphs()) {
                    CTP ctp = p.getCTP();
                    CTPPr ppr = ctp.isSetPPr() ? ctp.getPPr() : ctp.addNewPPr();
                    CTInd ind = ppr.isSetInd() ? ppr.getInd() : ppr.addNewInd();
                    ind.setLeft(BigInteger.ZERO);
                    ind.setFirstLine(BigInteger.ZERO);
                }
            }

            // 3.4 默认行高
//            CTTrPr trPr = row.getCtRow().isSetTrPr() ? row.getCtRow().getTrPr() : row.getCtRow().addNewTrPr();
//            CTHeight trHeight = trPr.isSetTrHeight() ? trPr.getTrHeight() : trPr.addNewTrHeight();
//            trHeight.setVal(BigInteger.valueOf(400));
//            trHeight.setHRule(STHeightRule.AT_LEAST);
        }
    }


    private static void setTableBorders(CTTblBorders borders) {
        // 上
        CTBorder top = borders.isSetTop() ? borders.getTop() : borders.addNewTop();
        top.setVal(STBorder.SINGLE);
        top.setSz(BigInteger.valueOf(4));
        top.setColor("000000");
        // 下
        CTBorder bottom = borders.isSetBottom() ? borders.getBottom() : borders.addNewBottom();
        bottom.setVal(STBorder.SINGLE);
        bottom.setSz(BigInteger.valueOf(4));
        bottom.setColor("000000");
        // 左
        CTBorder left = borders.isSetLeft() ? borders.getLeft() : borders.addNewLeft();
        left.setVal(STBorder.SINGLE);
        left.setSz(BigInteger.valueOf(4));
        left.setColor("000000");
        // 右
        CTBorder right = borders.isSetRight() ? borders.getRight() : borders.addNewRight();
        right.setVal(STBorder.SINGLE);
        right.setSz(BigInteger.valueOf(4));
        right.setColor("000000");
        // 内部横
        CTBorder insideH = borders.isSetInsideH() ? borders.getInsideH() : borders.addNewInsideH();
        insideH.setVal(STBorder.SINGLE);
        insideH.setSz(BigInteger.valueOf(4));
        insideH.setColor("000000");
        // 内部竖
        CTBorder insideV = borders.isSetInsideV() ? borders.getInsideV() : borders.addNewInsideV();
        insideV.setVal(STBorder.SINGLE);
        insideV.setSz(BigInteger.valueOf(4));
        insideV.setColor("000000");
    }

    /**
     * 设置单元格内边距
     */
    private static void setCellMargin(CTTblCellMar cellMar, int left, int right, int top, int bottom) {
        CTTblWidth leftMar = cellMar.isSetLeft() ? cellMar.getLeft() : cellMar.addNewLeft();
        leftMar.setType(STTblWidth.DXA);
        leftMar.setW(BigInteger.valueOf(left));

        CTTblWidth rightMar = cellMar.isSetRight() ? cellMar.getRight() : cellMar.addNewRight();
        rightMar.setType(STTblWidth.DXA);
        rightMar.setW(BigInteger.valueOf(right));

        CTTblWidth topMar = cellMar.isSetTop() ? cellMar.getTop() : cellMar.addNewTop();
        topMar.setType(STTblWidth.DXA);
        topMar.setW(BigInteger.valueOf(top));

        CTTblWidth bottomMar = cellMar.isSetBottom() ? cellMar.getBottom() : cellMar.addNewBottom();
        bottomMar.setType(STTblWidth.DXA);
        bottomMar.setW(BigInteger.valueOf(bottom));
    }
}
package com.doinner.csys.entity.csys;


import com.doinner.csys.constant.ConstantTrainingScheme;
import com.doinner.csys.domain.StandardGraduation;
import com.doinner.csys.entity.csys.model.*;
import com.doinner.csys.utils.TreeBuilderUtils;
import com.doinner.csys.utils.WordUtil;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.poi.xwpf.usermodel.*;

import java.io.*;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * 培养方案文档生成器 - 基于TrainingPlanModel生成培养方案Word文档
 *
 * 包含四个一级标题:
 * 1. 培养目标
 * 2. 毕业要求
 * 3. 修业时间与学时学分
 * 4. 教学训练体系与安排
 */
public class
TrainingPlanGenerator {

    private  String[] term={"第一学年秋季学期","第一学年春季学期","第二学年秋季学期","第二学年春季学期","第三学年秋季学期","第三学年春季学期","第四学年秋季学期","第四学年春季学期"};
    /**
     * 生成培养方案文档
     *
     * @param model      培养方案数据模型
     * @throws IOException 生成异常
     */
    public InputStream generate(TrainingPlanModel model) throws IOException {
        XWPFDocument document = new XWPFDocument();

        try {
            // 1. 创建标题
            WordUtil.createTitle(document, model.getTrainingPlanName());

            // 2. 培养目标
            generateTrainingTarget(model, document);

            // 3. 毕业要求
            generateStandardGraduations(model, document);

            // 4. 修业时间与学时学分
            generateDurationAndCredits(model, document);

            // 5. 教学训练体系与安排(课程信息)
            generateCourseArrangements(model, document);

            // 输出文档
            ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
            document.write(outputStream);
            outputStream.close();
            return new ByteArrayInputStream(outputStream.toByteArray());
        } finally {
            document.close();
        }
    }

    public void generate(TrainingPlanModel model, String outputPath) throws IOException {
        XWPFDocument document = new XWPFDocument();

        try {
            // 1. 创建标题
            WordUtil.createTitle(document, model.getTrainingPlanName());

            // 2. 培养目标
            generateTrainingTarget(model, document);

            // 3. 毕业要求
            generateStandardGraduations(model, document);

            // 4. 修业时间与学时学分
            generateDurationAndCredits(model, document);

            // 5. 教学训练体系与安排(课程信息)
            generateCourseArrangements(model, document);

            // 输出文档
            FileOutputStream out = new FileOutputStream(outputPath);
            document.write(out);
            out.close();

        } finally {
            document.close();
        }
    }

    /**
     * 生成培养目标部分
     */
    private void generateTrainingTarget(TrainingPlanModel model, XWPFDocument document) {
        TrainingTargetModel target = model.getTrainingTarget();
        if(ObjectUtils.isEmpty(target)){
            return;
        }
        // 一级标题
        WordUtil.createHeading(document, target.getFirstLevelTitle(), 1);

        // 一级内容
        WordUtil.createParagraph(document, target.getFirstLevelContent(), null);

        // 二级标题1
        WordUtil.createHeading(document, target.getSecondLevelTitle1(), 2);
        WordUtil.createParagraph(document, target.getSecondLevelContent1(), null);

        // 二级标题2
        WordUtil.createHeading(document, target.getSecondLevelTitle2(), 2);
        WordUtil.createParagraph(document, target.getSecondLevelContent2(), null);
    }

    /**
     * 生成毕业要求部分
     */
    private void generateStandardGraduations(TrainingPlanModel model, XWPFDocument document) {
        // 一级标题
        WordUtil.createHeading(document, "二、毕业要求", 1);

        // 毕业要求内容说明
        WordUtil.createParagraph(document, model.getStandardGraduationContent(), null);

        List<StandardGraduation> standardGraduations = TreeBuilderUtils.buildRootTree(model.getStandardGraduations());
        if(ObjectUtils.isEmpty(standardGraduations)){
            return;
        }
        // 遍历一级标题并处理子结构
        for (StandardGraduation standardGraduation : standardGraduations) {
            processFirstLevel(standardGraduation, document);
        }
    }

    /**
     * 处理一级标题及其子结构
     */
    private void processFirstLevel(StandardGraduation graduation, XWPFDocument document) {
        WordUtil.createHeading(document, graduation.getName(), 2);
        List<StandardGraduation> children = (List<StandardGraduation>) (graduation.getChildren());
        if (ObjectUtils.isNotEmpty(children)) {
            children.forEach(child -> processSecondLevel(child, document));
        }
    }

    /**
     * 处理二级标题及其子结构
     */
    private void processSecondLevel(StandardGraduation child, XWPFDocument document) {
        WordUtil.createHeading(document, child.getName(), 3);
        List<StandardGraduation> childrenList = (List<StandardGraduation>) (child.getChildren());
        if (ObjectUtils.isNotEmpty(childrenList)) {
            String content = buildContentString(childrenList);
            WordUtil.createParagraph(document, content, null);
        }
    }

    /**
     * 拼接子节点内容字符串
     */
    private String buildContentString(List<StandardGraduation> nodes) {
        StringBuilder stringBuilder = new StringBuilder();
        for (StandardGraduation node : nodes) {
            stringBuilder.append(WordUtil.addEndDot(node.getName(),","));
        }
        return WordUtil.addEndDot(stringBuilder.deleteCharAt(stringBuilder.length() - 1).toString(),"。");
    }


    /**
     * 根据父项ID查找子项
     */
    private List<StandardGraduation> findChildrenById(List<StandardGraduation> items, Long parentId) {
        List<StandardGraduation> children = new ArrayList<>();
        if (parentId == null) {
            return children;
        }
        for (StandardGraduation item : items) {
            if (parentId.equals(item.getParentId())) {
                children.add(item);
            }
        }
        return children;
    }

    /**
     * 生成修业时间与学时学分部分
     */
    private void generateDurationAndCredits(TrainingPlanModel model, XWPFDocument document) {
        DurationAndCreditsModel dac = model.getDurationAndCredits();

        // 一级标题
        WordUtil.createHeading(document, "三、修业时间与学时学分", 1);

        // (一)修业时间安排
        WordUtil.createHeading(document, dac.getFirstLevelTitle1(), 2);
        WordUtil.createParagraph(document, dac.getFirstLevelContent1(), null);

        // (二)学时学分要求
        WordUtil.createHeading(document, dac.getFirstLevelTitle2(), 2);
        WordUtil.createParagraph(document, dac.getFirstLevelContent2(), null);

        //学时学分要求表格
        generateDurationAndCreditsTable(dac, document);

        // (三)学分冲抵机制
        WordUtil.createHeading(document, dac.getFirstLevelTitle3(), 2);
        WordUtil.createParagraph(document, dac.getFirstLevelContent3(), null);


    }

    /**
     * 生成修业时间与学时学分表格
     */
    private XWPFTable generateDurationAndCreditsTable(DurationAndCreditsModel model, XWPFDocument document) {
        // 这里可以根据需要生成具体的表格
        // 计算总行数:表头6行 + 数据行 + 小计2行+合计1行
        int totalRows = 8+model.getDataSize()+2;
        int totalCols = 9; // 原始8列,删除第6列和第11列后实际13列

        CellWidth cellWidth = new CellWidth(9);
        XWPFTable table = document.createTable(totalRows, totalCols);
        WordUtil.initTableGrid(table,totalCols,1000);
        table.setWidthType(TableWidthType.PCT);
        table.setWidth("100%");

        // 创建表格
        // ========== 表头第1行(行号0) ==========
        // 列0:课程模块,跨行3行 3列
        setCellText(table.getRow(0).getCell(0), "课程体系", true, cellWidth.getCellWidth(3));
        WordUtil.mergeCellsVertical(table, 0, 0, 2);
        WordUtil.mergeCellsHorizontal(table, 0, 0, 2);
        WordUtil.mergeCellsHorizontal(table, 1, 0, 2);
        WordUtil.mergeCellsHorizontal(table, 2, 0, 2);



        // 列1:学时学分要求,跨行6列
        setCellText(table.getRow(0).getCell(1), "学时学分要求", true, cellWidth.getCellWidth(6));
        WordUtil.mergeCellsHorizontal(table, 0, 1, 6);

        // ========== 表头第2行(行号1) ==========
        // 列2:必修,跨行2行
        setCellText(table.getRow(1).getCell(1), "必修", true,  cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 1, 2);

        // 列3:选修,跨行2行
        setCellText(table.getRow(1).getCell(2), "选修", true, cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 2,3);

        // 列3:小计,跨行2行
        setCellText(table.getRow(1).getCell(3), "小计", true, cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 3, 4);
        // ========== 表头第2行(行号2) ==========
        setCellText(table.getRow(2).getCell(1), "学时", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(2), "学分", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(3), "学时", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(4), "学分", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(5), "学时", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(6), "学分", true, cellWidth.getCellWidth(1));
        // deleteCol(table,0,2,totalCols);
        // deleteCol(table,1,4,totalCols);
        // deleteCol(table,2,7,totalCols);
        // ========== 内容 ==========
        int dataRow=3;
        CountModel countModel = new CountModel();
        setCellText(getCell(table, dataRow,0), "公共基础\n课程\n", true, cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 0, 3, 3+model.getGeneralCourses().size());
        //排序
        List<CreditsDetailModel> detailModelList = model.getGeneralCourses().parallelStream()
                .sorted(Comparator.comparing(CreditsDetailModel::getModelNameSort)
                        .thenComparing(CreditsDetailModel::getChildrenNameModelSort))
                .collect(Collectors.toList());
        HashMap<String, int[]> mergeRowMap = new HashMap<>();
        for (int i = 0; i < detailModelList.size(); i++) {
            int firstCell=1;
            CreditsDetailModel creditsDetailModel = model.getGeneralCourses().get(i);
            if(ObjectUtils.isEmpty(creditsDetailModel.getChildrenNameModelName())||creditsDetailModel.getChildrenNameModelName().equals("-1")){
                //只有一级
                setCellText(getCell(table, dataRow,firstCell), creditsDetailModel.getModelName(), true, cellWidth.getCellWidth(2));
                WordUtil.mergeCellsHorizontal(table, dataRow, 1,2);
                // deleteCol(table,dataRow,8,totalCols);
            }else{
                //存在子集
                setCellText(getCell(table, dataRow,firstCell), creditsDetailModel.getModelName(), true, cellWidth.getCellWidth(1));
                firstCell++;
                setCellText(getCell(table, dataRow,firstCell), creditsDetailModel.getChildrenNameModelName(), true, cellWidth.getCellWidth(1));
                if(!mergeRowMap.containsKey(creditsDetailModel.getModelName())){
                    int[] rowArr=new int[2];
                    rowArr[0]=dataRow;
                    rowArr[1]=dataRow;
                    mergeRowMap.put(creditsDetailModel.getModelName(), rowArr);
                }else{
                    int[] rowArr = mergeRowMap.get(creditsDetailModel.getModelName());
                    rowArr[1]=dataRow;
                    mergeRowMap.put(creditsDetailModel.getModelName(), rowArr);
                }
            }
            firstCell++;
            setCreditsAndHours(creditsDetailModel,firstCell,table,dataRow,countModel,cellWidth);
            dataRow++;
        }
        //合并行
        mergeRowMap.forEach((k,rowArr)->{
            WordUtil.mergeCellsVertical(table,1,rowArr[0],rowArr[1]);
        });

        Map<String, CreditsDetailModel> disciplineMajorCourseMap = model.getDisciplineMajorCourseMap();
        setCellText(getCell(table, dataRow,0), "学科专业\n课程\n", true, cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 0, dataRow, dataRow+1);
        // deleteCol(table,dataRow,8,totalCols);

        // ========== 内容 第六行:8 ==========
        setCellText(getCell(table, dataRow,1), DictContent.DISCIPLINE_COURSE_NAME, true, cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, dataRow, 1,2);
        CreditsDetailModel disciplineDetail = disciplineMajorCourseMap.get(DictContent.DISCIPLINE_COURSE_NAME);
        setCreditsAndHours(disciplineDetail, 2,table, dataRow,countModel, cellWidth);
        // deleteCol(table,dataRow,8,totalCols);
        dataRow++;
        // ========== 内容 第七行:9 ==========
        setCellText(getCell(table, dataRow,1), DictContent.MAJOR_COURSE_NAME, true, cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, dataRow, 1,2);
        CreditsDetailModel majorDetail = disciplineMajorCourseMap.get(DictContent.MAJOR_COURSE_NAME);
        setCreditsAndHours(majorDetail,2, table, dataRow,countModel, cellWidth);
        // deleteCol(table,dataRow,8,totalCols);
        dataRow++;
        //小计
        setCellText(getCell(table, dataRow,0), "小计", true, cellWidth.getCellWidth(3));
        WordUtil.mergeCellsHorizontal(table, dataRow, 0, 2);
        setCellText(getCell(table, dataRow,1), countModel.getRequireHours()+"", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,2), countModel.getRequireCredit()+"", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,3), countModel.getOptionalHours()+"", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,4), countModel.getOptionalCredit()+"", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,5), countModel.getTotalHours()+"", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,6), countModel.getTotalCredit()+"", true, cellWidth.getCellWidth(1));
        // deleteCol(table,dataRow,7,totalCols);
        dataRow++;
        //=========表2================
        setCellText(getCell(table, dataRow,0), "实践训练体系", true, cellWidth.getCellWidth(3));
        WordUtil.mergeCellsVertical(table, 0, dataRow, dataRow+2);
        WordUtil.mergeCellsHorizontal(table, dataRow, 0, 2);
        WordUtil.mergeCellsHorizontal(table, dataRow+1, 0, 2);
        WordUtil.mergeCellsHorizontal(table, dataRow+2, 0, 2);
        // 列1:学时学分要求,跨行6列
        setCellText(getCell(table, dataRow,1), "学时学分要求", true, cellWidth.getCellWidth(6));
        WordUtil.mergeCellsHorizontal(table, dataRow, 1, 6);
        // deleteCol(table,dataRow,2,totalCols);
        dataRow++;
        // ========== 表头第2行(行号1) ==========
        // 列2:必修,跨行2行
        setCellText(getCell(table, dataRow,1), "必修", true, cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, dataRow, 1, 2);
        // 列3:选修,跨行2行
        setCellText(getCell(table, dataRow,2), "选修", true, cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, dataRow, 2, 3);
        // 列3:小计,跨行2行
        setCellText(getCell(table, dataRow,3), "小计", true, cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, dataRow, 3, 4);
        // deleteCol(table,dataRow,4,totalCols);
        dataRow++;

        setCellText(getCell(table, dataRow,1), "学时", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,2), "学分", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,3), "学时", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,4), "学分", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,5), "学时", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,6), "学分", true, cellWidth.getCellWidth(1));
        // deleteCol(table,dataRow,7,totalCols);
        dataRow++;
        //小计
        setCellText(getCell(table, dataRow,0), "小计", true, cellWidth.getCellWidth(3));
        WordUtil.mergeCellsHorizontal(table, dataRow, 0, 2);
        CreditsDetailModel trainingProjectCourse = model.getTrainingProjectCourses();
        //实践训练一共就一行 直接用countModel去加 算出合计
        setCreditsAndHours(trainingProjectCourse, 1,table, dataRow,countModel, cellWidth);
        // deleteCol(table,dataRow,7,totalCols);
        dataRow++;
        //合计
        setCellText(getCell(table, dataRow,0), "合计", true, cellWidth.getCellWidth(3));
        WordUtil.mergeCellsHorizontal(table, dataRow, 0, 2); setCellText(getCell(table, dataRow,1), countModel.getRequireHours()+"", true, "25%");
        setCellText(getCell(table, dataRow,1), countModel.getRequireHours()+"", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,2), countModel.getRequireCredit()+"", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,3), countModel.getOptionalHours()+"", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,4), countModel.getOptionalCredit()+"", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,5), countModel.getTotalHours()+"", true, cellWidth.getCellWidth(1));
        setCellText(getCell(table, dataRow,6), countModel.getTotalCredit()+"", true, cellWidth.getCellWidth(1));
        // deleteCol(table,dataRow,7,totalCols);
        return table;
    }

    private static XWPFTableCell getCell(XWPFTable table, int dataRow,int cell) {
        if(ObjectUtils.isEmpty(table.getRow(dataRow))) {
            table.createRow().createCell();
        }
        return table.getRow(dataRow).getCell(cell);
    }

    private void setCreditsAndHours(CreditsDetailModel creditsDetailModel, int firstCell, XWPFTable table, int dataRow , CountModel countModel, CellWidth cellWidth) {
        if(ObjectUtils.isEmpty(creditsDetailModel)){
            return;
        }
        setCellText(getCell(table, dataRow,firstCell), creditsDetailModel.getRequiredHours() + "", true, cellWidth.getCellWidth(1));
        countModel.setRequireHours(countModel.getRequireHours()+creditsDetailModel.getRequiredHours());

        setCellText(getCell(table, dataRow,firstCell+1), creditsDetailModel.getRequiredCredits() + "", true, cellWidth.getCellWidth(1));
        countModel.setRequireCredit(countModel.getRequireCredit()+creditsDetailModel.getRequiredCredits());

        setCellText(getCell(table, dataRow,firstCell+2), creditsDetailModel.getOptionalHours() + "", true, cellWidth.getCellWidth(1));
        countModel.setOptionalHours(countModel.getOptionalHours()+creditsDetailModel.getOptionalHours());

        setCellText(getCell(table, dataRow,firstCell+3), creditsDetailModel.getOptionalCredits() + "", true, cellWidth.getCellWidth(1));
        countModel.setOptionalCredit(countModel.getOptionalCredit()+creditsDetailModel.getOptionalCredits());

        setCellText(getCell(table, dataRow,firstCell+4), creditsDetailModel.getTotalHours() + "", true, cellWidth.getCellWidth(1));
        countModel.setTotalHours(countModel.getTotalHours()+creditsDetailModel.getTotalHours());

        setCellText(getCell(table, dataRow,firstCell+5), creditsDetailModel.getTotalCredits() + "", true, cellWidth.getCellWidth(1));
        countModel.setTotalCredit(countModel.getTotalCredit()+creditsDetailModel.getTotalCredits());

    }

    /**
     * 生成教学训练体系与安排(课程信息表格)
     */
    private void generateCourseArrangements(TrainingPlanModel model, XWPFDocument document) {
        // 一级标题
        WordUtil.createHeading(document, "四、教学训练体系与安排", 1);
        WordUtil.createParagraph(document,"通识课程任选课程安排表见附录1、专业任选课程安排表见附录2、其他课程和实践训练安排如下:",null);
        //二级标题
        WordUtil.createHeading(document, "(一)课程安排", 2);

        // 公共基础课程教学安排
        if (model.getGeneralCourses() != null && !model.getGeneralCourses().isEmpty()) {
            XWPFTable table = generateCourseTable(document, "1.公共基础课程教学安排", model.getGeneralCourses());
            WordUtil.createParagraph(document, "", null); // 空行
        }
        // 添加备注
        XWPFParagraph remarkParagraph = document.createParagraph();
        XWPFRun remarkRun = remarkParagraph.createRun();
        remarkRun.setText("备注:1.B代表必修课程、X代表限选课程、R代表任选课程;S代表考试,C代表考查(下同)。\n 2.《大学英语》免修规则:请军政基础教育学院补充。");
        remarkRun.setFontSize(10);

        // 学科基础课程
        if (model.getDisciplineCourses() != null && !model.getDisciplineCourses().isEmpty()) {
            XWPFTable table = disciplineCoursesTable(document, "2.学科基础课程教学安排", model.getDisciplineCourses());
            WordUtil.createParagraph(document, "", null); // 空行
        }

        // 专业课程
        if (model.getMajorCourses() != null && !model.getMajorCourses().isEmpty()) {
            XWPFTable table = majorCoursesTable(document, "3.专业课程教学安排", model.getMajorCourses());
            WordUtil.createParagraph(document, "", null); // 空行
        }
        //二级标题
        WordUtil.createHeading(document, "(二)实践训练安排", 2);
        //实践项目训练课程
        if (model.getTrainingSubjectCourses() != null && !model.getTrainingSubjectCourses().isEmpty()) {
            XWPFTable table = practicalProjectCourseTable(document, "1.实践训练课目与安排", model.getTrainingSubjectCourses());
            WordUtil.createParagraph(document, "", null); // 空行
        }
        //2.实践项目与安排
        if (model.getPracticalProjectCourse() != null && !model.getPracticalProjectCourse().isEmpty()) {
            XWPFTable table = trainingSubjectCoursesTable(document, "2.实践项目与安排", model.getPracticalProjectCourse());
            WordUtil.createParagraph(document, "", null); // 空行
        }

    }



    /**
     * 生成课程教学安排表格 - 严格按照TableCreateGenerator中的表头结构
     *
     * 表格结构:
     * - 列数:16列(删除第6列和第11列后实际为13列)
     * - 行数:表头3行 + 模块行 + 小计1行
     *
     * 表头结构(3行):
     * 行0:课程模块 | 课程名称 | 修读要求 | 考核方式 | 学时安排(跨3列) | 学期安排(跨8列)
     * 行1:[ Module行跨3行 ] | | | | 小计 | 讲授 | 实践 | 第一学年(跨2列) | 第二学年(跨2列) | 第三学年(跨2列) | 第四学年(跨2列)
     * 行2:[ Module行跨3行 ] | | | | | | | 秋 | 春 | 秋 | 春 | 秋 | 春 | 秋 | 春
     *
     * 删除第6列(空列)和第11列(空列),所以实际使用13列
     */
    private XWPFTable generateCourseTable(XWPFDocument document, String title, List<TrainingSchemeCourseModel> courses) {
        // 创建表格标题
        WordUtil.createHeading(document, title, 3);

        // 计算总行数:表头3行 + 数据行 + 小计1行
        int dataRows = courses.size();
        int totalRows = 3 + dataRows + 1;
        int totalCols = 16; // 原始16列,删除第6列和第11列后实际13列
        CellWidth cellWidth = new CellWidth(17);

        XWPFTable table = document.createTable(totalRows, totalCols);
        WordUtil.initTableGrid(table,totalCols,1000);

        table.setWidthType(TableWidthType.PCT);
        table.setWidth("100%");

        // 生成表头(3行)
        generateCourseHeader(table,cellWidth);


        int dataRowStart = 3;

        Map<String, Map<String, List<TrainingSchemeCourseModel>>> couseMap = TrainingSchemeCourseModel.groupCourses(courses);
        // 处理每一门课程,确定其所属模块
        AtomicInteger dataRow = new AtomicInteger(dataRowStart);
        CountModel countModel = new CountModel();
        couseMap.forEach((modelName,courseFourLevelMap) -> {
            int moduleRowStart = dataRow.get();
            AtomicInteger moduleRowEnd = new AtomicInteger(moduleRowStart);
            courseFourLevelMap.forEach((fourLevelName,courseModels) -> {
                int fourMoudleStart = dataRow.get();
                int fourMoudleEnd = fourMoudleStart + courseModels.size();
                int initCell=1;
                XWPFTableRow row = table.getRow(fourMoudleStart);
                //判断科学文化下的基础科学等子模块
                if(DictContent.FOUR_LEVEL_NAME_NULL .equals(fourLevelName)){
                    for (int i = fourMoudleStart; i < fourMoudleEnd; i++) {
                        WordUtil.mergeCellsHorizontal(table, i, 0,  1);
                        // deleteCol(table,i,15,15);
                    }
                }else{
                    initCell=2;
                    XWPFTableCell cell1 = row.getCell(1);
                    WordUtil.setCellText(cell1, fourLevelName, false,  cellWidth.getCellWidth(1));
                    WordUtil.mergeCellsVertical(table, 1, fourMoudleStart,  fourMoudleStart+courseModels.size() - 1);
                }
                for (int i = 0; i < courseModels.size(); i++) {
                    TrainingSchemeCourseModel course = courseModels.get(i);
                    // 设置课程数据行
                    int dataRowIndex = fourMoudleStart + i;
                    setCourseDataRow(table, dataRowIndex, course,initCell,countModel,cellWidth);
                }

                // 前进到下一个模块的起始行
                dataRow.set(fourMoudleEnd);
                // 更新模块结束行
                moduleRowEnd.set(fourMoudleEnd);
            });
            XWPFTableRow row = table.getRow(moduleRowStart);
            XWPFTableCell cell1 = row.getCell(0);
            WordUtil.setCellText(cell1, modelName, false,  cellWidth.getCellWidth(1));
            WordUtil.mergeCellsVertical(table, 0, moduleRowStart,  moduleRowEnd.get() - 1);

        });
        // 设置小计行
        int totalRow = totalRows - 1;
        setTotalRow(table, totalRow,countModel,4,7,cellWidth);
        // deleteCol(table,totalRow,15,16);

        return table;
    }

    private XWPFTable disciplineCoursesTable(XWPFDocument document, String title, List<TrainingSchemeCourseModel> courses) {
        // 创建表格标题
        WordUtil.createHeading(document, title, 3);

        // 计算总行数:表头3行 + 数据行 + 小计1行
        int dataRows = courses.size();
        int totalRows = 3 + dataRows + 1;
        int totalCols = 14; //原始15列,删除第6列和第11列后实际13列

        XWPFTable table = document.createTable(totalRows, totalCols);
        WordUtil.initTableGrid(table,totalCols,1000);

        table.setWidthType(TableWidthType.PCT);
        table.setWidth("100%");
        CellWidth cellWidth = new CellWidth(15);

        // 生成表头(3行)
        disciplineCoursesHeader(table,cellWidth);


        int dataRowStart = 3;

        // 处理每一门课程,确定其所属模块
        CountModel countModel = new CountModel();
        for (int i = 0; i < courses.size(); i++) {
            TrainingSchemeCourseModel course = courses.get(i);
            // 设置课程数据行
            int dataRowIndex = dataRowStart + i;
            setCourseDataRow(table, dataRowIndex, course,0,countModel, cellWidth);
        }

        // 设置小计行
        int totalRow = totalRows - 1;
        setTotalRow(table, totalRow,countModel,2,5, cellWidth);

        return table;
    }

    private XWPFTable majorCoursesTable(XWPFDocument document, String title, List<TrainingSchemeCourseModel> courses) {
        // 创建表格标题
        WordUtil.createHeading(document, title, 3);
        Map<String, List<TrainingSchemeCourseModel>> couseMap = courses.stream().collect(Collectors.groupingBy(course -> course.getSubMajorName()==null?"**专业方向":course.getSubMajorName()));

        // 计算总行数:表头3行 + 数据行 + 小计1行
        int dataRows = courses.size();
        int totalRows = 3 + dataRows +couseMap.size();
        int totalCols = 15; // 原始15列,删除第6列和第11列后实际13列

        XWPFTable table = document.createTable(totalRows, totalCols);
        WordUtil.initTableGrid(table,totalCols,1000);

        table.setWidthType(TableWidthType.PCT);
        table.setWidth("100%");
        CellWidth cellWidth = new CellWidth(16);
        // 生成表头(3行)
        majorCoursesHeader(table,cellWidth);


        int dataRowStart = 3;

        // 处理每一门课程,确定其所属模块
        //todo 专业方向字段
        AtomicInteger dataRow = new AtomicInteger(dataRowStart);
        couseMap.forEach((majorName,courseModels) -> {
            int moduleRowStart = dataRow.get();
            int moduleRowEnd = dataRow.get()+courseModels.size();
            CountModel countModel = new CountModel();
            for (int i = 0; i < courseModels.size(); i++) {
                TrainingSchemeCourseModel course = courseModels.get(i);
                // 设置课程数据行
                int dataRowIndex = moduleRowStart + i;
                setCourseDataRow(table, dataRowIndex, course,1,countModel, cellWidth);
            }
            XWPFTableRow row = table.getRow(moduleRowStart);
            XWPFTableCell cell1 = row.getCell(0);
            WordUtil.setCellText(cell1, majorName, false, "1134");
            WordUtil.mergeCellsVertical(table, 0, moduleRowStart,  moduleRowEnd - 1);
            // 设置小计行
            setTotalRow(table, moduleRowEnd,countModel,3,6, cellWidth);
            // deleteCol(table,moduleRowEnd,14,15);
            dataRow.set(moduleRowEnd+1);
        });
        return table;
    }



    private XWPFTable practicalProjectCourseTable(XWPFDocument document, String title, List<TrainingSchemeCourseModel> courses) {
        WordUtil.createHeading(document, title, 3);

        // 计算总行数:表头1行 + 数据行+小计
        int dataRows = courses.size();
        int totalRows =  dataRows + 1;
        int totalCols = 6; // 原始6

        XWPFTable table = document.createTable(totalRows, totalCols);
        WordUtil.initTableGrid(table,totalCols,1000);

        table.setWidthType(TableWidthType.PCT);
        table.setWidth("100%");
        CellWidth cellWidth = new CellWidth(6);
        // 生成表头(1行)
        practicalProjectCourseHeader(table,cellWidth);
        int dataRowStart = 1;

        Map<String, List<TrainingSchemeCourseModel>> courseMap = courses.stream().collect(Collectors.groupingBy(c -> c.getTrainingCourseModelName() == null ? "" : c.getTrainingCourseModelName()));
        // 处理每一门课程,确定其所属模块
        AtomicInteger moduleRowStart = new AtomicInteger(dataRowStart);
        CountModel countModel = new CountModel();
        courseMap.forEach((modelName,courseModels) -> {
            int moduleRowEnd = moduleRowStart.get()+courseModels.size();
            for (int i = 0; i < courseModels.size(); i++) {
                TrainingSchemeCourseModel course = courseModels.get(i);
                // 设置课程数据行
                int dataRowIndex = moduleRowStart.get() + i;
                XWPFTableRow row = table.getRow(dataRowIndex);
                //课程名称
                XWPFTableCell cell1 = row.getCell(1);
                WordUtil.setCellText(cell1, course.getName(), false, cellWidth.getCellWidth(1));
                //修读要求
                XWPFTableCell cell2 = row.getCell(2);
                String attrText = getAttrText(course.getCourseAttr());
                WordUtil.setCellText(cell2, attrText, false, cellWidth.getCellWidth(1));
                //时间安排
                XWPFTableCell cell3 = row.getCell(3);
                String timeWeek = course.getTimeWeek() == null ? "" : course.getTimeWeek().toString();
                String timeWeekText = timeWeek + (course.getUnit() == null ? "" : course.getUnit());
                WordUtil.setCellText(cell3, timeWeekText, false, cellWidth.getCellWidth(1));
                //学期安排
                XWPFTableCell cell4 = row.getCell(4);
                StringBuilder opentermStr=new StringBuilder();
                for (Integer openTerm : course.getOpenTerm()) {
                    if(ObjectUtils.isNotEmpty(openTerm)){
                        opentermStr.append(term[openTerm-1]+",");
                    }
                }
                if(ObjectUtils.isNotEmpty(opentermStr)){
                    WordUtil.setCellText(cell4,opentermStr.deleteCharAt(opentermStr.length()-1).toString(), false, cellWidth.getCellWidth(1));
                }
                //支撑课程或实践训练课目
                XWPFTableCell cell5 = row.getCell(5);
                WordUtil.setCellText(cell5, course.getRemark(), false, cellWidth.getCellWidth(1));
            }
            //设置模块行
            XWPFTableRow row = table.getRow(moduleRowStart.get());
            XWPFTableCell cell1 = row.getCell(0);
            WordUtil.setCellText(cell1, modelName, false, cellWidth.getCellWidth(1));
            WordUtil.mergeCellsVertical(table, 0, moduleRowStart.get(),  moduleRowEnd - 1);
            moduleRowStart.set(moduleRowEnd);
        });
        return table;
    }

    private XWPFTable trainingSubjectCoursesTable(XWPFDocument document, String title, List<TrainingSchemeCourseModel> courses) {
        WordUtil.createHeading(document, title, 3);

        // 计算总行数:表头1行 + 数据行
        int dataRows = courses.size();
        int totalRows =  dataRows + 1;
        int totalCols = 7; // 专业方向 + 项目层级 + 项目名称 + 修读要求 + 时间安排 + 学期安排 + 支撑课程

        XWPFTable table = document.createTable(totalRows, totalCols);
        WordUtil.initTableGrid(table,totalCols,1000);
        table.setWidthType(TableWidthType.PCT);
        table.setWidth("100%");
        CellWidth cellWidth = new CellWidth(totalCols);
        // 生成表头(1行)
        trainingSubjectCoursesHeader(table,cellWidth);


        int dataRowStart = 1;

        // 两层分组:外层专业方向(subMajorName) -> 内层项目层级(projectLevelName),使用 LinkedHashMap 保持输入顺序
        Map<String, Map<String, List<TrainingSchemeCourseModel>>> subMajorMap = courses.stream().collect(
                Collectors.groupingBy(
                        c -> c.getSubMajorName() == null ? "" : c.getSubMajorName(),
                        LinkedHashMap::new,
                        Collectors.groupingBy(
                                c -> c.getProjectLevelName() == null ? "**" : c.getProjectLevelName(),
                                LinkedHashMap::new,
                                Collectors.toList())));
        // 处理每一门课程,确定其所属模块
        AtomicInteger moduleRowStart = new AtomicInteger(dataRowStart);
        subMajorMap.forEach((subMajorName, levelMap) -> {
            int subMajorStart = moduleRowStart.get();
            levelMap.forEach((levelName, courseModels) -> {
                int levelStart = moduleRowStart.get();
                int levelEnd = levelStart + courseModels.size();
                for (int i = 0; i < courseModels.size(); i++) {
                    TrainingSchemeCourseModel course = courseModels.get(i);
                    // 设置课程数据行
                    int dataRowIndex = levelStart + i;
                    XWPFTableRow row = table.getRow(dataRowIndex);
                    //项目名称
                    XWPFTableCell cell1 = row.getCell(2);
                    WordUtil.setCellText(cell1, course.getName(), false, cellWidth.getCellWidth(1));
                    //修读要求
                    XWPFTableCell cell2 = row.getCell(3);
                    String attrText = getAttrText(course.getCourseAttr());
                    WordUtil.setCellText(cell2, attrText, false, cellWidth.getCellWidth(1));
                    //时间安排
                    XWPFTableCell cell3 = row.getCell(4);
                    String timeWeek = course.getTimeWeek() == null ? "" : course.getTimeWeek().toString();
                    String timeWeekText = timeWeek + (course.getUnit() == null ? "" : course.getUnit());
                    WordUtil.setCellText(cell3, timeWeekText, false, cellWidth.getCellWidth(1));
                    //学期安排
                    XWPFTableCell cell4 = row.getCell(5);
                    StringBuilder opentermStr=new StringBuilder();
                    for (Integer openTerm : course.getOpenTerm()) {
                        if(ObjectUtils.isNotEmpty(openTerm)){
                            opentermStr.append(term[openTerm-1]+",");
                        }
                    }
                    if(ObjectUtils.isNotEmpty(opentermStr)){
                        WordUtil.setCellText(cell4,opentermStr.deleteCharAt(opentermStr.length()-1).toString(), false, cellWidth.getCellWidth(1));
                    }
                    //支撑课程或实践训练课目
                    XWPFTableCell cell5 = row.getCell(6);
                    WordUtil.setCellText(cell5, course.getSupportingCourseNames(), false, cellWidth.getCellWidth(1));
                }
                //设置项目层级列(col1)合并
                XWPFTableRow levelRow = table.getRow(levelStart);
                WordUtil.setCellText(levelRow.getCell(1), levelName, false, cellWidth.getCellWidth(1));
                WordUtil.mergeCellsVertical(table, 1, levelStart, levelEnd - 1);
                moduleRowStart.set(levelEnd);
            });
            //设置专业方向列(col0)合并,字段为空显示为空
            int subMajorEnd = moduleRowStart.get();
            if (subMajorEnd > subMajorStart) {
                XWPFTableRow subMajorRow = table.getRow(subMajorStart);
                WordUtil.setCellText(subMajorRow.getCell(0), subMajorName == null ? "" : subMajorName, false, cellWidth.getCellWidth(1));
                WordUtil.mergeCellsVertical(table, 0, subMajorStart, subMajorEnd - 1);
            }
        });

        // 设置小计行
//        int totalRow = totalRows - 1;
//        setTotalRow(table, totalRow);
//        // deleteCol(table,totalRow,5,6);

        return table;
    }

    /**
     * 填充通识课程表格表头(3行)
     */
    private void generateCourseHeader(XWPFTable table, CellWidth cellWidth) {
        // ========== 表头第1行(行号0) ==========
        // 列0:课程模块,跨行3行
        setCellText(table.getRow(0).getCell(0), "课程模块", true, cellWidth.getCellWidth(3));
        WordUtil.mergeCellsVertical(table, 0, 0, 2);
        WordUtil.mergeCellsHorizontal(table, 0, 0, 1);
        WordUtil.mergeCellsHorizontal(table, 1, 0, 1);
        WordUtil.mergeCellsHorizontal(table, 2, 0, 1);

        // 列1:课程名称,跨行3行
        setCellText(table.getRow(0).getCell(1), "课程名称", true, cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 1, 0, 2);

        // 列2:修读要求,跨行3行
        setCellText(table.getRow(0).getCell(2), "修读\n要求", true, cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 2, 0, 2);

        // 列3:考核方式,跨行3行
        setCellText(table.getRow(0).getCell(3), "考核\n方式", true, cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 3, 0, 2);

        // 列4-6:学时安排,跨列3列(小计、讲授、实践)
        setCellText(table.getRow(0).getCell(4), "学时安排", true, cellWidth.getCellWidth(3));
        WordUtil.mergeCellsHorizontal(table, 0, 4, 6);

        // 列7-14:学期安排,跨列8列(4个学年,每个学年2个学期)
        setCellText(table.getRow(0).getCell(5), "学期安排", true, cellWidth.getCellWidth(8));
        WordUtil.mergeCellsHorizontal(table, 0, 5, 12);

        // ========== 表头第2行(行号1) ==========
        // 列4-6:学时子项
        setCellText(table.getRow(1).getCell(4), "小计", true, cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 4, 1, 2);
        setCellText(table.getRow(1).getCell(5), "讲授", true, cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 5, 1, 2);
        setCellText(table.getRow(1).getCell(6), "实践", true, cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 6, 1, 2);

        // 列7-8:第一学年,跨列2列
        setCellText(table.getRow(1).getCell(7), "第一学年", true, cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 7, 8);
        // 列9-10:第二学年,跨列2列
        setCellText(table.getRow(1).getCell(8), "第二学年", true, cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 8, 9);
        // 列11-12:第三学年,跨列2列
        setCellText(table.getRow(1).getCell(9), "第三学年", true, cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 9, 10);
        // 列13-14:第四学年,跨列2列
        setCellText(table.getRow(1).getCell(10), "第四学年", true, cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 10, 11);

        // ========== 表头第3行(行号2) ==========
        // 列7-14:学期子项(秋、春交替)
        setCellText(table.getRow(2).getCell(7), "秋", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(8), "春", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(9), "秋", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(10), "春", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(11), "秋", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(12), "春", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(13), "秋", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(14), "春", true, cellWidth.getCellWidth(1));

        // 删除第一行第二行多余的列
        // deleteCol(table,0,6,15); // 删除第6列(索引5)
        // deleteCol(table,1,11,15);// 删除第11列(索引10)
        // deleteCol(table,2,15,15);// 删除第11列(索引10)
    }

    /**
     * 专业大类课程教学安排
     *
     * @param table
     * @param cellWidth
     */
    private void disciplineCoursesHeader(XWPFTable table, CellWidth cellWidth) {
        // ========== 表头第1行(行号0) ==========

        // 列1:课程名称,跨行3行
        setCellText(table.getRow(0).getCell(0), "课程名称", true, cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 0, 0, 2);

        // 列2:修读要求,跨行3行
        setCellText(table.getRow(0).getCell(1), "修读\n要求", true,  cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 1, 0, 2);

        // 列3:考核方式,跨行3行
        setCellText(table.getRow(0).getCell(2), "考核\n方式", true,  cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 2, 0, 2);

        // 列4-6:学时安排,跨列3列(小计、讲授、实践)
        setCellText(table.getRow(0).getCell(3), "学时安排", true,  cellWidth.getCellWidth(3));
        WordUtil.mergeCellsHorizontal(table, 0, 3, 5);

        // 列7-14:学期安排,跨列8列(4个学年,每个学年2个学期)
        setCellText(table.getRow(0).getCell(4), "学期安排", true,  cellWidth.getCellWidth(8));
        WordUtil.mergeCellsHorizontal(table, 0, 4, 11);

        // ========== 表头第2行(行号1) ==========
        // 列4-6:学时子项
        setCellText(table.getRow(1).getCell(3), "小计", true,  cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 3, 1, 2);
        setCellText(table.getRow(1).getCell(4), "讲授", true,  cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 4, 1, 2);
        setCellText(table.getRow(1).getCell(5), "实践", true,  cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 5, 1, 2);

        // 列7-8:第一学年,跨列2列
        setCellText(table.getRow(1).getCell(6), "第一学年", true,  cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 6, 7);
        // 列9-10:第二学年,跨列2列
        setCellText(table.getRow(1).getCell(7), "第二学年", true,  cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 7, 8);
        // 列11-12:第三学年,跨列2列
        setCellText(table.getRow(1).getCell(8), "第三学年", true,  cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 8, 9);
        // 列13-14:第四学年,跨列2列
        setCellText(table.getRow(1).getCell(9), "第四学年", true,  cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 9, 10);

        // ========== 表头第3行(行号2) ==========
        // 列7-14:学期子项(秋、春交替)
        setCellText(table.getRow(2).getCell(6), "秋", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(7), "春", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(8), "秋", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(9), "春", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(10), "秋", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(11), "春", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(12), "秋", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(13), "春", true,  cellWidth.getCellWidth(1));
        // 删除第一行第二行多余的列
        // deleteCol(table,0,5,15); // 删除第6列(索引5)
        // deleteCol(table,1,10,15);// 删除第11列(索引10)
    }
    /**
     * 专业方向课程教学安排
     *
     * @param table
     * @param cellWidth
     */
    private void majorCoursesHeader(XWPFTable table, CellWidth cellWidth) {
        // ========== 表头第1行(行号0) ==========
        // 列0:课程模块,跨行3行
        setCellText(table.getRow(0).getCell(0), "专业方向", true,  cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 0, 0, 2);

        // 列1:课程名称,跨行3行
        setCellText(table.getRow(0).getCell(1), "课程名称", true,  cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 1, 0, 2);

        // 列2:修读要求,跨行3行
        setCellText(table.getRow(0).getCell(2), "修读\n要求", true,  cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 2, 0, 2);

        // 列3:考核方式,跨行3行
        setCellText(table.getRow(0).getCell(3), "考核\n方式", true,  cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 3, 0, 2);

        // 列4-6:学时安排,跨列3列(小计、讲授、实践)
        setCellText(table.getRow(0).getCell(4), "学时安排", true,  cellWidth.getCellWidth(3));
        WordUtil.mergeCellsHorizontal(table, 0, 4, 6);

        // 列7-14:学期安排,跨列8列(4个学年,每个学年2个学期)
        setCellText(table.getRow(0).getCell(5), "学期安排", true,  cellWidth.getCellWidth(8));
        WordUtil.mergeCellsHorizontal(table, 0, 5, 12);

        // ========== 表头第2行(行号1) ==========
        // 列4-6:学时子项
        setCellText(table.getRow(1).getCell(4), "小计", true,  cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 4, 1, 2);
        setCellText(table.getRow(1).getCell(5), "讲授", true,  cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 5, 1, 2);
        setCellText(table.getRow(1).getCell(6), "实践", true,  cellWidth.getCellWidth(1));
        WordUtil.mergeCellsVertical(table, 6, 1, 2);

        // 列7-8:第一学年,跨列2列
        setCellText(table.getRow(1).getCell(7), "第一学年", true,  cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 7, 8);
        // 列9-10:第二学年,跨列2列
        setCellText(table.getRow(1).getCell(8), "第二学年", true,  cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 8, 9);
        // 列11-12:第三学年,跨列2列
        setCellText(table.getRow(1).getCell(9), "第三学年", true,  cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 9, 10);
        // 列13-14:第四学年,跨列2列
        setCellText(table.getRow(1).getCell(10), "第四学年", true,  cellWidth.getCellWidth(2));
        WordUtil.mergeCellsHorizontal(table, 1, 10, 11);

        // ========== 表头第3行(行号2) ==========
        // 列7-14:学期子项(秋、春交替)
        setCellText(table.getRow(2).getCell(7), "秋", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(8), "春", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(9), "秋", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(10), "春", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(11), "秋", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(12), "春", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(13), "秋", true,  cellWidth.getCellWidth(1));
        setCellText(table.getRow(2).getCell(14), "春", true,  cellWidth.getCellWidth(1));

        // 删除第一行第二行多余的列
        // deleteCol(table,0,6,15); // 删除第6列(索引5)
        // deleteCol(table,1,11,15);// 删除第11列(索引10)
    }

    private void practicalProjectCourseHeader(XWPFTable table, CellWidth cellWidth) {
        setCellText(table.getRow(0).getCell(0), "课目模块", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(0).getCell(1), "课目名称", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(0).getCell(2), "修读要求", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(0).getCell(3), "时间安排", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(0).getCell(4), "学期安排", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(0).getCell(5), "备注", true, cellWidth.getCellWidth(1));
    }

    private void trainingSubjectCoursesHeader(XWPFTable table, CellWidth cellWidth) {
        setCellText(table.getRow(0).getCell(0), "专业方向", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(0).getCell(1), "项目层级", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(0).getCell(2), "项目名称", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(0).getCell(3), "修读要求", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(0).getCell(4), "时间安排", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(0).getCell(5), "学期安排", true, cellWidth.getCellWidth(1));
        setCellText(table.getRow(0).getCell(6), "支撑课程或实践训练课目", true, cellWidth.getCellWidth(1));
    }

    /**
     * 删除指定行列的单元格(用于删除空列)
     */
    private void  deleteCol(XWPFTable table, int row, int startCol, int endCol) {
        XWPFTableRow currentRow = table.getRow(row);
        for (int col = endCol; col >= startCol; col--) {
            if (currentRow.getCell(col) != null) {
                currentRow.removeCell(col); // 删除指定列的冗余单元格
            }
        }
    }

    /**
     * 设置表格单元格内容 - 带所有属性
     */
    private void setCellText(XWPFTableCell cell, String text, boolean isBold, String width) {
        if(ObjectUtils.isEmpty(cell)){
            return;
        }
        // 清空单元格默认段落
        while (cell.getParagraphs() != null && !cell.getParagraphs().isEmpty()) {
            cell.removeParagraph(0);
        }
        XWPFParagraph paragraph = cell.addParagraph();
        // 水平居中
        paragraph.setAlignment(ParagraphAlignment.CENTER);
        // 垂直居中
        cell.setVerticalAlignment(XWPFTableCell.XWPFVertAlign.CENTER);
        // 设置单元格宽度
        if (width != null && !width.isEmpty()) {
            cell.setWidth(width);
        }
        // 设置文本
        XWPFRun run = paragraph.createRun();
        run.setFontFamily("宋体");
        run.setText(text);
        run.setFontSize(9);
        if (isBold) {
            run.setBold(true);
        }
    }
    /**
     * 设置课程数据行
     */
    private void setCourseDataRow(XWPFTable table, int rowIndex, TrainingSchemeCourseModel course, int initCell, CountModel countModel, CellWidth cellWidth) {
        XWPFTableRow row = table.getRow(rowIndex);

        // 列1:课程名称
        XWPFTableCell cell1 = row.getCell(initCell);
        WordUtil.setCellText(cell1, course.getName(), false,  cellWidth.getCellWidth(1));

        // 列2:修读要求(B=必修,X=限选,R=任选)
        XWPFTableCell cell2 = row.getCell(initCell+1);
        String attrText = getAttrText(course.getCourseAttr());
        WordUtil.setCellText(cell2, attrText, false,  cellWidth.getCellWidth(1));

        // 列3:考核方式(S=考试,C=考查)
        XWPFTableCell cell3 = row.getCell(initCell+2);
        String assessText = getAssessText(course.getExaMethod());
        WordUtil.setCellText(cell3, assessText, false,  cellWidth.getCellWidth(1));

        // 列4:小计
        XWPFTableCell cell4 = row.getCell(initCell+3);
        WordUtil.setCellText(cell4, course.getHours() != null ? course.getHours().toString() : "", false,  cellWidth.getCellWidth(1));
        countModel.setTotalHours(countModel.getTotalHours()+course.getHours());

        // 列5:讲授
        XWPFTableCell cell5 = row.getCell(initCell+4);
        WordUtil.setCellText(cell5, course.getTeachHours() != null ? course.getTeachHours().toString() : "", false,  cellWidth.getCellWidth(1));
        countModel.setTeachHours(countModel.getTeachHours()+course.getTeachHours());

        // 列6:实践
        XWPFTableCell cell6 = row.getCell(initCell+5);
        WordUtil.setCellText(cell6, course.getPracticeHours() != null ? course.getPracticeHours().toString() : "", false,  cellWidth.getCellWidth(1));
        countModel.setPracticeHours(countModel.getPracticeHours()+course.getPracticeHours());

        // 列7-12:学期安排(在开课学期填写对应学时)
        setTermCheckmarks(row, course,initCell+6,cellWidth,countModel);
    }

//    private void setGenerateCourseDataRow(XWPFTable table, int rowIndex, TrainingSchemeCourseModel course,int initCell) {
//        XWPFTableRow row = table.getRow(rowIndex);
//        //列0:课程模块
//
//        // 列1:课程名称
//        XWPFTableCell cell1 = row.getCell(initCell);
//        WordUtil.setCellText(cell1, course.getName(), false, "1985");
//
//        // 列2:修读要求(B=必修,X=限选,R=任选)
//        XWPFTableCell cell2 = row.getCell(initCell+1);
//        String attrText = getAttrText(course.getCourseAttrName());
//        WordUtil.setCellText(cell2, attrText, false, "1021");
//
//        // 列3:考核方式(S=考试,C=考查)
//        XWPFTableCell cell3 = row.getCell(initCell+2);
//        String assessText = getAssessText(course.getExaMethod());
//        WordUtil.setCellText(cell3, assessText, false, "1021");
//
//        // 列4:小计
//        XWPFTableCell cell4 = row.getCell(initCell+3);
//        WordUtil.setCellText(cell4, course.getHours() != null ? course.getHours().toString() : "", false, "851");
//
//        // 列5:讲授
//        XWPFTableCell cell5 = row.getCell(initCell+4);
//        WordUtil.setCellText(cell5, course.getTeachHours() != null ? course.getTeachHours().toString() : "", false, "851");
//
//        // 列6:实践
//        XWPFTableCell cell6 = row.getCell(initCell+5);
//        WordUtil.setCellText(cell6, course.getPracticeHours() != null ? course.getPracticeHours().toString() : "", false, "851");
//
//        // 列7-12:学期安排(根据semesterSchedule和springAutumn判断是否打勾)
//        setTermCheckmarks(row, course,initCell+6);
//    }

    /**
     * 设置学期安排:在开课学期填写该学期对应的拆分学时(讲授+实践),未开课学期留空
     * 并将各学期学时累加到countModel中,供小计行统计该列总学时
     * 拆分学时优先取course.termHoursMap;若无(数据缺失),则按开课学期数均分课程总学时
     */
    private void setTermCheckmarks(XWPFTableRow row, TrainingSchemeCourseModel course, int initCell, CellWidth cellWidth, CountModel countModel) {
        List<Integer> openTermList = course.getOpenTerm();
        Map<Integer, Double> termHoursMap = course.getTermHoursMap();
        Double totalHours = course.getHours();

        // 默认值:8个学期(第一学年秋季~第四学年春季)均留空
        String[] termValues = {"", "", "", "", "", "", "", ""};
        if (ObjectUtils.isNotEmpty(openTermList)) {
            //过滤出有效的开课学期
            List<Integer> validTerms = openTermList.stream()
                    .filter(Objects::nonNull)
                    .filter(t -> t >= 1 && t <= 8)
                    .collect(Collectors.toList());
            for (Integer openTerm : validTerms) {
                int idx = openTerm - 1;
                //优先使用按学期拆分的学时;若无拆分数据则按开课学期数均分总学时
                Double termHours = (termHoursMap != null) ? termHoursMap.get(openTerm) : null;
                if (termHours == null && totalHours != null && !validTerms.isEmpty()) {
                    termHours = totalHours / validTerms.size();
                }
                if (termHours != null) {
                    termValues[idx] = formatHours(termHours);
                    if (countModel != null) {
                        countModel.addTermHours(idx, termHours);
                    }
                }
            }
        }

        // 设置单元格(8个学期对应列)
        for (int i = 0; i < 8; i++) {
            WordUtil.setCellText(row.getCell(initCell + i), termValues[i], false, cellWidth.getCellWidth(1));
        }
    }

    /**
     * 格式化学时:整数显示为整型,否则保留有效小数
     */
    private String formatHours(Double hours) {
        if (hours == null) {
            return "";
        }
        double v = hours;
        if (v == Math.rint(v) && !Double.isInfinite(v)) {
            return String.valueOf((long) v);
        }
        return new java.math.BigDecimal(v).stripTrailingZeros().toPlainString();
    }

    /**
     * 设置小计行
     *
     * @param dataCell       学时安排(小计/讲授/实践)三列的起始列
     * @param termStartCell  学期安排8列的起始列,用于填写各学期总学时
     */
    private void setTotalRow(XWPFTable table, int rowIndex, CountModel countModel, int dataCell, int termStartCell, CellWidth cellWidth) {
        XWPFTableRow row = table.getRow(rowIndex);

        // 列0:小计
        XWPFTableCell cell0 = row.getCell(0);
        WordUtil.setCellText(cell0, "小  计", true,  cellWidth.getCellWidth(3));
        // 合并列0-3
        WordUtil.mergeCellsHorizontal(table, rowIndex, 0, 1);
        XWPFTableCell cell1 = row.getCell(dataCell);
        WordUtil.setCellText(cell1, countModel.getTotalHours()+"", true,  cellWidth.getCellWidth(1));
        XWPFTableCell cell2 = row.getCell(dataCell+1);
        WordUtil.setCellText(cell2, countModel.getTeachHours()+"", true,  cellWidth.getCellWidth(1));
        XWPFTableCell cell3 = row.getCell(dataCell+2);
        WordUtil.setCellText(cell3, countModel.getPracticeHours()+"", true,  cellWidth.getCellWidth(1));

        // 学期安排各列总学时
        Double[] termHours = countModel.getTermHours();
        for (int i = 0; i < 8; i++) {
            WordUtil.setCellText(row.getCell(termStartCell + i), formatHours(termHours[i]), true, cellWidth.getCellWidth(1));
        }
    }

    /**
     * 获取修读要求文本(B=必修,X=限选,R=任选)
     */
    private String getAttrText(String attrName) {
        if (attrName == null) {
            return "";
        }
        // 根据课程属性名称获取对应代码
        if (attrName.contains(ConstantTrainingScheme.COMPULSORY_COURSE) ) {
            return "B";
        } else if (attrName.contains(ConstantTrainingScheme.ELECTIVE_COURSE) ) {
            return "X";
        } else if (attrName.contains(ConstantTrainingScheme.OPTIONAL_COURSE) ) {
            return "R";
        }
        return attrName;
    }

    /**
     * 获取考核方式文本(S=考试,C=考查)
     */
    private String getAssessText(String exaMethod) {
        if (exaMethod == null) {
            return "";
        }
        // 根据开课学期判断考核方式
        if (exaMethod.contains("1") || exaMethod.contains("S")) {
            return "S";
        } else if (exaMethod.contains("2") || exaMethod.contains("C")) {
            return "C";
        }
        return exaMethod;
    }


}


下一篇: 暂无相关文章
排名:
这家伙很懒,什么都没有留下
经验值
经验值:undefined/undefined
0
文章
0
评论
0
勋章
0
动态
0
粉丝
0
关注
获得勋章: