Browse Source

Merge branch 'master' of http://gogs.gisvg.com/YDM/graduate

Crazy 2 weeks ago
parent
commit
cdf54b3650

+ 7 - 4
yudao-module-system/yudao-module-system-biz/pom.xml

@@ -152,14 +152,17 @@
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-spring-boot-starter-protection</artifactId>
         </dependency>
-
-
         <dependency>
             <groupId>com.itextpdf</groupId>
             <artifactId>itextpdf</artifactId>
-            <version>5.5.13.2</version>
+            <version>5.5.13</version>
+        </dependency>
+        <!-- PDF中文支持 -->
+        <dependency>
+            <groupId>org.apache.pdfbox</groupId>
+            <artifactId>pdfbox</artifactId>
+            <version>2.0.28</version>
         </dependency>
-
     </dependencies>
 
 </project>

+ 460 - 109
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/mail/MailTemplateController.java

@@ -18,34 +18,46 @@ import cn.iocoder.yudao.module.system.service.user.AdminUserService;
 import com.alibaba.excel.EasyExcel;
 import com.alibaba.excel.ExcelWriter;
 import com.alibaba.excel.write.metadata.WriteSheet;
-import com.itextpdf.text.DocumentException;
-import com.itextpdf.text.Paragraph;
-
+import com.itextpdf.text.Document;
 import com.itextpdf.text.pdf.PdfWriter;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
-
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.PDPageContentStream;
+import org.apache.pdfbox.pdmodel.font.PDFont;
+import org.apache.pdfbox.pdmodel.font.PDTrueTypeFont;
+import org.apache.pdfbox.pdmodel.font.PDType0Font;
+import org.apache.pdfbox.pdmodel.font.PDType1Font;
+import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.xwpf.usermodel.*;
-
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTP;
+import org.openxmlformats.schemas.wordprocessingml.x2006.main.STTblWidth;
 import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
+import com.itextpdf.text.*;
+import com.itextpdf.text.pdf.PdfPCell;
+import com.itextpdf.text.pdf.PdfPTable;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
 
 import javax.annotation.Resource;
 import javax.validation.Valid;
 import java.io.*;
-
+import java.math.BigInteger;
 import java.nio.file.Files;
-import java.nio.file.Paths;
-
+import java.time.DayOfWeek;
 import java.time.LocalDate;
 import java.util.*;
-
+import java.util.List;
+import java.util.stream.Collectors;
 
 import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
 import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
-
+import static org.openxmlformats.schemas.wordprocessingml.x2006.main.STVerticalJc.CENTER;
 
 @Tag(name = "管理后台 - 邮件模版")
 @RestController
