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<String> headers, List<Integer> widths, List<List<String>> 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<String> headers,
|
List<Integer> widths, List<List<String>> rows) {
|
StringBuilder sb = new StringBuilder(1024 + rows.size() * 128);
|
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
sb.append("<?mso-application progid=\"Excel.Sheet\"?>\n");
|
sb.append("<Workbook xmlns=\"urn:schemas-microsoft-com:office:spreadsheet\" ");
|
sb.append("xmlns:ss=\"urn:schemas-microsoft-com:office:spreadsheet\">\n");
|
sb.append("<Worksheet ss:Name=\"").append(xmlAttr(sheetName)).append("\"><Table>");
|
if (widths != null) {
|
for (Integer w : widths) {
|
if (w == null) {
|
continue;
|
}
|
sb.append("<Column ss:Width=\"").append(w).append("\"/>");
|
}
|
}
|
sb.append(toRow(headers));
|
if (rows != null) {
|
for (List<String> row : rows) {
|
sb.append(toRow(row));
|
}
|
}
|
sb.append("</Table></Worksheet>\n</Workbook>");
|
return sb.toString();
|
}
|
|
private static String toRow(List<String> cells) {
|
StringBuilder sb = new StringBuilder("<Row>");
|
if (cells != null) {
|
for (String cell : cells) {
|
sb.append("<Cell><Data ss:Type=\"String\">")
|
.append(xmlText(cell))
|
.append("</Data></Cell>");
|
}
|
}
|
sb.append("</Row>");
|
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]", "_");
|
}
|
}
|