需求场景来源:
公司酒店PMS项目,需要根据不同业务场景,生成不同报表。需要异步导出报表,并将报表上传到OSS。并且在报表中心支持下载,查看其操作记录。
主要难点:
- 设计通用数据类型导出生成文件,并上传OSS
- 设计统一的导出服务入口,避免重复编写导出、上传、导出记录等相关代码
解决难点一:
采用阿里EasyExcel,设计通用导出工具,主要通过泛型支撑
/**
* excel工具类
*
* @author LGC
*/
public class ExcelUtils {
/**
* 读取excel文件
*
* @param file 文件
* @param head 表头类类型
* @param <T> 读取数据泛型
* @return 读取的数据集
* @throws IOException
*/
public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
return EasyExcel.read(file.getInputStream(), head, null)
.autoCloseStream(false)
.doReadAllSync();
}
/**
* 导出excel写入到响应中
*
* @param response 响应
* @param head 表头类类型
* @param data 导出数据
* @param <T> 导出数据泛型
* @throws IOException
*/
public static <T> void writeToResponse(HttpServletResponse response, String filename, Class<T> head, List<T> data) throws IOException {
try {
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename, "UTF-8") + ExcelTypeEnum.XLSX.getValue());
response.setContentType("application/vnd.ms-excel;charset=UTF-8");
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
EasyExcel.write(response.getOutputStream(), head)
.autoCloseStream(false)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.sheet().doWrite(data);
} catch (Exception e) {
throw new BizException("导出失败", e);
}
}
/**
* 导出excel写入到文件中
*
* @param filename 文件名
* @param head 表头类类型
* @param data 导出数据
* @param <R> 导出数据泛型
* @throws IOException
*/
public static <R> void writeToFile(String filename, Class<R> head, List<R> data) throws IOException {
try {
FileUtil.touch(filename);//hutool创建文件
EasyExcel.write(filename, head)
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
.sheet().doWrite(data);
} catch (Exception e) {
throw new BizException("导出文件失败", e);
}
}
/**
* 生成文件名称 规则 报表导出业务类型名称-导出数据日期起止年月日-uuid
*
* @param exportBusinessTypeEnum 报表导出业务类型
* @param startTime 开始时间(没有传空)
* @param endTime 结束时间(没有传空)
* @return 文件名称
*/
public static String generateExportFileName(ExportBusinessTypeEnum exportBusinessTypeEnum, Date startTime, Date endTime) {
StringBuilder sb = new StringBuilder();
sb.append(exportBusinessTypeEnum.getValue());
if (startTime != null && endTime != null) {
sb.append("_").append(DateUtil.formatDate(startTime)).append("_").append(DateUtil.formatDate(endTime));
}
sb.append("_").append(IdUtil.fastSimpleUUID()).append(".xlsx");
return sb.toString();
}
}
文件上传到OSS,上传后删除临时文件
/**
* 上传导出文件到OSS
*
* @param fileName 文件名称
* @return
*/
public String uploadExportFile(String fileName) {
File file = null;
try {
file = new File(fileName);
FileInputStream fileInputStream = new FileInputStream(file);
MultipartFile multipartFile = new MockMultipartFile(file.getName(), file.getName(), ContentType.APPLICATION_OCTET_STREAM.toString(), fileInputStream);
return uploadMultipartFile(multipartFile);
} catch (Exception e) {
log.error("上传文件到OSS异常:", e);
} finally {
FileUtil.del(file);
}
return null;
}
解决难点二:
采用函数式接口和泛型设计统一导出服务,采用 CompletableFuture 异步执行任务
/**
* 报表导出服务
*
* @author LGC
*/
public interface IReportExportService {
/**
* 导出报表
*
* @param req 导出报表通用请求对象
* @param t 报表查询类用于获取导出报表数据
* @param clazz 导出报表表头类类型
* @param function 函数式接口 获取导出报表数据
* @param <T> 泛型
* @param <R> 泛型
*/
<T, R> void export(ReportCommonReqDTO req, T t, Class<R> clazz, Function<T, List<R>> function);
}
/**
* 报表导出服务实现
*
* @author LGC
*/
@Service(value = "reportExportService")
@Slf4j
public class ReportExportServiceImpl implements IReportExportService {
@Resource
private AliyunOSSUtils aliyunOSSUtils;
@Resource
private ThreadPoolExecutor myThreadPoolExecutor;
@DubboReference(version = DubboConstant.VERSION_1, retries = DubboConstant.RETRIES, timeout = DubboConstant.TIMEOUT)
private IExportProvider exportProvider;
@Override
public <T, R> void export(ReportCommonReqDTO req, T t, Class<R> clazz, Function<T, List<R>> function) {
// 导出的业务类型
ExportBusinessTypeEnum exportReportEnum = req.getExportReportEnum();
// 生成导出文件名
String fileName = ExcelUtils.generateExportFileName(exportReportEnum, req.getStartDate(), req.getEndDate());
ExportTaskReqDTO exportTaskDto = new ExportTaskReqDTO(req.getHotelId(), req.getUserId(), req.getUsername(), exportReportEnum, fileName);
// 保存导出任务
int exportTaskId = exportProvider.saveExportTask(exportTaskDto);
if (exportTaskId > 0) {
// 异步执行文件导出并上传
CompletableFuture.runAsync(() -> {
String ossUrl = null;
// 执行函数式接口,获取导出数据
List<R> dataList = function.apply(t);
UpdateExportTaskReqDTO updateExportTaskReqDTO = new UpdateExportTaskReqDTO();
updateExportTaskReqDTO.setId(exportTaskId);
updateExportTaskReqDTO.setUserId(req.getUserId());
updateExportTaskReqDTO.setUserName(req.getUsername());
if (CollUtil.isNotEmpty(dataList)) {
try {
// 导出数据写入到文件
ExcelUtils.writeToFile(fileName, clazz, dataList);
// 上传文件到阿里云OSS
ossUrl = aliyunOSSUtils.uploadExportFile(fileName);
} catch (Exception e) {
log.error("生成{}报表错误,上传文件失败", exportReportEnum.getValue(), e);
}
if (StrUtil.isNotBlank(ossUrl)) {
// 导出成功,更新文件Url
updateExportTaskReqDTO.setFileUrl(ossUrl);
updateExportTaskReqDTO.setDesc("success");
exportProvider.updateExportTaskSuccess(updateExportTaskReqDTO);
} else {
updateExportTaskReqDTO.setDesc("生成" + exportReportEnum.getValue() + "报表错误,上传文件失败");
exportProvider.updateExportTaskFail(updateExportTaskReqDTO);
}
} else {
updateExportTaskReqDTO.setDesc("导出结果集不存在");
exportProvider.updateExportTaskFail(updateExportTaskReqDTO);
}
}, myThreadPoolExecutor);
}
}
}
/**
* @author LGC
*/
@ApiModel(description = "导出报表通用请求参数")
@Data
public class ReportCommonReqDTO implements Serializable {
private static final long serialVersionUID = -3277273666353496536L;
@ApiModelProperty(value = "报表导出业务类型")
private ExportBusinessTypeEnum exportReportEnum;
@ApiModelProperty(value = "门店ID")
private Integer hotelId;
@ApiModelProperty("开始日期")
private Date startDate;
@ApiModelProperty("结束时间")
private Date endDate;
@ApiModelProperty("操作人ID")
private Integer userId;
@ApiModelProperty("操作人")
private String username;
}
如何使用:
@Resource
private IReportExportService reportExportService;
/**
* 收支明细导出
*
* @param dto
*/
@Override
public void incomeAndExpenditureDetailExport(IncomeAndExpenditureDetailReqDTO dto) {
ReportCommonReqDTO reportReq = new ReportCommonReqDTO();
reportReq.setExportReportEnum(ExportBusinessTypeEnum.Export_Report_2);
reportReq.setHotelId(10001);
reportReq.setStartDate(dto.getStartDate());
reportReq.setEndDate(dto.getEndDate());
reportReq.setUserId(0);
reportReq.setUsername("用户名");
reportExportService.export(reportReq, dto, IncomeAndExpenditureDetailVO.class, req -> dataStatisticsMapper.incomeAndExpenditureDetailExport(dto));
}
/**
* @author LGC
*/
@ApiModel(description = "收支明细列表响应")
@Data
public class IncomeAndExpenditureDetailVO implements Serializable {
private static final long serialVersionUID = -3277273666353496536L;
@ExcelIgnore
@ApiModelProperty(value = "id")
private Integer id;
@ExcelProperty(value = "日期", index = 0)
@com.alibaba.excel.annotation.format.DateTimeFormat("yyyy-MM-dd")
@ApiModelProperty(value = "数据日期")
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
private Date dataDate;
@ExcelProperty(value = "预订单号", index = 1)
@ApiModelProperty(value = "预订单号")
private String orderNo;
@ExcelProperty(value = "房间号", index = 2)
@ApiModelProperty(value = "房间号")
private String roomNo;
@ExcelProperty(value = "客人姓名", index = 3)
@ApiModelProperty(value = "客人姓名")
private String guestName;
@ExcelProperty(value = "收支类型", index = 4)
@ApiModelProperty(value = "收支类型")
private String financeType;
@ExcelProperty(value = "金额", index = 5)
@ApiModelProperty(value = "金额")
private String amount;
@ExcelProperty(value = "项目", index = 6)
@ApiModelProperty(value = "项目")
private String projectName;
@ExcelProperty(value = "备注", index = 7)
@ApiModelProperty(value = "备注")
private String remarks;
}
/**
* 报表导出业务类型
*
* @author LGC
*/
@Getter
@AllArgsConstructor
public enum ExportBusinessTypeEnum implements IBaseEnum<Integer> {
Export_Report_1(1, "渠道来源明细"),
Export_Report_2(2, "收支明细"),
Export_Report_3(3, "门店统计"),
Export_Report_4(4, "业绩统计"),
;
private final Integer key;
private final String value;
}
表结构
CREATE TABLE `sys_export_task` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`hotel_id` int(11) NOT NULL COMMENT '酒店ID',
`business_type` tinyint(1) NOT NULL COMMENT '导出业务类型',
`business_name` varchar(50) NOT NULL COMMENT '导出业务名称',
`status` tinyint(1) DEFAULT '0' COMMENT '任务执行状态 0:待执行、1:成功、2:失败、3:系统删除',
`resp_desc` varchar(900) DEFAULT NULL COMMENT '失败原因,成功success',
`file_name` varchar(128) DEFAULT NULL COMMENT '文件名称',
`file_url` varchar(512) DEFAULT NULL COMMENT '文件保存地址',
`file_create_time` datetime DEFAULT NULL COMMENT '文件生成时间',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `index_1` (`hotel_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='导出任务表';
CREATE TABLE `sys_export_task_record` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`task_id` int(11) NOT NULL COMMENT '导出任务id',
`type` tinyint(1) DEFAULT '0' COMMENT '记录类型 0:文件待生成、1:文件已生成、2:下载、3:系统删除',
`operate_time` datetime NOT NULL COMMENT '操作时间',
`operate_user_id` int(11) DEFAULT NULL COMMENT '操作人ID',
`operate_user` varchar(50) NOT NULL COMMENT '操作人',
PRIMARY KEY (`id`),
KEY `index_1` (`task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='导出操作记录';