ExportServiceImpl.java
package org.darkroomlibrary.service.impl;
import org.darkroomlibrary.mapper.BookMapper;
import org.darkroomlibrary.mapper.BorrowRecordMapper;
import org.darkroomlibrary.mapper.UserMapper;
import org.darkroomlibrary.context.CurrentUserContext;
import org.darkroomlibrary.web.dto.query.BookPageQuery;
import org.darkroomlibrary.web.dto.query.BorrowRecordPageQuery;
import org.darkroomlibrary.web.dto.query.PageQuery;
import org.darkroomlibrary.web.dto.query.UserPageQuery;
import org.darkroomlibrary.domain.type.UserRole;
import org.darkroomlibrary.domain.model.Book;
import org.darkroomlibrary.domain.model.User;
import org.darkroomlibrary.web.view.BorrowRecordView;
import org.darkroomlibrary.service.ExportService;
import com.alibaba.excel.EasyExcel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* 数据导出服务实现
*/
@Slf4j
@Service
public class ExportServiceImpl implements ExportService {
@Resource
private BookMapper bookMapper;
@Resource
private BorrowRecordMapper borrowRecordMapper;
@Resource
private UserMapper userMapper;
private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
private static final DateTimeFormatter FILE_DTF = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
private static final int MAX_EXPORT_ROWS = 10_000;
// ==================== 公共模板方法 ====================
/**
* 通用导出模板:设置响应头 → 构建数据 → 写入 Excel
*/
private void export(HttpServletResponse response, String prefix, List<List<String>> data) {
try {
String fileName = prefix + "_" + LocalDateTime.now().format(FILE_DTF);
setExcelResponse(response, fileName);
EasyExcel.write(response.getOutputStream())
.sheet(prefix)
.doWrite(data);
} catch (Exception e) {
log.error("导出[{}]失败", prefix, e);
try {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "导出失败");
} catch (IOException ignored) {
}
}
}
private void setExcelResponse(HttpServletResponse response, String fileName) throws IOException {
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setCharacterEncoding("UTF-8");
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replace("+", "%20");
response.setHeader("Content-Disposition", "attachment;filename=" + encodedFileName + ".xlsx");
}
private String safeStr(String value) {
return value != null ? value : "";
}
private String safeDate(LocalDateTime dt) {
return dt != null ? dt.format(DTF) : "";
}
private boolean requireAdmin(HttpServletResponse response) {
Integer roleId = CurrentUserContext.roleCode();
if (Objects.equals(roleId, UserRole.ADMIN.code())
|| Objects.equals(roleId, UserRole.SUPER_ADMIN.code())) {
return true;
}
try {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "无导出权限");
} catch (IOException e) {
log.warn("导出权限拒绝响应失败", e);
}
return false;
}
private boolean isSuperAdmin() {
return Objects.equals(CurrentUserContext.roleCode(), UserRole.SUPER_ADMIN.code());
}
private String exportEmail(String email, boolean showFullEmail) {
if (showFullEmail) {
return safeStr(email);
}
return maskEmail(email);
}
private String maskEmail(String email) {
if (email == null || email.isBlank()) {
return "";
}
int atIndex = email.indexOf('@');
if (atIndex <= 0) {
return "***";
}
String localPart = email.substring(0, atIndex);
String domain = email.substring(atIndex);
if (localPart.length() == 1) {
return localPart + "***" + domain;
}
int keepLength = Math.min(3, localPart.length());
return localPart.substring(0, keepLength) + "***" + domain;
}
private boolean prepareExport(HttpServletResponse response, PageQuery query, int total) {
if (total > MAX_EXPORT_ROWS) {
try {
response.sendError(
HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE,
"导出数据超过" + MAX_EXPORT_ROWS + "行,请缩小筛选范围"
);
} catch (IOException e) {
log.warn("导出数量限制响应失败", e);
}
return false;
}
query.setCurrent(0);
query.setSize(MAX_EXPORT_ROWS);
return true;
}
// ==================== 导出方法 ====================
@Override
public void exportBorrowRecords(HttpServletResponse response, BorrowRecordPageQuery dto) {
if (!requireAdmin(response)) {
return;
}
if (dto == null) {
dto = new BorrowRecordPageQuery();
}
if (!prepareExport(response, dto, borrowRecordMapper.queryCount(dto))) {
return;
}
List<BorrowRecordView> records = borrowRecordMapper.query(dto);
List<List<String>> data = new ArrayList<>();
data.add(List.of("用户名", "图书名", "借阅时间", "应还日期", "归还时间", "状态", "罚款金额"));
for (BorrowRecordView record : records) {
data.add(List.of(
safeStr(record.getUserName()),
safeStr(record.getBookName()),
safeDate(record.getBorrowTime()),
safeDate(record.getDueDate()),
safeDate(record.getReturnTime()),
Boolean.TRUE.equals(record.getStatus()) ? "已归还" : "借阅中",
record.getFineAmount() != null ? record.getFineAmount().toString() : "0"
));
}
export(response, "借阅记录", data);
}
@Override
public void exportBooks(HttpServletResponse response, BookPageQuery dto) {
if (!requireAdmin(response)) {
return;
}
if (dto == null) {
dto = new BookPageQuery();
}
if (!prepareExport(response, dto, bookMapper.queryCount(dto))) {
return;
}
List<Book> books = bookMapper.query(dto);
List<List<String>> data = new ArrayList<>();
data.add(List.of("书名", "作者", "ISBN", "出版社", "分类", "总数量", "可借数量"));
for (Book book : books) {
data.add(List.of(
safeStr(book.getName()),
safeStr(book.getAuthor()),
safeStr(book.getIsbn()),
safeStr(book.getPublisher()),
safeStr(book.getCategory()),
book.getTotalCount() != null ? book.getTotalCount().toString() : "0",
book.getAvailableCount() != null ? book.getAvailableCount().toString() : "0"
));
}
export(response, "图书信息", data);
}
@Override
public void exportUsers(HttpServletResponse response, UserPageQuery dto) {
if (!requireAdmin(response)) {
return;
}
if (dto == null) {
dto = new UserPageQuery();
}
if (!prepareExport(response, dto, userMapper.queryCount(dto))) {
return;
}
List<User> users = userMapper.query(dto);
boolean showFullEmail = isSuperAdmin();
List<List<String>> data = new ArrayList<>();
data.add(List.of("账号", "昵称", "邮箱", "角色", "注册时间", "状态"));
for (User user : users) {
String roleName = UserRole.displayNameOf(user.getUserRole());
data.add(List.of(
safeStr(user.getUserAccount()),
safeStr(user.getUserName()),
exportEmail(user.getUserEmail(), showFullEmail),
roleName != null ? roleName : "未知",
safeDate(user.getCreateTime()),
Boolean.TRUE.equals(user.getIsLogin()) ? "已禁用" : "正常"
));
}
export(response, "用户信息", data);
}
@Override
public void exportOverdueRecords(HttpServletResponse response) {
if (!requireAdmin(response)) {
return;
}
BorrowRecordPageQuery dto = new BorrowRecordPageQuery();
dto.setOverdue(true);
if (!prepareExport(response, dto, borrowRecordMapper.queryCount(dto))) {
return;
}
List<BorrowRecordView> allRecords = borrowRecordMapper.query(dto);
List<List<String>> data = new ArrayList<>();
data.add(List.of("用户名", "书名", "借阅时间", "应还日期", "逾期天数", "罚款金额"));
LocalDateTime now = LocalDateTime.now();
for (BorrowRecordView record : allRecords) {
if (Boolean.TRUE.equals(record.getStatus())) continue;
if (record.getDueDate() == null || !now.isAfter(record.getDueDate())) continue;
long overdueDays = ChronoUnit.DAYS.between(record.getDueDate(), now);
data.add(List.of(
safeStr(record.getUserName()),
safeStr(record.getBookName()),
safeDate(record.getBorrowTime()),
safeDate(record.getDueDate()),
String.valueOf(overdueDays),
record.getFineAmount() != null ? record.getFineAmount().toString() : "0"
));
}
export(response, "逾期记录", data);
}
}