package jnpf.tmsUtil; import jnpf.util.DateUtil; import jnpf.util.StringUtil; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.List; /** * TMS 导出:生成 Excel 可识别的 SpreadsheetML(.xls),中文与日期列完整显示。 */ public final class TmsExcelExportHelper { /** 导出文件名时间戳:yyyyMMddHHmmss */ public static final String FILE_TIME_PATTERN = "yyyyMMddHHmmss"; private TmsExcelExportHelper() { } /** * 业务名 + 时间戳,如 {@code 培训效果统计_20261008114830}(不含扩展名也可)。 * 一般无需单独调用,{@link #writeSpreadsheet} 会自动套用。 */ public static String timedFileName(String baseName) { String base = sanitizeFileName(baseName); if (base.toLowerCase().endsWith(".xls")) { base = base.substring(0, base.length() - 4); } if (StringUtil.isEmpty(base)) { base = "export"; } return base + "_" + DateUtil.dateToString(new Date(), FILE_TIME_PATTERN); } /** * 写出 Excel;{@code fileName} 传业务名即可,内部统一追加 {@link #FILE_TIME_PATTERN} 时间戳并设置 Content-Disposition。 */ public static void writeSpreadsheet(HttpServletResponse response, String fileName, String sheetName, List headers, List widths, List> rows) throws IOException { byte[] bytes = buildSpreadsheetXml(sheetName, headers, widths, rows) .getBytes(StandardCharsets.UTF_8); String safeName = timedFileName(fileName) + ".xls"; String encoded = URLEncoder.encode(safeName, StandardCharsets.UTF_8).replace("+", "%20"); response.reset(); response.setCharacterEncoding("UTF-8"); response.setContentType("application/vnd.ms-excel;charset=UTF-8"); response.setHeader("Content-Disposition", "attachment; filename=\"" + encoded + "\"; filename*=UTF-8''" + encoded); response.setContentLength(bytes.length); try (OutputStream out = response.getOutputStream()) { out.write(bytes); out.flush(); } } public static String buildSpreadsheetXml(String sheetName, List headers, List widths, List> rows) { StringBuilder sb = new StringBuilder(1024 + rows.size() * 128); sb.append("\n"); sb.append("\n"); sb.append("\n"); sb.append(""); if (widths != null) { for (Integer w : widths) { if (w == null) { continue; } sb.append(""); } } sb.append(toRow(headers)); if (rows != null) { for (List row : rows) { sb.append(toRow(row)); } } sb.append("
\n
"); return sb.toString(); } private static String toRow(List cells) { StringBuilder sb = new StringBuilder(""); if (cells != null) { for (String cell : cells) { sb.append("") .append(xmlText(cell)) .append(""); } } sb.append(""); return sb.toString(); } private static String xmlText(String raw) { if (raw == null) { return ""; } return raw.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace("\"", """); } private static String xmlAttr(String raw) { String name = StringUtil.isEmpty(raw) ? "Sheet1" : raw.trim(); if (name.length() > 31) { name = name.substring(0, 31); } return xmlText(name).replace("/", "_"); } public static String sanitizeFileName(String raw) { String name = StringUtil.isEmpty(raw) ? "export" : raw.trim(); return name.replaceAll("[\\\\/:*?\"<>|\\r\\n]", "_"); } }