zhou zhou
6 天以前 34d36a15f339d331d668d4063cfdff50cffa5800
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
package com.vincent.rsf.server.common.service.impl;
 
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.vincent.rsf.framework.common.Cools;
import com.vincent.rsf.framework.exception.CoolException;
import com.vincent.rsf.server.common.domain.BaseParam;
import com.vincent.rsf.server.common.service.AsyncListExportTaskService;
import com.vincent.rsf.server.common.service.ListExportHandler;
import com.vincent.rsf.server.common.service.ListExportService;
import com.vincent.rsf.server.system.entity.ExportTask;
import com.vincent.rsf.server.system.service.ExportTaskService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
 
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.function.Function;
 
@Slf4j
@Service
@RequiredArgsConstructor
public class AsyncListExportTaskServiceImpl implements AsyncListExportTaskService {
    private final ExportTaskService exportTaskService;
    private final ListExportService listExportService;
 
    @Value("${logging.file.path:logs}")
    private String loggingFilePath;
 
    @Value("${rsf.export-task.expire-days:7}")
    private int exportTaskExpireDays;
 
    @Override
    public ExportTask createTask(
            String resourceKey,
            String defaultReportTitle,
            Map<String, Object> payload,
            Long tenantId,
            Long userId
    ) {
        ExportTask task = new ExportTask();
        task.setTaskCode(generateTaskCode());
        task.setResourceKey(resourceKey);
        task.setReportTitle(resolveReportTitle(payload, defaultReportTitle));
        task.setStatus(ExportTask.STATUS_PENDING);
        task.setPayloadJson(JSON.toJSONString(payload));
        task.setDeleted(0);
        task.setTenantId(tenantId);
        task.setCreateBy(userId);
        task.setUpdateBy(userId);
        task.setCreateTime(new Date());
        task.setUpdateTime(new Date());
        task.setExpireTime(resolveExpireTime());
        exportTaskService.save(task);
        return task;
    }
 
    @Override
    public ExportTask getTask(Long taskId, String resourceKey, Long tenantId, Long userId) {
        return exportTaskService.getOne(
                buildTaskQuery(taskId, tenantId, userId)
                        .eq(ExportTask::getResourceKey, resourceKey),
                false
        );
    }
 
    @Override
    public ExportTask getTask(Long taskId, Long tenantId, Long userId) {
        return exportTaskService.getOne(buildTaskQuery(taskId, tenantId, userId), false);
    }
 
    @Override
    public File getDownloadFile(Long taskId, String resourceKey, Long tenantId, Long userId) {
        ExportTask task = getTask(taskId, resourceKey, tenantId, userId);
        return resolveDownloadFile(task);
    }
 
    @Override
    public File getDownloadFile(Long taskId, Long tenantId, Long userId) {
        ExportTask task = getTask(taskId, tenantId, userId);
        return resolveDownloadFile(task);
    }
 
    private LambdaQueryWrapper<ExportTask> buildTaskQuery(Long taskId, Long tenantId, Long userId) {
        return new LambdaQueryWrapper<ExportTask>()
                .eq(ExportTask::getId, taskId)
                .eq(ExportTask::getDeleted, 0)
                .eq(ExportTask::getTenantId, tenantId)
                .eq(ExportTask::getCreateBy, userId);
    }
 
    private File resolveDownloadFile(ExportTask task) {
        if (task == null) {
            throw new CoolException("导出任务不存在");
        }
        if (!Objects.equals(task.getStatus(), ExportTask.STATUS_SUCCESS)) {
            throw new CoolException("导出任务尚未完成");
        }
        if (Cools.isEmpty(task.getFilePath())) {
            throw new CoolException("导出文件不存在");
        }
 
        File file = new File(task.getFilePath());
        if (!file.exists()) {
            throw new CoolException("导出文件不存在");
        }
        return file;
    }
 
