From b41a98fa92908194db6de219c288fa1f3c25f70b Mon Sep 17 00:00:00 2001 From: wang Date: Wed, 4 Feb 2026 08:58:29 +0800 Subject: [PATCH] =?UTF-8?q?fix:word=E8=BD=AC=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../word/WordApplicationsController.java | 550 ++++++++++++++++-- 数据同步 | 5 +- 2 files changed, 494 insertions(+), 61 deletions(-) diff --git a/yun-admin/src/main/java/net/lab1024/sa/admin/module/word/WordApplicationsController.java b/yun-admin/src/main/java/net/lab1024/sa/admin/module/word/WordApplicationsController.java index 8e63701..73b7a66 100644 --- a/yun-admin/src/main/java/net/lab1024/sa/admin/module/word/WordApplicationsController.java +++ b/yun-admin/src/main/java/net/lab1024/sa/admin/module/word/WordApplicationsController.java @@ -4,26 +4,25 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import net.lab1024.sa.admin.module.penalty.domain.entity.PenaltyApplyEntity; import net.lab1024.sa.admin.module.penalty.service.PenaltyApplyService; -import net.lab1024.sa.admin.module.service.domain.form.*; -import net.lab1024.sa.admin.module.service.domain.vo.LawyerStatisticsVO; -import net.lab1024.sa.admin.module.service.domain.vo.ServiceApplicationsVO; -import net.lab1024.sa.admin.module.service.domain.vo.ServiceReportStatisticsVO; -import net.lab1024.sa.admin.module.service.service.ServiceApplicationsService; import net.lab1024.sa.admin.module.system.department.domain.vo.DepartmentVO; import net.lab1024.sa.admin.module.system.department.service.DepartmentService; import net.lab1024.sa.admin.module.system.employee.domain.entity.EmployeeEntity; import net.lab1024.sa.admin.module.system.employee.service.EmployeeService; -import net.lab1024.sa.base.common.domain.PageResult; -import net.lab1024.sa.base.common.domain.ResponseDTO; -import net.lab1024.sa.base.common.domain.ValidateList; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; -import org.springframework.web.multipart.MultipartFile; -import javax.annotation.Resource; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import javax.imageio.ImageIO; import javax.servlet.http.HttpServletResponse; -import javax.validation.Valid; -import java.math.BigDecimal; -import java.util.List; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.*; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; /** * 无处罚证明开具 Controller @@ -32,58 +31,489 @@ import java.util.List; * @Date 2025-12-20 14:44:06 * @Copyright 1.0 */ - @RestController @Tag(name = "无处罚证明开具") +@RequestMapping("/wordCertificate") public class WordApplicationsController { - @Resource - WordCertificateService wordCertificateService; - @Resource + + @Autowired + private WordCertificateService wordCertificateService; + + @Autowired private PenaltyApplyService penaltyApplyService; - @Resource - EmployeeService employeeService; - @Resource - DepartmentService departmentService; - @Operation(summary = "无处罚证明下载 @author wzh") - @GetMapping("/wordCertificate/export/{id}") - public void wordCertificateExport(HttpServletResponse response, @PathVariable Integer id, - @RequestParam(defaultValue = "word") String format) throws Exception { - //查询出当前的开具证明信息 + + @Autowired + private EmployeeService employeeService; + + @Autowired + private DepartmentService departmentService; + + // 字体配置 - 使用文泉驿字体 + private static final int TITLE_FONT_SIZE = 100; // 标题 + private static final int BODY_FONT_SIZE = 78; // 正文 + private static final int SIGNATURE_FONT_SIZE = 72; // 落款 + + // 页面布局配置 + private static final int PAGE_WIDTH = 2480; // A4宽度 (300 DPI) + private static final int PAGE_HEIGHT = 3508; // A4高度 (300 DPI) + private static final int LEFT_MARGIN = 380; // 左边距 + private static final int RIGHT_MARGIN = 380; // 右边距 + private static final int TOP_MARGIN = 500; // 顶部边距 + private static final int LINE_SPACING = 40; // 行间距 + private static final int PARAGRAPH_SPACING = 100; // 段落间距 + + // 印章配置 + private static final String SEAL_IMAGE_PATH = "templates/official_seal.png"; + private static final int SEAL_WIDTH = 580; + private static final int SEAL_HEIGHT = 580; + + // 字体缓存 + private static Font titleFont; + private static Font bodyFont; + private static Font signatureFont; + + // 文泉驿字体映射 + private static final Map WENQUANYI_FONTS = new HashMap<>(); + static { + // 文泉驿字体名称映射 + WENQUANYI_FONTS.put("title", "WenQuanYi Zen Hei"); // 标题用正黑(较粗) + WENQUANYI_FONTS.put("body", "WenQuanYi Micro Hei"); // 正文用微米黑 + WENQUANYI_FONTS.put("bold", "WenQuanYi Zen Hei Sharp"); // 加粗用点阵正黑 + } + + /** + * 静态初始化字体 + */ + static { + try { + initFonts(); + System.out.println("字体初始化完成:"); + System.out.println("标题字体: " + titleFont.getFontName()); + System.out.println("正文字体: " + bodyFont.getFontName()); + System.out.println("落款字体: " + signatureFont.getFontName()); + } catch (Exception e) { + System.err.println("字体初始化失败,使用默认字体: " + e.getMessage()); + setFallbackFonts(); + } + } + + /** + * 初始化文泉驿字体 + */ + private static void initFonts() { + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + String[] availableFonts = ge.getAvailableFontFamilyNames(); + + System.out.println("可用字体列表:"); + for (String font : availableFonts) { + if (font.contains("WenQuanYi") || font.contains("文泉驿")) { + System.out.println(" - " + font); + } + } + + // 尝试加载文泉驿正黑作为标题字体 + titleFont = findAndCreateFont(availableFonts, + new String[]{"WenQuanYi Zen Hei", "文泉驿正黑", "WenQuanYi Zen Hei Sharp"}, + Font.BOLD, TITLE_FONT_SIZE); + + // 尝试加载文泉驿微米黑作为正文字体 + bodyFont = findAndCreateFont(availableFonts, + new String[]{"WenQuanYi Micro Hei", "文泉驿微米黑", "WenQuanYi Zen Hei"}, + Font.PLAIN, BODY_FONT_SIZE); + + // 落款字体使用正文字体但字号稍小 + if (bodyFont != null) { + signatureFont = bodyFont.deriveFont((float) SIGNATURE_FONT_SIZE); + } else { + signatureFont = new Font("SansSerif", Font.PLAIN, SIGNATURE_FONT_SIZE); + } + + // 如果字体未找到,使用默认字体 + if (titleFont == null) { + titleFont = new Font("SansSerif", Font.BOLD, TITLE_FONT_SIZE); + } + if (bodyFont == null) { + bodyFont = new Font("SansSerif", Font.PLAIN, BODY_FONT_SIZE); + signatureFont = new Font("SansSerif", Font.PLAIN, SIGNATURE_FONT_SIZE); + } + } + + /** + * 查找并创建字体 + */ + private static Font findAndCreateFont(String[] availableFonts, String[] preferredFonts, int style, int size) { + for (String preferred : preferredFonts) { + for (String available : availableFonts) { + if (available.equalsIgnoreCase(preferred) || available.contains(preferred)) { + System.out.println("找到字体: " + available + " -> 使用: " + preferred); + try { + Font font = new Font(available, style, size); + // 测试字体是否能显示中文 + if (testFontCanDisplayChinese(font)) { + return font; + } + } catch (Exception e) { + System.err.println("创建字体失败: " + available + ", 错误: " + e.getMessage()); + } + } + } + } + return null; + } + + /** + * 测试字体是否能显示中文 + */ + private static boolean testFontCanDisplayChinese(Font font) { + try { + return font.canDisplay('中') && font.canDisplay('文') && font.canDisplay('证'); + } catch (Exception e) { + return false; + } + } + + /** + * 设置备选字体 + */ + private static void setFallbackFonts() { + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + String[] fonts = ge.getAvailableFontFamilyNames(); + + // 寻找任何可用的中文字体 + String fallbackFont = "SansSerif"; + for (String font : fonts) { + if (font.matches(".*[\\u4e00-\\u9fa5].*") || + font.contains("Song") || font.contains("Hei") || + font.contains("Kai") || font.contains("Fang")) { + fallbackFont = font; + break; + } + } + + titleFont = new Font(fallbackFont, Font.BOLD, TITLE_FONT_SIZE); + bodyFont = new Font(fallbackFont, Font.PLAIN, BODY_FONT_SIZE); + signatureFont = new Font(fallbackFont, Font.PLAIN, SIGNATURE_FONT_SIZE); + } + + /** + * 预览无处罚证明(图片格式) + */ + @Operation(summary = "无处罚证明预览(图片格式) @author wzh") + @GetMapping("/export/{id}") + public void previewImage(HttpServletResponse response, @PathVariable Integer id) throws Exception { + try { + WordCertificateService.CertificateData certificateData = getCertificateData(id); + byte[] imageBytes = generateCertificateImage(certificateData); + + response.setContentType("image/png"); + String fileName = LocalDateTime.now()+".png"; + response.setHeader("Content-Disposition", "inline; filename=\"" + fileName + "\""); + response.setHeader("Cache-Control", "max-age=3600"); + response.setContentLength(imageBytes.length); + + try (OutputStream out = response.getOutputStream()) { + out.write(imageBytes); + out.flush(); + } + } catch (Exception e) { + // 生成错误图片 + byte[] errorImage = generateErrorImage("生成证明失败: " + e.getMessage()); + response.setContentType("image/png"); + response.setHeader("Content-Disposition", "inline; filename=\"error.png\""); + response.setContentLength(errorImage.length); + + try (OutputStream out = response.getOutputStream()) { + out.write(errorImage); + out.flush(); + } + } + } + + /** + * 生成错误提示图片 + */ + private byte[] generateErrorImage(String errorMessage) throws IOException { + BufferedImage image = new BufferedImage(800, 400, BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = image.createGraphics(); + + try { + setupGraphics(g2d); + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, 800, 400); + + g2d.setColor(Color.RED); + Font errorFont = new Font("SansSerif", Font.BOLD, 20); + g2d.setFont(errorFont); + + // 绘制错误信息 + g2d.drawString("错误:无法生成证明", 50, 100); + g2d.drawString("原因:" + errorMessage, 50, 140); + } finally { + g2d.dispose(); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(image, "PNG", baos); + return baos.toByteArray(); + } + + /** + * 根据ID获取证书数据 + */ + private WordCertificateService.CertificateData getCertificateData(Integer id) { PenaltyApplyEntity penaltyApplyEntity = penaltyApplyService.selectOne(id); - //查询用户姓名执业证号 - EmployeeEntity byId = employeeService.getById(penaltyApplyEntity.getUserId()); - //机构信息 - DepartmentVO departmentVO = departmentService.getById(byId.getDepartmentId()); - WordCertificateService.CertificateData certificateData = new WordCertificateService.CertificateData( - departmentVO.getDepartmentName(), - departmentVO.getCreditCode(), - byId.getActualName(), - byId.getCertificateNumber(), + EmployeeEntity employee = employeeService.getById(penaltyApplyEntity.getUserId()); + DepartmentVO department = departmentService.getById(employee.getDepartmentId()); + + return new WordCertificateService.CertificateData( + department.getDepartmentName(), + department.getCreditCode(), + employee.getActualName(), + employee.getCertificateNumber(), penaltyApplyEntity.getCreateTime() ); + } + + /** + * 生成证书图片 + */ + private byte[] generateCertificateImage(WordCertificateService.CertificateData data) throws IOException { + BufferedImage image = new BufferedImage(PAGE_WIDTH, PAGE_HEIGHT, BufferedImage.TYPE_INT_RGB); + Graphics2D g2d = image.createGraphics(); + + try { + setupGraphics(g2d); + + // 填充白色背景 + g2d.setColor(Color.WHITE); + g2d.fillRect(0, 0, PAGE_WIDTH, PAGE_HEIGHT); + g2d.setColor(Color.BLACK); + + // 绘制证书内容 + drawCertificateContent(g2d, data); - byte[] fileContent; - String fileName; - String contentType; - - - // 生成Word文档 - fileContent = wordCertificateService.generateCertificate(certificateData); - // fileName = "certificate.docx"; - //contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; - - - // 设置文档响应头 - //response.setContentType(contentType); - //response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"; filename*=UTF-8''" + fileName); - // 设置响应头 - response.setContentType("image/png"); - response.setHeader("Content-Disposition", "inline; filename=\"preview.png\""); - response.setHeader("Cache-Control", "max-age=3600"); - response.setContentLength(fileContent.length); - - // 将文件内容写入响应输出流 - response.getOutputStream().write(fileContent); - response.getOutputStream().flush(); - } -} + } finally { + g2d.dispose(); + } + + return convertImageToBytes(image); + } + + /** + * 设置Graphics2D参数 + */ + private void setupGraphics(Graphics2D g2d) { + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + } + + /** + * 绘制证书内容 + */ + private void drawCertificateContent(Graphics2D g2d, WordCertificateService.CertificateData data) { + // 1. 绘制标题"证明" - 居中对齐 + g2d.setFont(titleFont); + String title = "证 明"; + FontMetrics titleMetrics = g2d.getFontMetrics(); + + // 测试字体是否能显示 + if (titleMetrics.stringWidth(title) == 0) { + throw new RuntimeException("标题字体不可用,可能显示为方框"); + } + + int titleWidth = titleMetrics.stringWidth(title); + int titleX = (PAGE_WIDTH - titleWidth) / 2; + int currentY = TOP_MARGIN; + + g2d.drawString(title, titleX, currentY); + currentY += titleMetrics.getHeight() + PARAGRAPH_SPACING; + + // 2. 绘制正文 - 带首行缩进 + g2d.setFont(bodyFont); + FontMetrics bodyMetrics = g2d.getFontMetrics(); + int lineHeight = bodyMetrics.getHeight(); + int maxLineWidth = PAGE_WIDTH - LEFT_MARGIN - RIGHT_MARGIN; + + // 构建完整内容 + String fullContent = "兹证明" + data.getCertificateNo() + "(统一社会信用代码:" + + data.getPurpose() + ")" + data.getName() + + "律师(执业证号:" + data.getIdCard() + + ")近五年在我市执业期间未受到律师协会行业处分。"; + + // 首行缩进(两个全角空格) + String indent = "  "; + String firstLine = indent + fullContent; + + // 绘制正文(带自动换行) + currentY = drawWrappedText(g2d, firstLine, LEFT_MARGIN, currentY, maxLineWidth, lineHeight); + + // 3. 绘制"特此证明。" - 同样缩进 + currentY += lineHeight; + String herebyText = indent + "特此证明。"; + g2d.drawString(herebyText, LEFT_MARGIN, currentY); + currentY += lineHeight + PARAGRAPH_SPACING * 2; + + // 4. 绘制落款和印章 + drawSignatureWithSeal(g2d, currentY, data.getLocalDateTime()); + } + + /** + * 绘制自动换行文本 + */ + private int drawWrappedText(Graphics2D g2d, String text, int x, int y, int maxWidth, int lineHeight) { + FontMetrics metrics = g2d.getFontMetrics(); + StringBuilder currentLine = new StringBuilder(); + int lineIndex = 0; + int currentY = y; + + for (int i = 0; i < text.length(); i++) { + char ch = text.charAt(i); + currentLine.append(ch); + + // 检查当前行宽度 + int lineWidth = metrics.stringWidth(currentLine.toString()); + + if (lineWidth > maxWidth) { + // 绘制当前行(除了最后一个字符) + String lineToDraw = currentLine.substring(0, currentLine.length() - 1); + int drawX = x; + g2d.drawString(lineToDraw, drawX, currentY); + + // 新行开始(从最后一个字符开始) + currentLine = new StringBuilder(String.valueOf(ch)); + currentY += lineHeight + LINE_SPACING; + lineIndex++; + } + } + + // 绘制最后一行 + if (currentLine.length() > 0) { + int drawX = x; + g2d.drawString(currentLine.toString(), drawX, currentY); + currentY += lineHeight + LINE_SPACING; + } + + return currentY; + } + + /** + * 绘制落款和印章 + */ + private void drawSignatureWithSeal(Graphics2D g2d, int startY, LocalDateTime localDateTime) { + g2d.setFont(signatureFont); + FontMetrics metrics = g2d.getFontMetrics(); + int lineHeight = metrics.getHeight(); + + String associationName = "合肥市律师协会"; + int textWidth = metrics.stringWidth(associationName); + + // 测试字体是否能显示 + if (textWidth == 0) { + throw new RuntimeException("落款字体不可用,可能显示为方框"); + } + + int textX = PAGE_WIDTH - RIGHT_MARGIN - textWidth; + int textY = startY + lineHeight * 2; + + // 绘制印章 + drawSeal(g2d, textX, textY, textWidth, lineHeight); + + // 绘制文字 + g2d.drawString(associationName, textX, textY); + + // 绘制日期 + String dateText = localDateTime.format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")); + int dateWidth = metrics.stringWidth(dateText); + int dateX = PAGE_WIDTH - RIGHT_MARGIN - dateWidth; + int dateY = textY + lineHeight + LINE_SPACING * 2; + + g2d.drawString(dateText, dateX, dateY); + } + + /** + * 绘制印章 + */ + private void drawSeal(Graphics2D g2d, int textX, int textY, int textWidth, int lineHeight) { + try { + int sealX = textX + (textWidth - SEAL_WIDTH) / 2; + int sealY = textY - lineHeight * 2; + + Resource sealResource = new ClassPathResource(SEAL_IMAGE_PATH); + if (sealResource.exists()) { + BufferedImage sealImage = ImageIO.read(sealResource.getInputStream()); + BufferedImage transparentSeal = removeWhiteBackground(sealImage); + g2d.drawImage(transparentSeal, sealX, sealY, SEAL_WIDTH, SEAL_HEIGHT, null); + } + } catch (Exception e) { + // 忽略印章错误 + } + } + + /** + * 移除图片白色背景 + */ + private BufferedImage removeWhiteBackground(BufferedImage originalImage) { + int width = originalImage.getWidth(); + int height = originalImage.getHeight(); + BufferedImage result = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int rgb = originalImage.getRGB(x, y); + Color color = new Color(rgb, true); + if (color.getRed() > 240 && color.getGreen() > 240 && color.getBlue() > 240) { + result.setRGB(x, y, 0x00FFFFFF); + } else { + result.setRGB(x, y, rgb); + } + } + } + return result; + } + + /** + * 将图片转换为字节数组 + */ + private byte[] convertImageToBytes(BufferedImage image) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(image, "PNG", baos); + return baos.toByteArray(); + } + + /** + * 字体测试接口(用于调试) + */ + @Operation(summary = "字体测试接口 @author wzh") + @GetMapping("/test/fonts") + public String testFonts() { + StringBuilder result = new StringBuilder(); + result.append("

字体测试

"); + + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + String[] fonts = ge.getAvailableFontFamilyNames(); + + result.append("

可用字体 (").append(fonts.length).append("):

"); + result.append(""); + + result.append("

当前使用的字体:

"); + result.append(""); + + return result.toString(); + } +} \ No newline at end of file diff --git a/数据同步 b/数据同步 index 7b5dce1..4c865c2 100644 --- a/数据同步 +++ b/数据同步 @@ -13,4 +13,7 @@ WHERE EXISTS ( WHERE ti.principal IS NOT NULL AND te.employee_id IS NOT NULL AND te.employee_id = re.employee_id -); \ No newline at end of file +); + +启动脚本 +nohup java -server -Xms1024m -Xmx1024m -jar yun-admin-prod-3.0.0.jar > yun-admin-nohup.out 2>&1 &