刘光辉
10 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;");
    }
 
    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]", "_");
    }
}