    @Override
    @Async("taskExecutor")
    public <T, P extends BaseParam> void executeAsync(
            Long taskId,
            String resourceKey,
            Map<String, Object> payload,
            Function<Map<String, Object>, P> paramBuilder,
            ListExportHandler<T, P> exportHandler
    ) {
        ExportTask task = exportTaskService.getById(taskId);
        if (task == null) {
            return;
        }
 
        task.setStatus(ExportTask.STATUS_PROCESSING);
        task.setUpdateTime(new Date());
        exportTaskService.updateById(task);
 
        try {
            ListExportService.ExportWorkbook exportWorkbook =
                    listExportService.prepareExportWorkbook(payload, paramBuilder, exportHandler);
            File outputFile = createOutputFile(taskId, resourceKey, task.getReportTitle());
            writeWorkbook(exportWorkbook.workbook(), outputFile);
 
            task.setStatus(ExportTask.STATUS_SUCCESS);
            task.setRowCount(exportWorkbook.rowCount());
            task.setFileName(outputFile.getName());
        task.setFilePath(outputFile.getAbsolutePath());
        task.setErrorMsg(null);
        task.setUpdateTime(new Date());
        exportTaskService.updateById(task);
        } catch (Exception error) {
            log.error("异步导出失败, resourceKey={}, taskId={}", resourceKey, taskId, error);
            task.setStatus(ExportTask.STATUS_FAILED);
            task.setErrorMsg(truncateErrorMessage(error));
            task.setUpdateTime(new Date());
            exportTaskService.updateById(task);
        }
    }
 
    private String resolveReportTitle(Map<String, Object> payload, String defaultReportTitle) {
        Object metaObject = payload.get("meta");
        if (metaObject instanceof Map<?, ?> metaMap) {
            Object reportTitle = metaMap.get("reportTitle");
            if (reportTitle != null && !String.valueOf(reportTitle).trim().isEmpty()) {
                return String.valueOf(reportTitle).trim();
            }
        }
        return defaultReportTitle;
    }
 
    private String generateTaskCode() {
        return "EXP-" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + "-"
                + UUID.randomUUID().toString().replace("-", "").substring(0, 8).toUpperCase();
    }
 
    private Date resolveExpireTime() {
        return Date.from(Instant.now().plus(Math.max(exportTaskExpireDays, 1), ChronoUnit.DAYS));
    }
 
    private File createOutputFile(Long taskId, String resourceKey, String reportTitle) {
        String safeTitle = sanitizeFileName(reportTitle);
        String safeResourceKey = sanitizePathSegment(resourceKey);
        String dayFolder = new SimpleDateFormat("yyyyMMdd").format(new Date());
        File dir = new File(loggingFilePath, "export-tasks/" + safeResourceKey + "/" + dayFolder);
        if (!dir.exists() && !dir.mkdirs()) {
            throw new CoolException("导出目录创建失败");
        }
 
        String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
        return new File(dir, safeTitle + "-" + taskId + "-" + timestamp + ".xlsx");
    }
 
    private void writeWorkbook(Workbook workbook, File outputFile) throws IOException {
        try (Workbook targetWorkbook = workbook; FileOutputStream outputStream = new FileOutputStream(outputFile)) {
            targetWorkbook.write(outputStream);
            outputStream.flush();
        }
    }
 
    private String sanitizeFileName(String fileName) {
        String normalized = Objects.toString(fileName, "导出报表").trim();
        if (normalized.isEmpty()) {
            normalized = "导出报表";
        }
        return normalized.replaceAll("[\\\\/:*?\"<>|]", "_");
    }
 
    private String sanitizePathSegment(String segment) {
        String normalized = Objects.toString(segment, "default").trim();
        if (Cools.isEmpty(normalized)) {
            normalized = "default";
        }
        return normalized.replaceAll("[\\\\/:*?\"<>|]", "_");
    }
 
    private String truncateErrorMessage(Exception error) {
        String message = error == null ? "" : Objects.toString(error.getMessage(), error.getClass().getSimpleName());
        if (message.length() <= 500) {
            return message;
        }
        return message.substring(0, 500);
    }
}