|
|
@@ -1,11 +1,25 @@
|
|
|
package org.dromara.rs.service.impl;
|
|
|
|
|
|
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.itextpdf.text.Chunk;
|
|
|
+import com.itextpdf.text.Document;
|
|
|
+import com.itextpdf.text.DocumentException;
|
|
|
+import com.itextpdf.text.Element;
|
|
|
+import com.itextpdf.text.Font;
|
|
|
+import com.itextpdf.text.Image;
|
|
|
+import com.itextpdf.text.PageSize;
|
|
|
+import com.itextpdf.text.Paragraph;
|
|
|
+import com.itextpdf.text.Phrase;
|
|
|
+import com.itextpdf.text.pdf.BaseFont;
|
|
|
+import com.itextpdf.text.pdf.PdfPCell;
|
|
|
+import com.itextpdf.text.pdf.PdfPTable;
|
|
|
+import com.itextpdf.text.pdf.PdfWriter;
|
|
|
+import jakarta.servlet.http.HttpServletResponse;
|
|
|
import lombok.RequiredArgsConstructor;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.dromara.common.core.exception.ServiceException;
|
|
|
import org.dromara.common.core.utils.StringUtils;
|
|
|
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
|
|
import org.dromara.common.satoken.utils.LoginHelper;
|
|
|
@@ -22,10 +36,18 @@ import org.dromara.rs.service.IRsOverseasApprovalMonitorService;
|
|
|
import org.dromara.rs.service.IRsOverseasApprovalService;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
+import java.io.IOException;
|
|
|
+import java.io.OutputStream;
|
|
|
+import java.net.URLEncoder;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
import java.time.LocalDate;
|
|
|
import java.time.ZoneId;
|
|
|
import java.time.temporal.ChronoUnit;
|
|
|
-import java.util.*;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
/**
|
|
|
@@ -46,6 +68,7 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
|
|
|
|
|
/**
|
|
|
* 查询审批中的申请列表(待审核状态)
|
|
|
+ * 🔴 查询所有待审批申请(不限制申请人)
|
|
|
*/
|
|
|
@Override
|
|
|
public TableDataInfo<RsOverseasApprovalMonitorVo> queryPendingList(RsOverseasApprovalMonitorQuery query) {
|
|
|
@@ -54,6 +77,7 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
|
|
// 构建查询条件
|
|
|
QueryWrapper<RsOverseasApplication> wrapper = buildQueryWrapper(query);
|
|
|
wrapper.eq("status", "0"); // 只查询待审核状态
|
|
|
+ // 🔴 不再限制申请人,拥有权限的用户可以看到所有数据
|
|
|
|
|
|
// 分页查询
|
|
|
Page<RsOverseasApplication> page = applicationMapper.selectPage(
|
|
|
@@ -70,11 +94,13 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
|
|
resultPage.setRecords(voList);
|
|
|
resultPage.setTotal(page.getTotal());
|
|
|
|
|
|
+ log.info("✅ 查询到 {} 条待审批申请", voList.size());
|
|
|
return TableDataInfo.build(resultPage);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 查询已完成审批的申请列表(已通过状态)
|
|
|
+ * 🔴 查询所有已完成审批申请(不限制申请人)
|
|
|
*/
|
|
|
@Override
|
|
|
public TableDataInfo<RsOverseasApprovalMonitorVo> queryCompletedList(RsOverseasApprovalMonitorQuery query) {
|
|
|
@@ -83,6 +109,7 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
|
|
// 构建查询条件
|
|
|
QueryWrapper<RsOverseasApplication> wrapper = buildQueryWrapper(query);
|
|
|
wrapper.eq("status", "1"); // 只查询已通过状态
|
|
|
+ // 🔴 不再限制申请人,拥有权限的用户可以看到所有数据
|
|
|
|
|
|
// 分页查询
|
|
|
Page<RsOverseasApplication> page = applicationMapper.selectPage(
|
|
|
@@ -99,6 +126,7 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
|
|
resultPage.setRecords(voList);
|
|
|
resultPage.setTotal(page.getTotal());
|
|
|
|
|
|
+ log.info("✅ 查询到 {} 条已完成审批申请", voList.size());
|
|
|
return TableDataInfo.build(resultPage);
|
|
|
}
|
|
|
|
|
|
@@ -143,11 +171,13 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
|
|
.eq("tenant_id", tenantId));
|
|
|
statistics.put("pendingCount", pendingCount);
|
|
|
|
|
|
- // 2. 各审批级别的待审批数量
|
|
|
+ // 2. 各审批级别的待审批数量(五级审批制度)
|
|
|
Map<String, Long> pendingByLevel = new HashMap<>();
|
|
|
- pendingByLevel.put("level1", countByStatusAndLevel("0", "1", tenantId)); // 部门主管
|
|
|
- pendingByLevel.put("level2", countByStatusAndLevel("0", "2", tenantId)); // 分管领导
|
|
|
- pendingByLevel.put("level3", countByStatusAndLevel("0", "3", tenantId)); // 校领导
|
|
|
+ pendingByLevel.put("level1", countByStatusAndLevel("0", "1", tenantId)); // 部门负责人
|
|
|
+ pendingByLevel.put("level2", countByStatusAndLevel("0", "2", tenantId)); // 人事处
|
|
|
+ pendingByLevel.put("level3", countByStatusAndLevel("0", "3", tenantId)); // 纪委
|
|
|
+ pendingByLevel.put("level4", countByStatusAndLevel("0", "4", tenantId)); // 学院分管领导
|
|
|
+ pendingByLevel.put("level5", countByStatusAndLevel("0", "5", tenantId)); // 学院党委书记
|
|
|
statistics.put("pendingByLevel", pendingByLevel);
|
|
|
|
|
|
// 3. 已完成审批总数
|
|
|
@@ -285,9 +315,20 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
|
|
// 审批状态信息
|
|
|
vo.setStatus(application.getStatus());
|
|
|
vo.setStatusText(getStatusText(application.getStatus()));
|
|
|
- vo.setCurrentApprover(application.getCurrentApprover());
|
|
|
- vo.setApprovalLevel(application.getApprovalLevel());
|
|
|
- vo.setApprovalLevelText(getApprovalLevelText(application.getApprovalLevel()));
|
|
|
+
|
|
|
+ // 根据申请状态设置当前审批人和审批级别
|
|
|
+ if ("1".equals(application.getStatus()) || "2".equals(application.getStatus())) {
|
|
|
+ // 审批已完成(通过或拒绝)
|
|
|
+ vo.setCurrentApprover("审批已完成");
|
|
|
+ vo.setApprovalLevel(application.getApprovalLevel());
|
|
|
+ vo.setApprovalLevelText("审批已完成");
|
|
|
+ } else {
|
|
|
+ // 待审核状态,显示当前审批信息
|
|
|
+ vo.setCurrentApprover(application.getCurrentApprover());
|
|
|
+ vo.setApprovalLevel(application.getApprovalLevel());
|
|
|
+ vo.setApprovalLevelText(getApprovalLevelText(application.getApprovalLevel()));
|
|
|
+ }
|
|
|
+
|
|
|
vo.setApplyTime(application.getApplyTime());
|
|
|
vo.setApprovalTime(application.getApprovalTime());
|
|
|
|
|
|
@@ -336,8 +377,8 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
|
|
.count();
|
|
|
vo.setApprovedLevels((int) approvedCount);
|
|
|
|
|
|
- // 总审批级别数(假设最多3级:部门主管、分管领导、校领导)
|
|
|
- vo.setTotalLevels(3);
|
|
|
+ // 总审批级别数(五级审批制度:部门负责人、人事处、纪委、学院分管领导、学院党委书记)
|
|
|
+ vo.setTotalLevels(5);
|
|
|
|
|
|
// 审批进度文本
|
|
|
vo.setApprovalProgress(approvedCount + "/" + vo.getTotalLevels());
|
|
|
@@ -350,8 +391,8 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
|
|
} else {
|
|
|
// 未开始审批
|
|
|
vo.setApprovedLevels(0);
|
|
|
- vo.setTotalLevels(3);
|
|
|
- vo.setApprovalProgress("0/3");
|
|
|
+ vo.setTotalLevels(5);
|
|
|
+ vo.setApprovalProgress("0/5");
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -380,6 +421,7 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
|
|
|
|
|
/**
|
|
|
* 获取审批级别文本
|
|
|
+ * 五级审批制度:1-部门负责人,2-人事处,3-纪委,4-学院分管领导,5-学院党委书记
|
|
|
*/
|
|
|
private String getApprovalLevelText(String level) {
|
|
|
if (level == null) {
|
|
|
@@ -387,11 +429,15 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
|
|
}
|
|
|
switch (level) {
|
|
|
case "1":
|
|
|
- return "部门主管审批";
|
|
|
+ return "部门负责人审批";
|
|
|
case "2":
|
|
|
- return "分管领导审批";
|
|
|
+ return "人事处审批";
|
|
|
case "3":
|
|
|
- return "校领导审批";
|
|
|
+ return "纪委审批";
|
|
|
+ case "4":
|
|
|
+ return "学院分管领导审批";
|
|
|
+ case "5":
|
|
|
+ return "学院党委书记审批";
|
|
|
default:
|
|
|
return "未知";
|
|
|
}
|
|
|
@@ -495,4 +541,560 @@ public class RsOverseasApprovalMonitorServiceImpl implements IRsOverseasApproval
|
|
|
return politicalStatus;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 下载出国申请审批表PDF
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public void downloadApprovalPdf(Long applicationId, HttpServletResponse response) {
|
|
|
+ log.info("开始生成出国申请审批表PDF,申请ID:{}", applicationId);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 查询申请信息
|
|
|
+ RsOverseasApplication application = applicationMapper.selectById(applicationId);
|
|
|
+ if (application == null) {
|
|
|
+ throw new ServiceException("未找到申请信息");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询审批历史
|
|
|
+ List<RsOverseasApprovalVo> approvalHistory = approvalService.queryByApplicationId(applicationId);
|
|
|
+
|
|
|
+ // 调试日志:输出审批历史数据
|
|
|
+ log.info("📋 查询到审批历史记录数量: {}", approvalHistory != null ? approvalHistory.size() : 0);
|
|
|
+ if (approvalHistory != null && !approvalHistory.isEmpty()) {
|
|
|
+ for (RsOverseasApprovalVo approval : approvalHistory) {
|
|
|
+ log.info(" - 级别{}: 审批人={}, 意见={}, 签字数据长度={}",
|
|
|
+ approval.getApprovalLevel(),
|
|
|
+ approval.getApproverName(),
|
|
|
+ approval.getApprovalOpinion(),
|
|
|
+ approval.getApprovalSignature() != null ? approval.getApprovalSignature().length() : 0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置响应头
|
|
|
+ String fileName = "出国申请审批表_" + application.getApplicantName() + "_"
|
|
|
+ + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".pdf";
|
|
|
+ response.setContentType("application/pdf");
|
|
|
+ response.setHeader("Content-Disposition",
|
|
|
+ "attachment; filename*=UTF-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8));
|
|
|
+
|
|
|
+ // 生成PDF
|
|
|
+ generateApprovalPdf(application, approvalHistory, response.getOutputStream());
|
|
|
+
|
|
|
+ log.info("✅ 出国申请审批表PDF生成成功");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("❌ 生成出国申请审批表PDF失败:{}", e.getMessage(), e);
|
|
|
+ throw new ServiceException("生成PDF失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成审批表PDF文档
|
|
|
+ */
|
|
|
+ private void generateApprovalPdf(RsOverseasApplication application,
|
|
|
+ List<RsOverseasApprovalVo> approvalHistory, OutputStream outputStream)
|
|
|
+ throws DocumentException, IOException {
|
|
|
+
|
|
|
+ Document document = new Document(PageSize.A4, 40, 40, 40, 40);
|
|
|
+ PdfWriter.getInstance(document, outputStream);
|
|
|
+ document.open();
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 创建中文字体
|
|
|
+ BaseFont baseFont = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.NOT_EMBEDDED);
|
|
|
+ Font titleFont = new Font(baseFont, 16, Font.BOLD);
|
|
|
+ Font headerFont = new Font(baseFont, 14, Font.BOLD);
|
|
|
+ Font normalFont = new Font(baseFont, 10, Font.NORMAL);
|
|
|
+ Font smallFont = new Font(baseFont, 9, Font.NORMAL);
|
|
|
+
|
|
|
+ // 1. 标题
|
|
|
+ Paragraph title1 = new Paragraph("杭州第一技师学院", titleFont);
|
|
|
+ title1.setAlignment(Element.ALIGN_CENTER);
|
|
|
+ title1.setSpacingAfter(5);
|
|
|
+ document.add(title1);
|
|
|
+
|
|
|
+ Paragraph title2 = new Paragraph("登记报备人员因私事出国(境)审批表", headerFont);
|
|
|
+ title2.setAlignment(Element.ALIGN_CENTER);
|
|
|
+ title2.setSpacingAfter(15);
|
|
|
+ document.add(title2);
|
|
|
+
|
|
|
+ // 2. 个人信息表格(优化列宽,使垂直线对齐)
|
|
|
+ PdfPTable infoTable = new PdfPTable(12); // 12列
|
|
|
+ infoTable.setWidthPercentage(100);
|
|
|
+ // 列宽:使关键分界线对齐
|
|
|
+ infoTable.setWidths(new float[] { 6f, 9f, 5f, 7f, 8f, 9f, 8f, 9f, 6f, 9f, 8f, 10f });
|
|
|
+
|
|
|
+ // 第一行:姓名、性别、出生年月、政治面貌
|
|
|
+ addCell(infoTable, "姓名", normalFont, true, 1); // 列1: 6f
|
|
|
+ addCell(infoTable, application.getApplicantName(), normalFont, false, 2); // 列2-3: 9f+5f=14f
|
|
|
+ addCell(infoTable, "性别", normalFont, true, 1); // 列4: 7f
|
|
|
+ addCell(infoTable, translateGender(application.getGender()), normalFont, false, 1); // 列5: 8f
|
|
|
+ addCell(infoTable, "出生年月", normalFont, true, 1); // 列6: 9f
|
|
|
+ addCell(infoTable, formatBirthDate(application.getBirthDate()), normalFont, false, 2); // 列7-8: 8f+9f=17f
|
|
|
+ addCell(infoTable, "政治面貌", normalFont, true, 1); // 列9: 6f
|
|
|
+ addCell(infoTable, translatePoliticalStatus(application.getPoliticalStatus()), normalFont, false, 3); // 列10-12:
|
|
|
+ // 9f+8f+10f=27f
|
|
|
+
|
|
|
+ // 第二行:现工作部门、职称、职务
|
|
|
+ addCell(infoTable, "现工作部门\n(教学系)", normalFont, true, 3); // 列1-3: 6f+9f+5f=20f
|
|
|
+ addCell(infoTable, application.getDepartment(), normalFont, false, 2); // 列4-5: 7f+8f=15f
|
|
|
+ addCell(infoTable, "职称", normalFont, true, 1); // 列6: 9f
|
|
|
+ addCell(infoTable, application.getProfessionalTitle(), normalFont, false, 2); // 列7-8: 8f+9f=17f
|
|
|
+ addCell(infoTable, "职务", normalFont, true, 1); // 列9: 6f
|
|
|
+ addCell(infoTable, application.getPosition(), normalFont, false, 3); // 列10-12: 9f+8f+10f=27f
|
|
|
+
|
|
|
+ // 第三行:申领证件名称、证件号码
|
|
|
+ addCell(infoTable, "申领证件名称", normalFont, true, 1); // 列1: 6f
|
|
|
+ addCell(infoTable, application.getDocumentName(), normalFont, false, 2); // 列2-3: 9f+5f=14f
|
|
|
+ addCell(infoTable, "证件号码", normalFont, true, 2); // 列4-5: 7f+8f=15f
|
|
|
+ addCell(infoTable, application.getDocumentNumber(), normalFont, false, 7); // 列6-12:
|
|
|
+ // 9f+8f+9f+6f+9f+8f+10f=59f
|
|
|
+
|
|
|
+ // 第四行:前往国家或地区(按照附件2标准格式)
|
|
|
+ addCell(infoTable, "前往国家\n或地区", normalFont, true, 1); // 列1: 6f
|
|
|
+
|
|
|
+ PdfPCell destinationCell = new PdfPCell();
|
|
|
+ destinationCell.setColspan(11); // 列2-12: 占据剩余11列
|
|
|
+ destinationCell.setMinimumHeight(60);
|
|
|
+ destinationCell.setVerticalAlignment(Element.ALIGN_TOP);
|
|
|
+ destinationCell.setPadding(0);
|
|
|
+
|
|
|
+ // 创建嵌套表格:5列布局(国家名称 | 出境事由 | 旅游/探亲 | 探望对象关系标签 | 探望对象关系内容)
|
|
|
+ // 列宽精确对齐外层表格的垂直线位置
|
|
|
+ PdfPTable innerTable = new PdfPTable(5);
|
|
|
+ innerTable.setWidthPercentage(100);
|
|
|
+ // 对齐到外层列3|列5|列8|列10|列12的位置: 14f, 15f, 26f, 15f, 18f (总计88f)
|
|
|
+ innerTable.setWidths(new float[] { 14f, 15f, 26f, 15f, 18f });
|
|
|
+
|
|
|
+ // 第一列:国家名称(合并3行)
|
|
|
+ String destination = StringUtils.isNotBlank(application.getDestinationCountry())
|
|
|
+ ? application.getDestinationCountry()
|
|
|
+ : "";
|
|
|
+ PdfPCell countryCell = new PdfPCell(new Phrase(destination, smallFont));
|
|
|
+ countryCell.setRowspan(3);
|
|
|
+ countryCell.setMinimumHeight(60);
|
|
|
+ countryCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
|
|
+ countryCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
|
|
+ countryCell.setPadding(3);
|
|
|
+ innerTable.addCell(countryCell);
|
|
|
+
|
|
|
+ // 第一行:出境事由(合并2行)| 旅游选项(占据右侧3列)
|
|
|
+ // 出境事由标签(合并2行)
|
|
|
+ PdfPCell reasonLabelCell = new PdfPCell(new Phrase("出境事由", smallFont));
|
|
|
+ reasonLabelCell.setRowspan(2);
|
|
|
+ reasonLabelCell.setMinimumHeight(40);
|
|
|
+ reasonLabelCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
|
|
+ reasonLabelCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
|
|
+ innerTable.addCell(reasonLabelCell);
|
|
|
+
|
|
|
+ // 旅游选项框(占据3列)
|
|
|
+ String travelMark = "旅游".equals(application.getExitReason()) ? "√" : "";
|
|
|
+ PdfPCell travelCell = new PdfPCell(new Phrase("旅游( " + travelMark + " )", smallFont));
|
|
|
+ travelCell.setColspan(3);
|
|
|
+ travelCell.setMinimumHeight(20);
|
|
|
+ travelCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
|
|
+ travelCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
|
|
+ innerTable.addCell(travelCell);
|
|
|
+
|
|
|
+ // 第二行:探亲选项 | 探望对象关系标签 | 探望对象关系内容
|
|
|
+ // 探亲选项框
|
|
|
+ String visitMark = "探亲".equals(application.getExitReason()) ? "√" : "";
|
|
|
+ PdfPCell visitCell = new PdfPCell(new Phrase("探亲( " + visitMark + " )", smallFont));
|
|
|
+ visitCell.setMinimumHeight(20);
|
|
|
+ visitCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
|
|
+ visitCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
|
|
+ innerTable.addCell(visitCell);
|
|
|
+
|
|
|
+ // 探望对象关系标签(独立单元格)
|
|
|
+ PdfPCell relationLabelCell = new PdfPCell(new Phrase("探望对象姓名及\n与申请者关系", smallFont));
|
|
|
+ relationLabelCell.setMinimumHeight(20);
|
|
|
+ relationLabelCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
|
|
+ relationLabelCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
|
|
+ relationLabelCell.setPadding(3);
|
|
|
+ innerTable.addCell(relationLabelCell);
|
|
|
+
|
|
|
+ // 探望对象关系内容(独立单元格)
|
|
|
+ String relationship = StringUtils.isNotBlank(application.getVisitRelationship())
|
|
|
+ ? application.getVisitRelationship()
|
|
|
+ : "";
|
|
|
+ PdfPCell relationContentCell = new PdfPCell(new Phrase(relationship, smallFont));
|
|
|
+ relationContentCell.setMinimumHeight(20);
|
|
|
+ relationContentCell.setVerticalAlignment(Element.ALIGN_TOP);
|
|
|
+ relationContentCell.setPadding(3);
|
|
|
+ innerTable.addCell(relationContentCell);
|
|
|
+
|
|
|
+ // 第三行:备注 | 备注内容(国家名称已合并,从第2列开始,备注内容占据3列)
|
|
|
+ PdfPCell remarkLabelCell = new PdfPCell(new Phrase("备注", smallFont));
|
|
|
+ remarkLabelCell.setMinimumHeight(20);
|
|
|
+ remarkLabelCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
|
|
+ remarkLabelCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
|
|
+ remarkLabelCell.setPadding(3);
|
|
|
+ innerTable.addCell(remarkLabelCell);
|
|
|
+
|
|
|
+ String remarks = StringUtils.isNotBlank(application.getRemarks())
|
|
|
+ ? application.getRemarks()
|
|
|
+ : "";
|
|
|
+ PdfPCell remarkValueCell = new PdfPCell(new Phrase(remarks, smallFont));
|
|
|
+ remarkValueCell.setColspan(3); // 占据第3、4、5列
|
|
|
+ remarkValueCell.setMinimumHeight(20);
|
|
|
+ remarkValueCell.setVerticalAlignment(Element.ALIGN_TOP);
|
|
|
+ remarkValueCell.setPadding(3);
|
|
|
+ innerTable.addCell(remarkValueCell);
|
|
|
+
|
|
|
+ destinationCell.addElement(innerTable);
|
|
|
+ infoTable.addCell(destinationCell);
|
|
|
+
|
|
|
+ // 第五行:出国时间
|
|
|
+ addCell(infoTable, "出过(境)时间", normalFont, true, 2); // 列1-2
|
|
|
+ PdfPCell timeCell = new PdfPCell();
|
|
|
+ timeCell.setColspan(10); // 列3-12: 占据剩余10列
|
|
|
+ timeCell.setMinimumHeight(30);
|
|
|
+ timeCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
|
|
+
|
|
|
+ String timeText = String.format("拟于 %s 至 %s 出国(境),为期 %d 天。",
|
|
|
+ formatDateChinese(application.getStartDate()),
|
|
|
+ formatDateChinese(application.getEndDate()),
|
|
|
+ application.getDuration() != null ? application.getDuration() : 0);
|
|
|
+ Paragraph timePara = new Paragraph(timeText, smallFont);
|
|
|
+ timeCell.addElement(timePara);
|
|
|
+ infoTable.addCell(timeCell);
|
|
|
+
|
|
|
+ // 第六行:承诺声明(修复断层问题,使用单一段落)
|
|
|
+ PdfPCell promiseCell = new PdfPCell();
|
|
|
+ promiseCell.setColspan(12); // 占据全部12列
|
|
|
+ promiseCell.setMinimumHeight(70);
|
|
|
+ promiseCell.setVerticalAlignment(Element.ALIGN_TOP);
|
|
|
+ promiseCell.setPadding(5);
|
|
|
+
|
|
|
+ // 使用单一段落避免断层,通过Chunk控制对齐
|
|
|
+ Paragraph promisePara = new Paragraph();
|
|
|
+ promisePara.setSpacingAfter(0);
|
|
|
+ promisePara.setSpacingBefore(0);
|
|
|
+
|
|
|
+ // 承诺内容 - 居中部分
|
|
|
+ Chunk promiseText1 = new Chunk("本人承诺,在出国(境)期间遵守当地法律法规和中国的法律法规,结束后如期回国并到学校", smallFont);
|
|
|
+ Chunk promiseText2 = new Chunk("\n报到,如逾期不归,同意作自动离职处理。\n", smallFont);
|
|
|
+ promisePara.add(promiseText1);
|
|
|
+ promisePara.add(promiseText2);
|
|
|
+ promisePara.setAlignment(Element.ALIGN_CENTER);
|
|
|
+ promiseCell.addElement(promisePara);
|
|
|
+
|
|
|
+ // 联系信息和签字 - 左对齐部分
|
|
|
+ Paragraph contactPara = new Paragraph();
|
|
|
+ contactPara.setSpacingAfter(0);
|
|
|
+ contactPara.setSpacingBefore(2);
|
|
|
+ contactPara.setAlignment(Element.ALIGN_LEFT);
|
|
|
+ contactPara.setIndentationLeft(10);
|
|
|
+ contactPara.add(new Chunk("手写签字:", smallFont));
|
|
|
+
|
|
|
+ // 如果有签字,插入签字图片
|
|
|
+ if (StringUtils.isNotBlank(application.getSignature())) {
|
|
|
+ try {
|
|
|
+ Image signatureImage = getImageFromBase64(application.getSignature());
|
|
|
+ if (signatureImage != null) {
|
|
|
+ signatureImage.scaleToFit(60, 20);
|
|
|
+ contactPara.add(new Chunk(signatureImage, 0, -5));
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("插入申请人签字图片失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ contactPara.add(new Chunk(" 联系电话:", smallFont));
|
|
|
+ if (StringUtils.isNotBlank(application.getMobilePhone())) {
|
|
|
+ contactPara.add(new Chunk(application.getMobilePhone(), smallFont));
|
|
|
+ } else {
|
|
|
+ contactPara.add(new Chunk("13333333333", smallFont));
|
|
|
+ }
|
|
|
+ contactPara.add(new Chunk(" 应急联系人及联系电话:", smallFont));
|
|
|
+ if (StringUtils.isNotBlank(application.getEmergencyContact())) {
|
|
|
+ contactPara.add(new Chunk(application.getEmergencyContact(), smallFont));
|
|
|
+ } else {
|
|
|
+ contactPara.add(new Chunk("13333333333", smallFont));
|
|
|
+ }
|
|
|
+ promiseCell.addElement(contactPara);
|
|
|
+
|
|
|
+ // 日期 - 靠右对齐部分(使用申请日期)
|
|
|
+ Paragraph datePara = new Paragraph();
|
|
|
+ datePara.setSpacingAfter(0);
|
|
|
+ datePara.setSpacingBefore(2);
|
|
|
+ datePara.setAlignment(Element.ALIGN_RIGHT);
|
|
|
+ datePara.setIndentationRight(20);
|
|
|
+ // 使用申请时间作为申请人签名日期(优先使用applyTime,其次使用createTime)
|
|
|
+ Date applicationDateTime = application.getApplyTime() != null
|
|
|
+ ? application.getApplyTime()
|
|
|
+ : application.getCreateTime();
|
|
|
+ String applicationDate = applicationDateTime != null
|
|
|
+ ? formatDateChinese(applicationDateTime)
|
|
|
+ : "年 月 日";
|
|
|
+ datePara.add(new Chunk(applicationDate, smallFont));
|
|
|
+ promiseCell.addElement(datePara);
|
|
|
+
|
|
|
+ infoTable.addCell(promiseCell);
|
|
|
+
|
|
|
+ document.add(infoTable);
|
|
|
+
|
|
|
+ // 3. 审批意见表格(紧贴个人信息表格,无间隔)
|
|
|
+ PdfPTable approvalTable = new PdfPTable(2);
|
|
|
+ approvalTable.setWidthPercentage(100);
|
|
|
+ approvalTable.setWidths(new float[] { 20f, 80f });
|
|
|
+ approvalTable.setSpacingBefore(0); // 设置为0,与上方表格紧密连接
|
|
|
+
|
|
|
+ // 部门(教学系)意见
|
|
|
+ addApprovalRowWithSignature(approvalTable, "部门\n(教学\n系)\n意见",
|
|
|
+ approvalHistory, "1", normalFont, smallFont);
|
|
|
+
|
|
|
+ // 人事(组织)处审核意见
|
|
|
+ addApprovalRowWithSignature(approvalTable, "人事\n(组\n织)处\n审核\n意见",
|
|
|
+ approvalHistory, "2", normalFont, smallFont);
|
|
|
+
|
|
|
+ // 纪委意见
|
|
|
+ addApprovalRowWithSignature(approvalTable, "纪委\n意见",
|
|
|
+ approvalHistory, "3", normalFont, smallFont);
|
|
|
+
|
|
|
+ // 学院分管领导意见(独立行)
|
|
|
+ addApprovalRowWithLeaderSignature(approvalTable, "学院分\n管领导\n意见",
|
|
|
+ approvalHistory, "4", normalFont, smallFont);
|
|
|
+
|
|
|
+ // 学院党委书记意见(独立行)
|
|
|
+ addApprovalRowWithSignature(approvalTable, "学院党\n委书记\n意见",
|
|
|
+ approvalHistory, "5", normalFont, smallFont);
|
|
|
+
|
|
|
+ document.add(approvalTable);
|
|
|
+
|
|
|
+ // 4. 备注说明(移除空段落,直接连接)
|
|
|
+ Paragraph note = new Paragraph();
|
|
|
+ note.setSpacingBefore(5); // 只留很小的间距
|
|
|
+ note.add(new Chunk("备注:1. 因私出境时间,除特殊原因外,原则上应为假期;\n", smallFont));
|
|
|
+ note.add(new Chunk(" 2. 新申办证件名,本表证件号码一栏不填。", smallFont));
|
|
|
+ document.add(note);
|
|
|
+
|
|
|
+ } finally {
|
|
|
+ document.close();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 添加表格单元格
|
|
|
+ */
|
|
|
+ private void addCell(PdfPTable table, String text, Font font, boolean isHeader, int colspan) {
|
|
|
+ PdfPCell cell = new PdfPCell(new Phrase(text, font));
|
|
|
+ cell.setColspan(colspan);
|
|
|
+ cell.setMinimumHeight(25);
|
|
|
+ cell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
|
|
+ cell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
|
|
+ // 移除灰色背景,所有单元格统一白色背景
|
|
|
+ // if (isHeader) {
|
|
|
+ // cell.setBackgroundColor(BaseColor.LIGHT_GRAY);
|
|
|
+ // }
|
|
|
+ table.addCell(cell);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 添加审批意见行(带签字显示)
|
|
|
+ */
|
|
|
+ private void addApprovalRowWithSignature(PdfPTable table, String label,
|
|
|
+ List<RsOverseasApprovalVo> approvalHistory, String level, Font labelFont, Font contentFont) {
|
|
|
+ PdfPCell labelCell = new PdfPCell(new Phrase(label, labelFont));
|
|
|
+ labelCell.setMinimumHeight(50);
|
|
|
+ labelCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
|
|
+ labelCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
|
|
+ table.addCell(labelCell);
|
|
|
+
|
|
|
+ PdfPCell opinionCell = new PdfPCell();
|
|
|
+ opinionCell.setMinimumHeight(50);
|
|
|
+ opinionCell.setVerticalAlignment(Element.ALIGN_TOP);
|
|
|
+ opinionCell.setPadding(5);
|
|
|
+
|
|
|
+ Paragraph para = new Paragraph();
|
|
|
+ String opinion = getApprovalOpinion(approvalHistory, level);
|
|
|
+ para.add(new Chunk(opinion, contentFont));
|
|
|
+ para.add(new Chunk("\n\n负责人签字:", contentFont));
|
|
|
+
|
|
|
+ // 添加签字图片
|
|
|
+ String signature = getApprovalSignature(approvalHistory, level);
|
|
|
+ if (StringUtils.isNotBlank(signature)) {
|
|
|
+ try {
|
|
|
+ Image signImage = getImageFromBase64(signature);
|
|
|
+ if (signImage != null) {
|
|
|
+ signImage.scaleToFit(60, 20);
|
|
|
+ para.add(new Chunk(signImage, 0, -5));
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("插入审批人签字图片失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 日期靠右(使用审批日期)
|
|
|
+ opinionCell.addElement(para);
|
|
|
+ Paragraph datePara = new Paragraph();
|
|
|
+ datePara.setAlignment(Element.ALIGN_RIGHT);
|
|
|
+ datePara.setIndentationRight(10);
|
|
|
+ // 获取审批日期
|
|
|
+ Date approvalDate = getApprovalDate(approvalHistory, level);
|
|
|
+ String dateText = approvalDate != null ? formatDateChinese(approvalDate) : "年 月 日";
|
|
|
+ datePara.add(new Chunk(dateText, contentFont));
|
|
|
+ opinionCell.addElement(datePara);
|
|
|
+
|
|
|
+ table.addCell(opinionCell);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 添加学院分管领导审批行(使用"领导签字"并包含"学院"文字)
|
|
|
+ */
|
|
|
+ private void addApprovalRowWithLeaderSignature(PdfPTable table, String label,
|
|
|
+ List<RsOverseasApprovalVo> approvalHistory, String level, Font labelFont, Font contentFont) {
|
|
|
+ PdfPCell labelCell = new PdfPCell(new Phrase(label, labelFont));
|
|
|
+ labelCell.setMinimumHeight(50);
|
|
|
+ labelCell.setVerticalAlignment(Element.ALIGN_MIDDLE);
|
|
|
+ labelCell.setHorizontalAlignment(Element.ALIGN_CENTER);
|
|
|
+ table.addCell(labelCell);
|
|
|
+
|
|
|
+ PdfPCell opinionCell = new PdfPCell();
|
|
|
+ opinionCell.setMinimumHeight(50);
|
|
|
+ opinionCell.setVerticalAlignment(Element.ALIGN_TOP);
|
|
|
+ opinionCell.setPadding(5);
|
|
|
+
|
|
|
+ Paragraph para = new Paragraph();
|
|
|
+ String opinion = getApprovalOpinion(approvalHistory, level);
|
|
|
+ para.add(new Chunk(opinion, contentFont));
|
|
|
+ para.add(new Chunk("\n\n领导签字:", contentFont));
|
|
|
+
|
|
|
+ // 添加签字图片
|
|
|
+ String signature = getApprovalSignature(approvalHistory, level);
|
|
|
+ if (StringUtils.isNotBlank(signature)) {
|
|
|
+ try {
|
|
|
+ Image signImage = getImageFromBase64(signature);
|
|
|
+ if (signImage != null) {
|
|
|
+ signImage.scaleToFit(60, 20);
|
|
|
+ para.add(new Chunk(signImage, 0, -5));
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("插入分管领导签字图片失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 日期靠右(使用审批日期)
|
|
|
+ opinionCell.addElement(para);
|
|
|
+ Paragraph datePara = new Paragraph();
|
|
|
+ datePara.setAlignment(Element.ALIGN_RIGHT);
|
|
|
+ datePara.setIndentationRight(10);
|
|
|
+ // 获取审批日期
|
|
|
+ Date approvalDate = getApprovalDate(approvalHistory, level);
|
|
|
+ String dateText = approvalDate != null ? formatDateChinese(approvalDate) : "年 月 日";
|
|
|
+ datePara.add(new Chunk(dateText, contentFont));
|
|
|
+ opinionCell.addElement(datePara);
|
|
|
+
|
|
|
+ table.addCell(opinionCell);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取指定级别的审批意见
|
|
|
+ */
|
|
|
+ private String getApprovalOpinion(List<RsOverseasApprovalVo> approvalHistory, String level) {
|
|
|
+ if (approvalHistory == null || approvalHistory.isEmpty()) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+
|
|
|
+ return approvalHistory.stream()
|
|
|
+ .filter(a -> level.equals(a.getApprovalLevel()))
|
|
|
+ .findFirst()
|
|
|
+ .map(a -> {
|
|
|
+ String opinion = StringUtils.isNotBlank(a.getApprovalOpinion())
|
|
|
+ ? a.getApprovalOpinion()
|
|
|
+ : ("1".equals(a.getApprovalStatus()) ? "同意" : "");
|
|
|
+ return opinion;
|
|
|
+ })
|
|
|
+ .orElse("");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化日期
|
|
|
+ */
|
|
|
+ private String formatDate(Date date) {
|
|
|
+ if (date == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
|
|
|
+ return sdf.format(date);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化日期为中文格式(yyyy 年 MM 月 dd 日)
|
|
|
+ */
|
|
|
+ private String formatDateChinese(Date date) {
|
|
|
+ if (date == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy 年 MM 月 dd 日");
|
|
|
+ return sdf.format(date);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 格式化出生日期(只显示年月)
|
|
|
+ */
|
|
|
+ private String formatBirthDate(Date date) {
|
|
|
+ if (date == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月");
|
|
|
+ return sdf.format(date);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取指定级别的审批签字
|
|
|
+ */
|
|
|
+ private String getApprovalSignature(List<RsOverseasApprovalVo> approvalHistory, String level) {
|
|
|
+ if (approvalHistory == null || approvalHistory.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return approvalHistory.stream()
|
|
|
+ .filter(a -> level.equals(a.getApprovalLevel()))
|
|
|
+ .findFirst()
|
|
|
+ .map(RsOverseasApprovalVo::getApprovalSignature)
|
|
|
+ .orElse(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取指定级别的审批日期
|
|
|
+ */
|
|
|
+ private Date getApprovalDate(List<RsOverseasApprovalVo> approvalHistory, String level) {
|
|
|
+ if (approvalHistory == null || approvalHistory.isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return approvalHistory.stream()
|
|
|
+ .filter(a -> level.equals(a.getApprovalLevel()))
|
|
|
+ .findFirst()
|
|
|
+ .map(RsOverseasApprovalVo::getApprovalTime)
|
|
|
+ .orElse(null);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从Base64字符串创建Image对象
|
|
|
+ */
|
|
|
+ private Image getImageFromBase64(String base64String) {
|
|
|
+ if (StringUtils.isBlank(base64String)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 处理data URL格式:data:image/png;base64,xxxxx
|
|
|
+ String base64Data = base64String;
|
|
|
+ if (base64String.contains(",")) {
|
|
|
+ base64Data = base64String.substring(base64String.indexOf(",") + 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Base64解码
|
|
|
+ byte[] imageBytes = java.util.Base64.getDecoder().decode(base64Data);
|
|
|
+
|
|
|
+ // 创建Image对象
|
|
|
+ Image image = Image.getInstance(imageBytes);
|
|
|
+ return image;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("从Base64创建图片失败: {}", e.getMessage());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|