feat: 将统计数据导出从Excel改为CSV格式

This commit is contained in:
2026-01-13 23:28:45 +08:00
parent 90c2b36739
commit ae2c359101
4 changed files with 129 additions and 96 deletions

View File

@@ -32,7 +32,7 @@ public class StatisticsController {
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<byte[]> exportActivityData(
@PathVariable Long activityId,
@RequestParam(defaultValue = "excel") String format) {
@RequestParam(defaultValue = "csv") String format) {
return statisticsService.exportActivityData(activityId, format);
}

View File

@@ -12,7 +12,7 @@ import com.campus.activity.mapper.CheckInMapper;
import com.campus.activity.mapper.RegistrationMapper;
import com.campus.activity.mapper.ReviewMapper;
import com.campus.activity.service.StatisticsService;
import com.campus.activity.util.ExcelUtil;
import com.campus.activity.util.CsvUtil;
import com.campus.activity.vo.ActivityStatisticsVO;
import com.campus.activity.vo.OverviewStatisticsVO;
import lombok.RequiredArgsConstructor;
@@ -34,7 +34,7 @@ public class StatisticsServiceImpl implements StatisticsService {
private final CheckInMapper checkInMapper;
private final ReviewMapper reviewMapper;
private final ActivityMapper activityMapper;
private final ExcelUtil excelUtil;
private final CsvUtil csvUtil;
@Override
public ActivityStatisticsVO getActivityStatistics(Long activityId) {
@@ -98,21 +98,15 @@ public class StatisticsServiceImpl implements StatisticsService {
throw new BusinessException(ResultCode.ACTIVITY_NOT_FOUND);
}
List<Registration> registrations = registrationMapper.selectList(
new LambdaQueryWrapper<Registration>()
.eq(Registration::getActivityId, activityId)
.eq(Registration::getStatus, 1)
);
byte[] csvBytes = csvUtil.exportActivityData(activity);
byte[] excelBytes = excelUtil.exportActivityData(activity, registrations);
String filename = "activity_" + activityId + "_data.xlsx";
String filename = "activity_" + activityId + "_statistics.csv";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + filename)
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(excelBytes.length)
.body(excelBytes);
.contentType(MediaType.parseMediaType("text/csv; charset=UTF-8"))
.contentLength(csvBytes.length)
.body(csvBytes);
}
@Override

View File

@@ -0,0 +1,121 @@
package com.campus.activity.util;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.campus.activity.entity.Activity;
import com.campus.activity.entity.CheckIn;
import com.campus.activity.entity.Registration;
import com.campus.activity.entity.Review;
import com.campus.activity.mapper.CheckInMapper;
import com.campus.activity.mapper.RegistrationMapper;
import com.campus.activity.mapper.ReviewMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Map;
@Component
@RequiredArgsConstructor
public class CsvUtil {
private final RegistrationMapper registrationMapper;
private final CheckInMapper checkInMapper;
private final ReviewMapper reviewMapper;
public byte[] exportActivityData(Activity activity) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
// 写入BOM头让Excel正确识别UTF-8编码
outputStream.write(0xEF);
outputStream.write(0xBB);
outputStream.write(0xBF);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 写入表头
StringBuilder header = new StringBuilder();
header.append("活动ID,活动名称,开始时间,结束时间,活动地点,报名人数上限,报名人数,签到人数,签到率,评价人数,平均评分,1星,2星,3星,4星,5星");
outputStream.write(header.toString().getBytes(StandardCharsets.UTF_8));
outputStream.write("\n".getBytes(StandardCharsets.UTF_8));
// 统计数据
long registeredCount = registrationMapper.selectCount(
new LambdaQueryWrapper<Registration>()
.eq(Registration::getActivityId, activity.getId())
.in(Registration::getStatus, 1, 2)
);
List<CheckIn> checkIns = checkInMapper.selectList(
new LambdaQueryWrapper<CheckIn>()
.eq(CheckIn::getActivityId, activity.getId())
);
long checkedInCount = checkIns.size();
double checkInRate = registeredCount > 0 ? (double) checkedInCount / registeredCount : 0.0;
List<Review> reviews = reviewMapper.selectList(
new LambdaQueryWrapper<Review>()
.eq(Review::getActivityId, activity.getId())
);
long reviewCount = reviews.size();
double averageRating = reviews.isEmpty() ? 0.0 :
reviews.stream().mapToInt(Review::getRating).average().orElse(0.0);
// 评分分布
Map<Integer, Long> ratingDistribution = new java.util.HashMap<>();
for (int i = 1; i <= 5; i++) {
final int rating = i;
long count = reviews.stream().filter(r -> r.getRating() == rating).count();
ratingDistribution.put(rating, count);
}
// 写入数据行
StringBuilder dataLine = new StringBuilder();
dataLine.append(escapeCsv(String.valueOf(activity.getId()))).append(",");
dataLine.append(escapeCsv(activity.getTitle())).append(",");
dataLine.append(escapeCsv(activity.getStartTime() != null ? activity.getStartTime().format(formatter) : "")).append(",");
dataLine.append(escapeCsv(activity.getEndTime() != null ? activity.getEndTime().format(formatter) : "")).append(",");
dataLine.append(escapeCsv(activity.getLocation())).append(",");
dataLine.append(escapeCsv(String.valueOf(activity.getMaxParticipants()))).append(",");
dataLine.append(escapeCsv(String.valueOf(registeredCount))).append(",");
dataLine.append(escapeCsv(String.valueOf(checkedInCount))).append(",");
dataLine.append(escapeCsv(String.format("%.2f%%", checkInRate * 100))).append(",");
dataLine.append(escapeCsv(String.valueOf(reviewCount))).append(",");
dataLine.append(escapeCsv(String.format("%.1f", averageRating))).append(",");
dataLine.append(escapeCsv(String.valueOf(ratingDistribution.getOrDefault(1, 0L)))).append(",");
dataLine.append(escapeCsv(String.valueOf(ratingDistribution.getOrDefault(2, 0L)))).append(",");
dataLine.append(escapeCsv(String.valueOf(ratingDistribution.getOrDefault(3, 0L)))).append(",");
dataLine.append(escapeCsv(String.valueOf(ratingDistribution.getOrDefault(4, 0L)))).append(",");
dataLine.append(escapeCsv(String.valueOf(ratingDistribution.getOrDefault(5, 0L))));
outputStream.write(dataLine.toString().getBytes(StandardCharsets.UTF_8));
return outputStream.toByteArray();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("导出CSV失败: " + e.getMessage(), e);
} finally {
try {
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private String escapeCsv(String value) {
if (value == null) {
return "";
}
// 如果包含逗号、引号或换行符,需要用引号包裹并转义
if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
return "\"" + value.replace("\"", "\"\"") + "\"";
}
return value;
}
}

View File

@@ -1,82 +0,0 @@
package com.campus.activity.util;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
import com.alibaba.excel.write.metadata.style.WriteFont;
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
import com.campus.activity.entity.Activity;
import com.campus.activity.entity.Registration;
import com.campus.activity.entity.User;
import com.campus.activity.mapper.UserMapper;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;
@Component
@RequiredArgsConstructor
public class ExcelUtil {
private final UserMapper userMapper;
public byte[] exportActivityData(Activity activity, List<Registration> registrations) {
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
WriteCellStyle headWriteCellStyle = new WriteCellStyle();
WriteFont headWriteFont = new WriteFont();
headWriteFont.setFontHeightInPoints((short) 11);
headWriteFont.setBold(true);
headWriteCellStyle.setWriteFont(headWriteFont);
headWriteCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
HorizontalCellStyleStrategy horizontalCellStyleStrategy =
new HorizontalCellStyleStrategy(headWriteCellStyle, contentWriteCellStyle);
List<RegistrationExportData> dataList = new ArrayList<>();
for (Registration registration : registrations) {
User user = userMapper.selectById(registration.getUserId());
RegistrationExportData data = new RegistrationExportData();
data.setStudentId(user != null ? user.getStudentId() : "");
data.setName(user != null ? user.getName() : "");
data.setUsername(user != null ? user.getUsername() : "");
data.setEmail(user != null ? user.getEmail() : "");
data.setPhone(user != null ? user.getPhone() : "");
data.setTicketCode(registration.getTicketCode());
data.setRegistrationTime(registration.getCreatedAt());
data.setStatus(registration.getStatus() == 1 ? "已报名" :
registration.getStatus() == 2 ? "已签到" : "已取消");
dataList.add(data);
}
EasyExcel.write(outputStream)
.head(RegistrationExportData.class)
.registerWriteHandler(horizontalCellStyleStrategy)
.sheet("报名数据")
.doWrite(dataList);
return outputStream.toByteArray();
} catch (Exception e) {
throw new RuntimeException("导出Excel失败", e);
}
}
@lombok.Data
public static class RegistrationExportData {
private String studentId;
private String name;
private String username;
private String email;
private String phone;
private String ticketCode;
private java.time.LocalDateTime registrationTime;
private String status;
}
}