@@ -65,11 +77,13 @@ public class MailTemplateController {
     @Resource
     private PermissionService permissionService;
 
+    private static final short[] COLUMN_WIDTHS = {20, 20, 20, 20}; // 根据需要设置列宽
+
     @Scheduled(cron = "0 30 8 * * ?") // 每天早上 8:30发送前一天的
     @Operation(summary = "定时发送邮件给导师")
     public void sendMailToTeacherScheduled() {
         try {
-            sendWordMailToTeacher();
+            sendPDFMailToTeacher();
         } catch (IOException e) {
             throw new RuntimeException(e);
         }
@@ -208,7 +222,6 @@ public class MailTemplateController {
                             .append("<br/>");
                 }
 
-
                 if (!errorList.isEmpty()) {
                     templateParams.put("errType","未打卡同学清单");
                     templateParams.put("errList",errorListBuilder);
@@ -242,21 +255,248 @@ public class MailTemplateController {
 
                 document.write(byteArrayOutputStream);
 
-                String outputPath ="output.pdf";
-                // 将 Word 文档转换为 PDF
-                convert(byteArrayOutputStream.toByteArray(), outputPath);
+                Map<String, InputStream> attachments = new HashMap<>();
+                String fileName = String.format("%s考勤信息.docx", (teacher.getDeptId() == null || teacher.getDeptId() == 0 ? "" : teacher.getDeptName()) + yesterday);
+                attachments.put(fileName, new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
+                // 发送邮件,包含附件
+                if (teacher.getEmail() != null) {
+                    mailSendService.sendSingleMailToMemberWithAttachments("1473464808@qq.com", null, "attendance-list-word", templateParams, attachments);
+                }
+                // 每次推送后等待0.1s
+                try {
+                    Thread.sleep(100);  // 0.1s
+                } catch (InterruptedException e) {
+                    throw new RuntimeException(e);
+                }
+            }
+        }
+    }
+    @PostMapping("/sendPDFToTeacher")
+    @Operation(summary = "发送PDF给导师")
+    public void sendPDFMailToTeacher() throws IOException {
+        // 获取导师
+        Set<Long> collegeIdList = permissionService.getUserListByRoleId(113L);
+        List<AdminUserDO> TeacherList = adminUserService.getUserList(collegeIdList);
+
+        StudentAttendancePageReqVO pageReqVO = new StudentAttendancePageReqVO();
+        Map<String, Object> templateParams = new HashMap<>(); // 模板参数设置
+
+        // 获取前一天
+        LocalDate yesterday = LocalDate.now().minusDays(1);
+
+        if (TeacherList != null && !TeacherList.isEmpty()) {
+            for (AdminUserDO teacher : TeacherList) {
+                if (!(teacher.getDeptId() == null || teacher.getDeptId() == 0)) {
+                    pageReqVO.setDeptId(teacher.getDeptId());
+                }
+                if (!(teacher.getDeptId() == null || teacher.getDeptId() == 0)) {
+                    pageReqVO.setDeptId(teacher.getDeptId());
+                }
+                pageReqVO.setDate(yesterday);
+                List<StudentAttendanceSupervisorTemplateVO> normalList = BeanUtils.toBean(
+                        studentAttendanceService.getStudentAttendanceListForTeacher(pageReqVO.setSupervisorId(teacher.getId())),
+                        StudentAttendanceSupervisorTemplateVO.class
+                );
+                List<StudentAttendanceSupervisorTemplateVO> errorList = BeanUtils.toBean(
+                        studentAttendanceService.getStudentAttendanceErrorListForTeacher(pageReqVO.setSupervisorId(teacher.getId())),
+                        StudentAttendanceSupervisorTemplateVO.class
+                );
+                if (normalList.isEmpty() && errorList.isEmpty()) {
+                    if(!adminUserService.isSupervisorNullHasStudent(teacher.getId())){
+                        continue;
+                    }
+                }
+                templateParams.put("teacherName", teacher.getNickname());
+                templateParams.put("date",yesterday.toString());
 
-                FileInputStream pdfInputStream =new FileInputStream(outputPath);
 
+                StringBuilder normalListBuilder = new StringBuilder();
+                for (StudentAttendanceSupervisorTemplateVO attendance : normalList) {
+                    normalListBuilder
+                            .append(" 学生姓名: ").append(attendance.getStudentName())
+                            .append(" 学生学号: ").append(attendance.getUserNumber() != null ? attendance.getUserNumber() : "无")
+                            .append(" 打卡时间: ").append(attendance.getClockInTime() != null ? attendance.getClockInTime() : "未打卡")
+                            .append("<br/>");
+                }
 
+                StringBuilder errorListBuilder = new StringBuilder();
+                for (StudentAttendanceSupervisorTemplateVO attendance : errorList) {
+                    errorListBuilder
+                            .append(" 学生姓名: ").append(attendance.getStudentName())
+                            .append(" 学生学号: ").append(attendance.getUserNumber() != null ? attendance.getUserNumber() : "无")
+                            .append(" 打卡时间: ").append(attendance.getClockInTime() != null ? attendance.getClockInTime() : "未打卡")
+                            .append("<br/>");
+                }
                 Map<String, InputStream> attachments = new HashMap<>();
-                String fileName = String.format("%s考勤信息.pdf", (teacher.getDeptId() == null || teacher.getDeptId() == 0 ? "" : teacher.getDeptName()) + yesterday);
-                attachments.put(fileName, pdfInputStream);
+
+                try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+                     PDDocument document = new PDDocument()) {
+
+                    PDPage page = new PDPage();
+                    document.addPage(page);
+                    // 提前创建 5 个流并存储在列表中
+                    List<PDPageContentStream> contentStreams = new ArrayList<>();
+                    contentStreams.add(new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true));
+                    for (int i = 0; i < 1; i++) {
+                        PDPage newPage = new PDPage();
+                        document.addPage(newPage);
+                        contentStreams.add(new PDPageContentStream(document, newPage, PDPageContentStream.AppendMode.APPEND, true, true));
+                    }
+
+                    PDFont font = PDType0Font.load(document, getClass().getResourceAsStream("/font/STXIHEI.TTF"));
+                // 使用第一个流写标题
+                    PDPageContentStream contentStream = contentStreams.get(0);
+                    // 页面宽度与高度
+                    float pageWidth = page.getMediaBox().getWidth();
+                    float pageHeight = page.getMediaBox().getHeight();
+
+                    // 设置初始 Y 坐标,页面顶部保留边距
+                    float margin = 50;
+                    float yPosition = pageHeight - margin;
+
+                    // 1. 添加标题
+                    String title = "测绘地理信息学院研究生考勤情况日报告";
+                    float titleWidth = font.getStringWidth(title) / 1000 * 16; // 字体宽度 * 字号
+                    float titleX = (pageWidth - titleWidth) / 2; // 居中显示
+                    contentStream.beginText();
+                    contentStream.setFont(font, 16);
+                    contentStream.newLineAtOffset(titleX, yPosition);
+                    contentStream.showText(title);
+                    contentStream.endText();
+                    yPosition -= 100; // 下移一行
+
+
+                    // 2. 添加老师称呼
+                    String teacherGreeting = "尊敬的" + teacher.getNickname() + "导师:";
+                    contentStream.beginText();
+                    contentStream.setFont(font, 14);
+                    contentStream.newLineAtOffset(50, yPosition); // 左对齐,X 偏移量 100
+                    contentStream.showText(teacherGreeting);
+                    contentStream.endText();
+                    yPosition -= 50; // 下移一行
+
+                    // 3. 添加日期信息段落
+                    String dateInfoText = "您所指导的研究生 " + yesterday.getYear() + " 年 " + yesterday.getMonthValue() + " 月 " + yesterday.getDayOfMonth() + " 日 考勤情况如下:";
+                    contentStream.beginText();
+                    contentStream.setFont(font, 14);
+                    contentStream.newLineAtOffset(50, yPosition); // 左对齐,X 偏移量 60
+                    contentStream.showText(dateInfoText);
+                    contentStream.endText();
+                    yPosition -= 30; // 下移一行
+
+                    String errlistName = "打卡异常同学清单";
+
+                    // 计算标题宽度以居中显示
+                    float errlistNameWidth = font.getStringWidth(errlistName) / 1000 * 16;
+                    float errlistNameX = (pageWidth - errlistNameWidth) / 2;
+
+
+                    contentStream.beginText();
+                    contentStream.setFont(font, 16); // 设置字体大小为 16
+                    contentStream.newLineAtOffset(errlistNameX, yPosition);
+                    contentStream.showText(errlistName);
+                    contentStream.endText();
+                    yPosition -= 30; // 下移高度
+
+                    if (!errorList.isEmpty()) {
+                        templateParams.put("errType","未打卡同学清单");
+                        templateParams.put("errList",errorListBuilder);
+                        yPosition = generatePDFWithTableUsingStreamPool(contentStreams, document, page, errorList, yPosition, font, false);
+                    } else {
+                        templateParams.put("errType","未打卡同学清单!");
+                        templateParams.put("errList","无");
+                        writeText(contentStream, "无", 100, yPosition, font);
+                        yPosition -= 30; // 下移高度
+                    }
+                    System.out.println(teacher.getNickname() + "我没进来"+ currentStreamIndex + contentStreams.size());
+
+                    if (yPosition < margin + 40) {
+                        currentStreamIndex++;
+                        // 如果流池已经用尽,则动态创建新的流
+                        if (currentStreamIndex >= contentStreams.size()) {
+                            System.out.println(teacher.getNickname() + "我进来了"+ currentStreamIndex + contentStreams.size());
+                            PDPage newPage = new PDPage();
+                            document.addPage(newPage);
+                            PDPageContentStream newStream = new PDPageContentStream(document, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
+                            contentStreams.add(newStream);
+                        }
+                        // 切换到下一个流
+                        contentStream = contentStreams.get(currentStreamIndex);
+                        yPosition = pageHeight - margin;
+                    }
+
+                    contentStream = contentStreams.get(currentStreamIndex);
+
+                    String listName = "正常打卡同学清单";
+
+                    // 计算标题宽度以居中显示
+                    float listNameWidth = font.getStringWidth(listName) / 1000 * 16;
+                    float listNameX = (pageWidth - listNameWidth) / 2;
+
+
+                    contentStream.beginText();
+                    contentStream.setFont(font, 16); // 设置字体大小为 16
+                    contentStream.newLineAtOffset(listNameX, yPosition);
+                    contentStream.showText(listName);
+                    contentStream.endText();
+
+                    yPosition -= 20; // 下移高度
+
+                    // 添加正常打卡学生列表
+                    if (!normalList.isEmpty()) {
+                        templateParams.put("normaltype","正常打卡同学清单");
+                        templateParams.put("normalList",normalListBuilder);
+                        yPosition = generatePDFWithTableUsingStreamPool(contentStreams, document, page, normalList, yPosition, font, true);
+                    } else {
+                        templateParams.put("normaltype","正常打卡同学清单");
+                        templateParams.put("normalList","无");
+                        writeText(contentStream, "无", 100, yPosition, font);
+                        yPosition -= 30; // 下移高度
+                    }
+
+
+                    // 添加结尾
+                    String footer = "测绘地理信息学院研究生管理委员会";
+
+                    if (yPosition < margin + 40) {
+                        currentStreamIndex++;
+                        // 如果流池已经用尽,则动态创建新的流
+                        if (currentStreamIndex >= contentStreams.size()) {
+                            PDPage newPage = new PDPage();
+                            document.addPage(newPage);
+                            PDPageContentStream newStream = new PDPageContentStream(document, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
+                            contentStreams.add(newStream);
+                        }
+                        // 切换到下一个流
+                        contentStream = contentStreams.get(currentStreamIndex);
+                        yPosition = pageHeight - margin;
+                    }
+                    contentStream = contentStreams.get(currentStreamIndex);
+                    // 写结尾
+                    writeText(contentStream, footer, 350, yPosition, font);
+
+                    // 关闭所有流
+                    for (PDPageContentStream stream : contentStreams) {
+                        if (stream != null) {
+                            stream.close();
+                        }
+                    }
+
+                    // 保存文档到输出流
+                    document.save(byteArrayOutputStream);
+                    // 保存文档到输出流
+                    String fileName = String.format("%s考勤信息.pdf", (teacher.getDeptId() == null || teacher.getDeptId() == 0 ? "" : teacher.getDeptName()) + yesterday);
+                    attachments.put(fileName, new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
+                } catch (IOException e) {
+                    e.printStackTrace();
+                    throw new RuntimeException("生成 PDF 时发生错误", e);
+                }
                 // 发送邮件,包含附件
                 if (teacher.getEmail() != null) {
-                    mailSendService.sendSingleMailToMemberWithAttachments("1157853982@qq.com", null, "attendance-list-word", templateParams, attachments);
+                    mailSendService.sendSingleMailToMemberWithAttachments(teacher.getEmail(), null, "attendance-list-word", templateParams, attachments);
+                    currentStreamIndex = 0;
                 }
-                // 每次推送后等待0.1s
+                // 每次推送后等待0.1
                 try {
                     Thread.sleep(100);  // 0.1s
                 } catch (InterruptedException e) {
@@ -266,6 +506,20 @@ public class MailTemplateController {
         }
     }
 
+    private void writeText(PDPageContentStream contentStream, String text, float x, float y, PDFont font) throws IOException {
+        if (contentStream == null) {
+            throw new IllegalStateException("ContentStream is null!");
+        }
+        if (font == null) {
+            throw new IllegalStateException("Font is null!");
+        }
+
+        contentStream.beginText();
+        contentStream.setFont(font, 12); // 默认字体大小
+        contentStream.newLineAtOffset(x, y);
+        contentStream.showText(text);
+        contentStream.endText();
+    }
     @PostMapping("/sendExcelToCollege")
     @Operation(summary = "发送Excel给学院")
     public void sendExcelMailToCollege() throws IOException {
@@ -576,17 +830,11 @@ public class MailTemplateController {
 
                 document.write(byteArrayOutputStream);
 
-                String outputPath ="output.pdf";
-                // 将 Word 文档转换为 PDF
-                convert(byteArrayOutputStream.toByteArray(), outputPath);
-
-                FileInputStream pdfInputStream =new FileInputStream(outputPath);
-
                 // 创建附件并将文件保存
                 Map<String, InputStream> attachments = new HashMap<>();
 //            String fileName = String.format("%s考勤信息.docx", (teacher.getDeptId()==null||teacher.getDeptId()==0?"":teacher.getDeptName())+yesterday);
                 String fileName = String.format("%s考勤信息.docx", yesterday);
-                attachments.put(fileName, pdfInputStream);
+                attachments.put(fileName, new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
                 // 发送邮件
                 if (teacher.getEmail() != null) {
                     mailSendService.sendSingleMailToMemberWithAttachments(email, null, "attendance-list-excel", templateParams, attachments);
@@ -699,6 +947,7 @@ public class MailTemplateController {
         paragraph.setSpacingAfter(spacingAfter);
     }
 
+
     // 方法: 创建表格并填充数据
     private void createAttendanceTable(XWPFDocument document, List<StudentAttendanceSupervisorTemplateVO> dataList, boolean isNormalList) {
         XWPFTable table = document.createTable();
@@ -746,102 +995,204 @@ public class MailTemplateController {
         }
     }
 
-
-    /**
-     * 生成用于展示的考勤报告文字内容
-     *
-     * @param teacher    导师信息
-     * @param normalList 正常打卡学生列表
-     * @param errorList  未打卡学生列表
-     * @param date       报告日期
-     * @return 生成的文字内容
-     */
-    public String generateAttendanceReportText(AdminUserDO teacher,
-                                               List<StudentAttendanceSupervisorTemplateVO> normalList,
-                                               List<StudentAttendanceSupervisorTemplateVO> errorList,
-                                               LocalDate date) {
-        StringBuilder report = new StringBuilder();
-
-        // 标题
-        report.append("测绘地理信息学院研究生考勤情况日报告").append("\n\n");
-
-        // 问候语
-        report.append("尊敬的").append(teacher.getNickname()).append("老师:").append("\n\n");
-
-        // 时间信息
-        report.append("         您所指导的研究生 ")
-                .append(date.getYear()).append(" 年 ")
-                .append(date.getMonthValue()).append(" 月 ")
-                .append(date.getDayOfMonth()).append(" 日考勤情况如下:").append("\n\n");
-
-        // 未打卡学生清单
-        if (errorList != null && !errorList.isEmpty()) {
-            report.append("未打卡同学清单").append("\n");
-            report.append(generateAttendanceTable(errorList, false)).append("\n");
-            report.append("特此通知,请您及时检查学生在校情况。").append("\n\n");
-            report.append("测绘地理信息学院研究生管理委员会").append("\n");
-            report.append(date.toString()).append("\n\n");
-        } else {
-            report.append("您所指导的所有研究生今日已全部打卡!").append("\n\n");
-            report.append("测绘地理信息学院研究生管理委员会").append("\n");
-            report.append(date.toString()).append("\n\n");
+    private int currentStreamIndex = 0; // 用于记录当前使用的流索引
+    private float generatePDFWithTableUsingStreamPool(List<PDPageContentStream> contentStreams, PDDocument document, PDPage page,
+                                                      List<StudentAttendanceSupervisorTemplateVO> dataList,
+                                                      float yPosition, PDFont font, boolean isNormal) throws IOException {
+        float margin = 50;
+        float tableWidth = page.getMediaBox().getWidth() - 2 * margin; // 表格总宽度
+        float rowHeight = 20; // 每行高度
+        float yStartNewPage = page.getMediaBox().getHeight() - 50; // 新页面顶部位置
+        float cellMargin = 5; // 单元格内边距
+
+        float[] colWidths = {40, 100, 100, 100, 200}; // 列宽
+        String[] headers = {
+                "序号", "学生姓名", "学生学号", "实验室房号", isNormal ? "打卡时间" : "备注"
+        };
+
+        // 当前使用的流
+        PDPageContentStream contentStream = contentStreams.get(currentStreamIndex);
+
+        // 绘制表头
+        for (int i = 0; i < headers.length; i++) {
+            float cellX = margin + getColumnStartX(i, colWidths);
+            drawCell(contentStream, cellX, yPosition, colWidths[i], rowHeight, headers[i], font, true);
         }
+        yPosition -= rowHeight; // 下移一行
+
+
+        // 绘制表格内容
+        int index = 1;
+        for (StudentAttendanceSupervisorTemplateVO data : dataList) {
+            System.out.println( "生成表格中!!"+ currentStreamIndex + contentStreams.size());
+
+            if (yPosition < margin + rowHeight) {
+                currentStreamIndex++;
+                 // 如果流池已经用尽,则动态创建新的流
+                if (currentStreamIndex >= contentStreams.size()) {
+                    PDPage newPage = new PDPage();
+                    document.addPage(newPage);
+                    PDPageContentStream newStream = new PDPageContentStream(document, newPage, PDPageContentStream.AppendMode.APPEND, true, true);
+                    contentStreams.add(newStream);
+                }
+                 // 切换到下一个流
+                contentStream = contentStreams.get(currentStreamIndex);
+                yPosition = yStartNewPage;
+
+                for (int i = 0; i < headers.length; i++) {
+                    float cellX = margin + getColumnStartX(i, colWidths);
+                    drawCell(contentStream, cellX, yPosition, colWidths[i], rowHeight, headers[i], font, true);
+                }
 
-        // 正常打卡学生清单
-        report.append("正常打卡同学清单").append("\n");
-        report.append(generateAttendanceTable(normalList, true));
+                yPosition -= rowHeight; // 下移一行
+            }
+
+            String[] rowData = {
+                    String.valueOf(index),
+                    data.getStudentName(),
+                    data.getUserNumber() != null ? data.getUserNumber() : "无",
+                    data.getDeptName() != null ? data.getDeptName() : "无",
+                    isNormal
+                            ? (data.getClockInTime() != null ? data.getClockInTime().toString() : "无")
+                            : (data.getRemark() != null ? data.getRemark() : "无")
+            };
+
+            for (int i = 0; i < rowData.length; i++) {
+                float cellX = margin + getColumnStartX(i, colWidths);
+                drawCell(contentStream, cellX, yPosition, colWidths[i], rowHeight, rowData[i], font, false);
+            }
 
-        return report.toString();
+            yPosition -= rowHeight; // 下移一行
+            index++;
+        }
+        yPosition -= 30; // 下移一行
+        return yPosition;
     }
 
-    /**
-     * 生成考勤表格内容
-     *
-     * @param dataList     学生考勤数据列表
-     * @param isNormalList 是否为正常打卡名单
-     * @return 表格文字内容
-     */
-    private String generateAttendanceTable(List<StudentAttendanceSupervisorTemplateVO> dataList, boolean isNormalList) {
-        StringBuilder table = new StringBuilder();
+    private float getColumnStartX(int columnIndex, float[] colWidths) {
+        float x = 0;
+        for (int i = 0; i < columnIndex; i++) {
+            x += colWidths[i];
+        }
+        return x;
+    }
+    private void drawCell(PDPageContentStream contentStream, float x, float y, float width, float height,
+                          String text, PDFont font, boolean isHeader) throws IOException {
 
-        // 表头
-        table.append(String.format("%-5s%-10s%-15s%-15s%-15s%n", "序号", "学生姓名", "学生学号", "所在实验室房号", isNormalList ? "打卡时间" : "备注"));
+        if (contentStream == null) {
+            throw new IllegalStateException("contentStream is null in drawCell method.");
+        }
 
-        // 填充数据
-        int sequenceNumber = 1;
-        for (StudentAttendanceSupervisorTemplateVO vo : dataList) {
-            table.append(String.format("%-5d%-10s%-15s%-15s%-15s%n",
-                    sequenceNumber++, vo.getStudentName(), vo.getUserNumber(), vo.getDeptName(),
-                    isNormalList ? vo.getClockInTime() : vo.getRemark()));
+        if (font == null) {
+            throw new IllegalStateException("font is null in drawCell method.");
         }
 
-        return table.toString();
+        // 绘制单元格边框
+        contentStream.addRect(x, y - height/2, width, height);
+        contentStream.stroke();
+
+        // 设置字体(原有字体)
+        contentStream.setFont(font, 12); // 保持字体大小为 12
+
+        // 绘制文本
+        float textX = x + 5; // 左侧留 5 单元格内边距
+        float textY = y - height / 2 + 5; // 高度中心对齐,并略微向下调整
 
+        contentStream.beginText();
+        contentStream.newLineAtOffset(textX, textY);
+
+        // 如果是表头,设置加粗效果(PDType0Font 不支持加粗,需通过更换字体实现)
+        if (isHeader) {
+            contentStream.setFont(font, 14); // 加大字体模拟加粗
+        }
+
+        // 检查文字宽度,进行动态换行处理
+        float textWidth = getStringWidth(font, 12, text); // 计算文字宽度
+        if (textWidth > width - 10) { // 如果文字宽度超出单元格宽度
+            String[] words = text.split(" "); // 按空格拆分单词
+            StringBuilder line = new StringBuilder();
+            for (String word : words) {
+                if (getStringWidth(font, 12, line + word) > width - 10) {
+                    // 当前行文字超出宽度,写入并换行
+                    contentStream.showText(line.toString());
+                    contentStream.newLineAtOffset(0, -12); // 换行(12 是字体高度)
+                    line = new StringBuilder();
+                }
+                line.append(word).append(" ");
+            }
+            // 写入剩余文字
+            if (line.length() != 0) {
+                contentStream.showText(line.toString().trim());
+            }
+        } else {
+            contentStream.showText(text); // 正常绘制文字
+        }
+
+        contentStream.endText();
     }
 
-    public static void convert(byte[] wordBytes, String outputFilePath) {
+
+    // 获取文字宽度的工具方法
+    private float getStringWidth(PDFont font, float fontSize, String text) throws IOException {
+        return font.getStringWidth(text) / 1000 * fontSize;
+    }
+
+    @GetMapping("/generatePdf")
+    public void generatePdf() {
+        PDDocument document = new PDDocument();
+// 创建 ByteArrayOutputStream 用来存储 PDF 文件数据
+        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
+
+        PDPage page = new PDPage();
+        document.addPage(page);
+
         try {
-            // 使用 ByteArrayInputStream 读取 Word 文档的字节数据
-            ByteArrayInputStream bis = new ByteArrayInputStream(wordBytes);
-            XWPFDocument document = new XWPFDocument(bis);
-
-            // 创建 PDF 文档
-            com.itextpdf.text.Document pdfDocument = (com.itextpdf.text.Document) new com.itextpdf.text.Document();
-            PdfWriter.getInstance((com.itextpdf.text.Document) pdfDocument, Files.newOutputStream(Paths.get(outputFilePath)));
-            pdfDocument.open();
-
-            // 将 Word 文档内容写入 PDF 文档
-            List<XWPFParagraph> paragraphs = document.getParagraphs();
-            for (XWPFParagraph paragraph : paragraphs) {
-                pdfDocument.add(new Paragraph(paragraph.getText()));
+
+            PDFont font = PDType0Font.load(document, getClass().getResourceAsStream("/font/STXIHEI.TTF"));
+            // 创建第一页
+            PDPage page1 = new PDPage();
+            document.addPage(page1);
+
+            // 添加内容到第一页
+            try (PDPageContentStream contentStream1 = new PDPageContentStream(document, page1)) {
+                contentStream1.beginText();
+                contentStream1.setFont(font, 12);
+                contentStream1.newLineAtOffset(25, 750);
+                contentStream1.showText("This is the first page.");
+                contentStream1.endText();
+            }
+
+            // 创建第二页
+            PDPage page2 = new PDPage();
+            document.addPage(page2);
+
+            // 添加内容到第二页
+            try (PDPageContentStream contentStream2 = new PDPageContentStream(document, page2)) {
+                contentStream2.beginText();
+                contentStream2.setFont(font, 12);
+                contentStream2.newLineAtOffset(25, 750);
+                contentStream2.showText("This is the second page.");
+                contentStream2.endText();
+            }
+            Map<String, Object> templateParams = new HashMap<>();//模板参数设置
+            // 附件
+            Map<String, InputStream> attachments = new HashMap<>();
+            // 创建附件
+
+            document.save(byteArrayOutputStream);
+            document.close();
+
+            attachments.put( "_" + "123.pdf", new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
+            mailSendService.sendSingleMailToMemberWithAttachments("1473464808@qq.com", null, "attendance-list-excel", templateParams, attachments);
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            try {
+                document.close();
+            } catch (IOException e) {
+                e.printStackTrace();
             }
-            // 关闭文档
-            pdfDocument.close();
-            bis.close();
-            System.out.println("Word转PDF成功!");
-        } catch (IOException | DocumentException e) {
-            System.out.println("Word转PDF失败:" + e.getMessage());
         }
     }
-
 }

+ 251 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/PdfUtils.java

@@ -0,0 +1,251 @@
+package cn.iocoder.yudao.module.system.util;
+import com.itextpdf.text.*;
+import com.itextpdf.text.pdf.BaseFont;
+import com.itextpdf.text.pdf.PdfPCell;
+import com.itextpdf.text.pdf.PdfPTable;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @version V1.0
+ * @Description:生成PDF文件
+ * @Author: Mr.LJie
+ * @Date: 2021/11/30 14:17
+ */
+public class PdfUtils {
+    /**
+     * 固定高
+     */
+    public static float fixedHeight = 27f;
+    /**
+     * 跨行
+     */
+    public static int rowSpan = 0;
+    /**
+     * 跨列
+     */
+    public static int colSpan = 0;
+    /**
+     * 左边距
+     */
+    public static float marginLeft = 20;
+    /**
+     * 右边距
+     */
+    public static float marginRight = 20;
+    /**
+     * 上边距
+     */
+    public static float marginTop = 20;
+    /**
+     * 下边距
+     */
+    public static float marginBottom = 20;
+
+    /***
+     * @Description :创建A4纵向文件
+     * @param
+     * @return com.itextpdf.text.Document
+     * @Author Mr.Jie
+     * @Date 2021/11/30 14:25
+     */
+    public static Document createPortraitDocument() {
+        //生成pdf
+        Document document = new Document();
+        // 页面大小
+        Rectangle rectangle = new Rectangle(PageSize.A4);
+        // 页面背景颜色
+        rectangle.setBackgroundColor(BaseColor.WHITE);
+        document.setPageSize(rectangle);
+        // 页边距 左,右,上,下
+        document.setMargins(marginLeft, marginRight, marginTop, marginBottom);
+        return document;
+    }
+
+    /***
+     * @Description :创建A4横向文件
+     * @param
+     * @return com.itextpdf.text.Document
+     * @Author Mr.Jie
+     * @Date 2021/11/30 15:42
+     */
+    public static Document createTransverseDocument() {
+        //生成pdf
+        Document document = new Document();
+        // 页面大小
+        Rectangle rectangle = new Rectangle(new RectangleReadOnly(842F, 595F));
+        // 页面背景颜色
+        rectangle.setBackgroundColor(BaseColor.WHITE);
+        document.setPageSize(rectangle);
+        // 页边距 左,右,上,下
+        document.setMargins(marginLeft, marginRight, marginTop, marginBottom);
+
+        return document;
+    }
+
+    /**
+     * @param text 段落内容
+     * @param font 字体设置
+     * @return com.itextpdf.text.Paragraph
+     * @Description :创建段落内容
+     * @Author Mr.Jie
+     * @Date 2021/11/30 14:59
+     */
+    public static Paragraph createParagraph(String text, Font font) {
+        Paragraph elements = new Paragraph(text, font);
+        elements.setSpacingBefore(5);
+        elements.setSpacingAfter(5);
+        return elements;
+    }
+
+
+    /***
+     * @Description :创建字体样式
+     * @param fontNumber    字号
+     * @param fontSize      字体大小
+     * @param fontColor     字体颜色
+     * @return com.itextpdf.text.Font
+     * @Author Mr.Jie
+     * @Date 2021/11/30 14:57
+     */
+    public static Font createFont(int fontNumber, int fontSize, BaseColor fontColor) {
+        //中文字体 ----不然中文会乱码
+        BaseFont bf = null;
+        try {
+            bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
+
+            return new Font(bf, fontNumber, fontSize, fontColor);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return new Font(bf, Font.DEFAULTSIZE, Font.NORMAL, BaseColor.BLACK);
+    }
+
+    /***
+     * @Description :隐藏表格边框线
+     * @param cell  单元格
+     * @return void
+     * @Author Mr.Jie
+     * @Date 2021/11/30 14:57
+     */
+    public static void disableBorderSide(PdfPCell cell) {
+        if (cell != null) {
+            cell.disableBorderSide(1);
+            cell.disableBorderSide(2);
+            cell.disableBorderSide(4);
+            cell.disableBorderSide(8);
+        }
+    }
+
+
+    /***
+     * @Description :创建居中得单元格
+     * @param
+     * @return com.itextpdf.text.pdf.PdfPCell
+     * @Author Mr.Jie
+     * @Date 2021/11/30 14:57
+     */
+    public static PdfPCell createCenterPdfPCell() {
+        PdfPCell cell = new PdfPCell();
+        cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
+        cell.setHorizontalAlignment(Element.ALIGN_CENTER);
+        cell.setFixedHeight(fixedHeight);
+        return cell;
+    }
+
+    /***
+     * @Description :创建指定文字得单元格
+     * @param text      文字内容
+     * @param rowSpan   跨行合并单元格
+     * @param colSpan   跨列合并单元格
+     * @param font      字体
+     * @return com.itextpdf.text.pdf.PdfPCell
+     * @Author Mr.Jie
+     * @Date 2021/11/30 14:50
+     */
+    public static PdfPCell createCenterPdfPCell(String text, int rowSpan, int colSpan, Font font) {
+        PdfPCell cell = new PdfPCell(new Paragraph(text, font));
+        cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
+        cell.setHorizontalAlignment(Element.ALIGN_LEFT);
+        cell.setFixedHeight(fixedHeight);
+        cell.setRowspan(rowSpan);
+        cell.setColspan(colSpan);
+        return cell;
+    }
+
+    /***
+     * @Description :表格列数
+     * @param len
+     * @return com.itextpdf.text.pdf.PdfPTable
+     * @Author Mr.Jie
+     * @Date 2021/11/30 14:52
+     */
+    public static PdfPTable createPdfPTable(int len) {
+        PdfPTable pdfPTable = new PdfPTable(len);
+        //宽度100%填充
+        pdfPTable.setWidthPercentage(100);
+        return pdfPTable;
+    }
+
+    /***
+     * @Description :创建表头
+     * @param document
+     * @param headers
+     * @param font
+     * @return void
+     * @Author Mr.Jie
+     * @Date 2021/11/30 15:27
+     */
+    public static void createHeader(Document document, PdfPTable table, String[] headers, Font font) throws DocumentException {
+        for (int i = 0; i < headers.length; i++) {
+            //默认不跨行、跨列
+            PdfPCell cell = PdfUtils.createCenterPdfPCell(headers[i], rowSpan, rowSpan, font);
+            //居中显示
+            cell.setHorizontalAlignment(Element.ALIGN_CENTER);
+            cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
+            table.addCell(cell);
+        }
+        document.add(table);
+    }
+
+    /***
+     * @Description :数据处理
+     * @param document
+     * @param table
+     * @param list
+     * @param font
+     * @return void
+     * @Author Mr.Jie
+     * @Date 2021/11/30 15:32
+     */
+    public static void dataProcessing(Document document, PdfPTable table, List<Map<String, Object>> list, Font font, int columns) throws DocumentException {
+        float totalTableHeight = 0;
+        float pageHeight = PageSize.A4.getHeight();
+        float bottomMargin = document.bottomMargin();
+        float topMargin = document.topMargin();
+        for (Map<String, Object> map : list) {
+            // 累加表格高度
+            totalTableHeight += fixedHeight;
+            for (Map.Entry<String, Object> entry : map.entrySet()) {
+                Object value = entry.getValue();
+                //默认不跨行、跨列
+                PdfPCell cell = PdfUtils.createCenterPdfPCell(value.toString(), rowSpan, rowSpan, font);
+                // 将单元格添加到表格
+                table.addCell(cell);
+            }
+            // 添加行信息
+            document.add(table);
+            // 检查是否超出一页容量
+            if (totalTableHeight > (pageHeight - bottomMargin - topMargin)) {
+                // 如果超出后重置
+                totalTableHeight = 0;
+                // 开始新的一页
+                document.newPage();
+                // 创建表格
+                table = createPdfPTable(columns);
+            }
+        }
+    }
+}

+ 23 - 23
yudao-server/src/main/resources/application-local.yaml

@@ -46,8 +46,8 @@ spring:
       primary: master
       datasource:
         master:
-          url: jdbc:mysql://172.16.59.50:3306/graduate?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
-#          url: jdbc:mysql://127.0.0.1:3306/graduate?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
+#          url: jdbc:mysql://172.16.59.50:3306/graduate?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
+          url: jdbc:mysql://127.0.0.1:3306/graduate?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true # MySQL Connector/J 8.X 连接的示例
           #          url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai # MySQL Connector/J 5.X 连接的示例
           #          url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
           #          url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
@@ -71,8 +71,8 @@ spring:
 
   # Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
   redis:
-    host: 172.16.59.50  # 地址
-#    host: 127.0.0.1  # 地址
+#    host: 172.16.59.50  # 地址
+    host: 127.0.0.1  # 地址
     port: 6379 # 端口
     database: 2 # 数据库索引
     password: 123456 # 密码,建议生产环境开启
@@ -159,26 +159,26 @@ logging:
   file:
     name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
   level:
-    root: OFF
+#    root: OFF
     # 配置自己写的 MyBatis Mapper 打印日志
-#    cn.iocoder.yudao.module.bpm.dal.mysql: debug
-#    cn.iocoder.yudao.module.infra.dal.mysql: debug
-#    cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiErrorLogMapper: INFO # 配置 ApiErrorLogMapper 的日志级别为 info,避免和 GlobalExceptionHandler 重复打印
-#    cn.iocoder.yudao.module.infra.dal.mysql.job.JobLogMapper: INFO # 配置 JobLogMapper 的日志级别为 info
-#    cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper: INFO # 配置 FileConfigMapper 的日志级别为 info
-#    cn.iocoder.yudao.module.pay.dal.mysql: debug
-#    cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskMapper: INFO # 配置 PayNotifyTaskMapper 的日志级别为 info
-#    cn.iocoder.yudao.module.system.dal.mysql: debug
-#    cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper: INFO # 配置 SmsChannelMapper 的日志级别为 info
-#    cn.iocoder.yudao.module.tool.dal.mysql: debug
-#    cn.iocoder.yudao.module.member.dal.mysql: debug
-#    cn.iocoder.yudao.module.trade.dal.mysql: debug
-#    cn.iocoder.yudao.module.promotion.dal.mysql: debug
-#    cn.iocoder.yudao.module.statistics.dal.mysql: debug
-#    cn.iocoder.yudao.module.crm.dal.mysql: debug
-#    cn.iocoder.yudao.module.erp.dal.mysql: debug
-#    cn.iocoder.yudao.module.ai.dal.mysql: debug
-#    org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR
+    cn.iocoder.yudao.module.bpm.dal.mysql: debug
+    cn.iocoder.yudao.module.infra.dal.mysql: debug
+    cn.iocoder.yudao.module.infra.dal.mysql.logger.ApiErrorLogMapper: INFO # 配置 ApiErrorLogMapper 的日志级别为 info,避免和 GlobalExceptionHandler 重复打印
+    cn.iocoder.yudao.module.infra.dal.mysql.job.JobLogMapper: INFO # 配置 JobLogMapper 的日志级别为 info
+    cn.iocoder.yudao.module.infra.dal.mysql.file.FileConfigMapper: INFO # 配置 FileConfigMapper 的日志级别为 info
+    cn.iocoder.yudao.module.pay.dal.mysql: debug
+    cn.iocoder.yudao.module.pay.dal.mysql.notify.PayNotifyTaskMapper: INFO # 配置 PayNotifyTaskMapper 的日志级别为 info
+    cn.iocoder.yudao.module.system.dal.mysql: debug
+    cn.iocoder.yudao.module.system.dal.mysql.sms.SmsChannelMapper: INFO # 配置 SmsChannelMapper 的日志级别为 info
+    cn.iocoder.yudao.module.tool.dal.mysql: debug
+    cn.iocoder.yudao.module.member.dal.mysql: debug
+    cn.iocoder.yudao.module.trade.dal.mysql: debug
+    cn.iocoder.yudao.module.promotion.dal.mysql: debug
+    cn.iocoder.yudao.module.statistics.dal.mysql: debug
+    cn.iocoder.yudao.module.crm.dal.mysql: debug
+    cn.iocoder.yudao.module.erp.dal.mysql: debug
+    cn.iocoder.yudao.module.ai.dal.mysql: debug
+    org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR
     # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示
 
 debug: false