@@ -26,24 +26,22 @@ import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource ;
import java.io.ByteArrayOutputStream ;
import java.io.FileOutputStream ;
import java.io.IOException ;
import java.io.InputStream ;
import java.nio.file.Files ;
import java.nio.file.Path ;
import java.nio.file.Paths ;
import java.time.LocalDateTime ;
import java.time.format.DateTimeFormatter ;
import java.util.* ;
import java.io.ByteArrayOutputStream ;
import java.io.IOException ;
import jakarta.servlet.http.HttpServletResponse ;
import java.util.zip.ZipEntry ;
import org.apache.poi.ss.usermodel.Cell ;
import org.apache.poi.ss.usermodel.Row ;
import org.apache.poi.ss.usermodel.Sheet ;
import org.apache.poi.xssf.usermodel.XSSFWorkbook ;
import java.nio.file.Files ;
import java.nio.file.Path ;
import java.nio.file.Paths ;
import java.io.FileOutputStream ;
import java.time.format.DateTimeFormatter ;
@Service
@Slf4j
@@ -265,11 +263,9 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
r . createCell ( ci + + ) . setCellValue ( cst . getPhoneNumber ( ) = = null ? " " : cst . getPhoneNumber ( ) ) ;
r . createCell ( ci + + ) . setCellValue ( cst . getDesignTitle ( ) = = null ? " " : cst . getDesignTitle ( ) ) ;
r . createCell ( ci + + ) . setCellValue ( cst . getDesignDescription ( ) = = null ? " " : cst . getDesignDescription ( ) ) ;
// r. createCell(ci++).setCellValue(cst.getPdfPath() == null ? "" : cst.getPdfPath()) ;
// r. createCell(ci++).setCellValue(cst.getVideoPath() == null ? "" : cst. getVideoPath()) ;
// 视频时长(秒)
r . createCell( ci + + ) . setCellValue ( cst . getPdfPath ( ) = = null ? " " : cst . getPdfPath ( ) ) ;
r . createCell( ci + + ) . setCellValue ( cst . getVideoPath ( ) = = null ? " " : cst . getVideoPath( ) ) ;
r . createCell ( ci + + ) . setCellValue ( cst . getVideoDuration ( ) = = null ? " " : cst . getVideoDuration ( ) . toString ( ) ) ;
// 视频大小、PDF 大小:以 MB 导出,保留两位小数
if ( cst . getVideoSize ( ) = = null ) {
r . createCell ( ci + + ) . setCellValue ( " " ) ;
} else {
@@ -332,11 +328,9 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
r . createCell ( ci + + ) . setCellValue ( cst . getPhoneNumber ( ) = = null ? " " : cst . getPhoneNumber ( ) ) ;
r . createCell ( ci + + ) . setCellValue ( cst . getDesignTitle ( ) = = null ? " " : cst . getDesignTitle ( ) ) ;
r . createCell ( ci + + ) . setCellValue ( cst . getDesignDescription ( ) = = null ? " " : cst . getDesignDescription ( ) ) ;
// r. createCell(ci++).setCellValue(cst.getPdfPath() == null ? "" : cst.getPdfPath()) ;
// r. createCell(ci++).setCellValue(cst.getVideoPath() == null ? "" : cst. getVideoPath()) ;
// 视频时长(秒)
r . createCell( ci + + ) . setCellValue ( cst . getPdfPath ( ) = = null ? " " : cst . getPdfPath ( ) ) ;
r . createCell( ci + + ) . setCellValue ( cst . getVideoPath ( ) = = null ? " " : cst . getVideoPath( ) ) ;
r . createCell ( ci + + ) . setCellValue ( cst . getVideoDuration ( ) = = null ? " " : cst . getVideoDuration ( ) . toString ( ) ) ;
// 视频大小、PDF 大小:以 MB 导出,保留两位小数
if ( cst . getVideoSize ( ) = = null ) {
r . createCell ( ci + + ) . setCellValue ( " " ) ;
} else {
@@ -480,7 +474,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
}
@Override
public int exportContestantFiles ( Integer minContestantNumber , Integer maxContestantNumber ) throws Exception {
public byte [ ] exportContestantFilesAsZip ( Integer minContestantNumber , Integer maxContestantNumber ) throws Exception {
if ( minContestantNumber = = null | | maxContestantNumber = = null ) {
throw new BusinessException ( " minContestantNumber and maxContestantNumber are required. " ) ;
}
@@ -488,7 +482,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
throw new BusinessException ( " minContestantNumber cannot be greater than maxContestantNumber. " ) ;
}
// 1. 根据contestantNumber范围查询参赛者
// 1. 根据 contestantNumber 范围查询参赛者
QueryWrapper < Contestant > queryWrapper = new QueryWrapper < > ( ) ;
queryWrapper . lambda ( )
. ge ( Contestant : : getContestantNumber , minContestantNumber )
@@ -498,90 +492,112 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
if ( contestants . isEmpty ( ) ) {
log . info ( " No contestants found in range [{}, {}] " , minContestantNumber , maxContestantNumber ) ;
return 0 ;
return new byte [ 0 ] ;
}
// 2. 创建基础目录
String baseDir = uploadDir + " /contestants " ;
Path basePath = Paths . get ( baseDir ) . toAbsolutePath ( ) ;
Files . createDirectories ( basePath ) ;
log . info ( " Base directory created: {} " , basePath ) ;
// 2. 在内存中构建 ZIP
try ( ByteArrayOutputStream bao s = new ByteArrayOutputStream ( ) ;
java . util . zip . ZipOutputStream zos = new java . util . zip . ZipOutputStream ( baos ) ) {
int exportedCou nt = 0 ;
// 3. 遍历每个参赛者,下载文件
for ( Contestant contestant : contestants ) {
Integer contestantNumber = contestant . getContestantNumber ( ) ;
if ( contestantNumber = = null ) {
log . warn ( " Contestant {} has no contestantNumber, skipping " , contestant . getId ( ) ) ;
continue ;
}
// 创建参赛者文件夹
String contestantDir = baseDir + " / " + contestantNumber ;
Path contestantPath = Paths . get ( contestantDir ) ;
Files . createDirectories ( contestantPath ) ;
// 下载PDF文件
String pdfPath = contestant . getPdfPath ( ) ;
if ( StringUtils . isNotBlank ( pdfPath ) ) {
try {
String fileName = pdfPath . contains ( " / " ) ?
pdfPath . substring ( pdfPath . lastIndexOf ( " / " ) + 1 ) : " design.pdf " ;
downloadFileFromMinio ( pdfPath , contestantPath . toString ( ) , " design.pdf " ) ;
log . info ( " Downloaded PDF for contestant {} " , fileName ) ;
} catch ( Exception e ) {
log . error ( " Failed to download PDF for contestant {}: {} " , contestantNumber , e . getMessage ( ) ) ;
for ( Contestant contesta nt : contestants ) {
Integer contestantNumber = contestant . getContestantNumber ( ) ;
if ( contestantNumber = = null ) {
log . warn ( " Contestant {} has no contestantNumber, skipping " , contestant. getId ( ) ) ;
continue ;
}
}
// 下载视频文件
String videoPath = contestant . getVideoPath ( ) ;
if ( StringUtils . isNotBlank ( videoPath ) ) {
try {
// 根据路径判断视频格式
String dirPrefix = contestantNumber + " / " ;
// 添加 PDF 文件
String pdfPath = contestant . getPdfPath ( ) ;
if ( StringUtils . isNotBlank ( pdfPath ) ) {
addMinioFileToZip ( zos , pdfPath , dirPrefix + " design.pdf " ) ;
}
// 添加视频文件
String videoPath = contestant . getVideoPath ( ) ;
if ( StringUtils . isNotBlank ( videoPath ) ) {
String fileName = videoPath . contains ( " / " ) ?
videoPath . substring ( videoPath . lastIndexOf ( " / " ) + 1 ) : " video.mp4 " ;
downloadFileFromMinio ( videoPath , contestantPath . toString ( ) , fileName ) ;
log . info ( " Downloaded video for contestant {} " , contestantNumber ) ;
} catch ( Exception e ) {
log . error ( " Failed to download video for contestant {}: {} " , contestantNumber , e . getMessage ( ) ) ;
addMinioFileToZip ( zos , videoPath , dirPrefix + fileName ) ;
}
// 添加参赛者信息 txt 文件
StringBuilder sb = new StringBuilder ( ) ;
sb . append ( " === Contestant Information === \ n \ n " ) ;
sb . append ( " ID: " ) . append ( nullSafe ( contestant . getId ( ) ) ) . append ( " \ n " ) ;
sb . append ( " Email: " ) . append ( nullSafe ( contestant . getEmail ( ) ) ) . append ( " \ n " ) ;
sb . append ( " Contestant Number: " ) . append ( contestantNumber ) . append ( " \ n " ) ;
sb . append ( " First Name: " ) . append ( nullSafe ( contestant . getFirstName ( ) ) ) . append ( " \ n " ) ;
sb . append ( " Last Name: " ) . append ( nullSafe ( contestant . getLastName ( ) ) ) . append ( " \ n " ) ;
sb . append ( " Gender: " ) . append ( nullSafe ( contestant . getGender ( ) ) ) . append ( " \ n " ) ;
sb . append ( " Occupation: " ) . append ( nullSafe ( contestant . getOccupation ( ) ) ) . append ( " \ n " ) ;
sb . append ( " Age: " ) . append ( contestant . getAge ( ) ! = null ? contestant . getAge ( ) : " N/A " ) . append ( " \ n " ) ;
sb . append ( " Country/Region/City: " ) . append ( nullSafe ( contestant . getCountryRegionCity ( ) ) ) . append ( " \ n " ) ;
sb . append ( " Phone Number: " ) . append ( nullSafe ( contestant . getPhoneNumber ( ) ) ) . append ( " \ n " ) ;
sb . append ( " Design Title: " ) . append ( nullSafe ( contestant . getDesignTitle ( ) ) ) . append ( " \ n " ) ;
sb . append ( " Design Description: " ) . append ( nullSafe ( contestant . getDesignDescription ( ) ) ) . append ( " \ n " ) ;
sb . append ( " PDF Path: " ) . append ( nullSafe ( pdfPath ) ) . append ( " \ n " ) ;
sb . append ( " PDF Size (bytes): " ) . append ( contestant . getPdfSize ( ) ! = null ? contestant . getPdfSize ( ) : " N/A " ) . append ( " \ n " ) ;
sb . append ( " Video Path: " ) . append ( nullSafe ( videoPath ) ) . append ( " \ n " ) ;
sb . append ( " Video Duration (seconds): " ) . append ( contestant . getVideoDuration ( ) ! = null ? contestant . getVideoDuration ( ) : " N/A " ) . append ( " \ n " ) ;
sb . append ( " Video Size (bytes): " ) . append ( contestant . getVideoSize ( ) ! = null ? contestant . getVideoSize ( ) : " N/A " ) . append ( " \ n " ) ;
sb . append ( " Created At: " ) . append ( contestant . getCreatedAt ( ) ! = null ? contestant . getCreatedAt ( ) : " N/A " ) . append ( " \ n " ) ;
sb . append ( " Updated At: " ) . append ( contestant . getUpdatedAt ( ) ! = null ? contestant . getUpdatedAt ( ) : " N/A " ) . append ( " \ n " ) ;
ZipEntry infoEntry = new ZipEntry ( dirPrefix + " contestant_info.txt " ) ;
zos . putNextEntry ( infoEntry ) ;
zos . write ( sb . toString ( ) . getBytes ( java . nio . charset . StandardCharsets . UTF_8 ) ) ;
zos . closeEntry ( ) ;
log . info ( " Added contestant {} info to zip " , contestantNumber ) ;
}
exportedCount + + ;
zos . finish ( ) ;
log . info ( " ZIP built for {} contestants, size: {} bytes " , contestants . size ( ) , baos . size ( ) ) ;
return baos . toByteArray ( ) ;
}
log . info ( " Exported {} contestants' files to {} " , exportedCount , basePath ) ;
return exportedCount ;
}
/**
* 从 MinIO下载文件到本地
* @param minioPath MinIO路径 (格式: bucketName/objectPath)
* @param localDir 本地目录
* @param fileName 本地文件名
* 将 MinIO 文件流式写入 ZIP, 不落盘
* @param zos ZIP 输出流
* @param minioPath MinIO 路径(格式: bucketName/objectPath)
* @param entryName ZIP 条目名称
*/
private void downloadFileFromMinio ( String minioPath , String localDir , String file Name) {
private void addMinioFileToZip ( java . util . zip . ZipOutputStream zos , String minioPath , String entry Name) {
if ( StringUtils . isBlank ( minioPath ) ) {
return ;
}
// 从路径中提取bucket名称和对象名称
int index = minioPath . indexOf ( " / " ) ;
if ( index = = - 1 ) {
log . warn ( " Invalid MinIO path: {} " , minioPath ) ;
return ;
}
String bucketName = minioPath . substring ( 0 , index ) ;
String objectName = minioPath . substring ( index + 1 ) ;
// 构建本地文件完整路径
Path localFilePath = Paths . get ( localDir , file Name ) ;
try ( InputStream in = minioUtil . download ( bucketName , objectName ) ) {
ZipEntry entry = new ZipEntry ( entry Name ) ;
zos . putNextEntry ( entry ) ;
byte [ ] buffer = new byte [ 8192 ] ;
int bytesRead ;
while ( ( bytesRead = in . read ( buffer ) ) ! = - 1 ) {
zos . write ( buffer , 0 , bytesRead ) ;
}
zos . closeEntry ( ) ;
log . info ( " Added {} to zip ({} bytes) " , entryName , entry . getSize ( ) ) ;
} catch ( Exception e ) {
log . error ( " Failed to add {} to zip: {} " , entryName , e . getMessage ( ) ) ;
}
}
// 下载文件
minioUtil . downloadMinioObjectToLocal ( bucketName , objectName , localFilePath . toString ( ) ) ;
@Override
public long getContestantCount ( ) {
return contestantMapper . selectCount ( null ) ;
}
private String nullSafe ( String value ) {
return value ! = null ? value : " N/A " ;
}
}