人员文件包下载

This commit is contained in:
方亮 2025-12-18 17:45:26 +08:00
parent 32eaee2930
commit aa2828252e
7 changed files with 392 additions and 0 deletions

View File

@ -0,0 +1,208 @@
package com.bonus.bmw.controller;
import com.bonus.bmw.domain.po.DownloadTask;
import com.bonus.bmw.domain.po.ZipFileMapping;
import com.bonus.bmw.domain.vo.BmWorkerAtt;
import com.bonus.bmw.service.ZipDownloadService;
import io.minio.GetObjectArgs;
import io.minio.MinioClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.compress.archivers.zip.Zip64Mode;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
@RestController
@RequestMapping("/zipDownload")
@Slf4j
public class ZipDownloadController {
@Resource
private ZipDownloadService service;
@Resource
private RedisTemplate<String, DownloadTask> redisTemplate;
private static final String TASK_PREFIX = "zip_task:"; // key 前缀
private static final long TASK_EXPIRE_SECONDS = 2 * 3600; // 2小时过期
private final ExecutorService executor = Executors.newFixedThreadPool(4);
private static final MinioClient minioClient = MinioClient.builder()
.endpoint("http://192.168.0.14:9090")
.credentials("minio", "bonus@admin123")
.build();
private static final String bucket = "realname";
@PostMapping("/createZipTask")
public ResponseEntity<DownloadTask> createZipTask(@RequestBody BmWorkerAtt o) {
String taskId = UUID.randomUUID().toString();
String redisKey = TASK_PREFIX + taskId;
DownloadTask task = new DownloadTask(taskId);
// 保存到 Redis设置过期时间
redisTemplate.opsForValue().set(redisKey, task, TASK_EXPIRE_SECONDS, TimeUnit.SECONDS);
//查询文件地址
List<ZipFileMapping> objectNames = service.getFileList(o);
// List<ZipFileMapping> objectNames = new ArrayList<>();
// objectNames.add(new ZipFileMapping("relname/2025/11/11/1f18f8b3c0b14143be1f30db6b078e2d.jpg","工程1/分包1/班组1/张三/人脸/user123_face.jpg"));
// objectNames.add(new ZipFileMapping("relname/2025/11/11/ae3044c218714985ba8977479f712278.jpg","工程1/分包1/班组1/张三/合同/con.jpg"));
// 异步执行打包
executor.submit(() -> {
try {
String zipName = taskId + ".zip";
String zipPath = "/tmp/downloads/" + zipName;
// generateZip(request.getFilePaths(), zipPath); // 你的打包逻辑
generateZipFromMinIO(minioClient, bucket, objectNames, zipPath);
// 更新任务状态
task.setStatus("completed");
// task.setDownloadUrl("/downloadFile/" + zipName);
task.setDownloadUrl("/" + zipName);
redisTemplate.opsForValue().set(redisKey, task, TASK_EXPIRE_SECONDS, TimeUnit.SECONDS);
} catch (Exception e) {
task.setStatus("failed");
task.setErrorMessage(e.getMessage());
redisTemplate.opsForValue().set(redisKey, task, TASK_EXPIRE_SECONDS, TimeUnit.SECONDS);
}
});
return ResponseEntity.accepted().body(task);
}
/**
* 查询任务状态
*/
@GetMapping("/taskStatus/{taskId}")
public ResponseEntity<DownloadTask> getTaskStatus(@PathVariable String taskId) {
String redisKey = TASK_PREFIX + taskId;
DownloadTask task = redisTemplate.opsForValue().get(redisKey);
if (task == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(task);
}
/**
* 下载文件
*/
@GetMapping("/downloadFile/{filename:.+}")
public void serveZipFile(@PathVariable String filename, HttpServletResponse response) throws IOException {
// 1. 基础安全禁止路径遍历
if (filename.contains("..") || filename.startsWith("/") || filename.startsWith("\\")) {
response.sendError(403, "Path traversal not allowed");
return;
}
// 2. 必须以 .zip 结尾
if (!filename.endsWith(".zip")) {
response.sendError(400, "Only .zip files allowed");
return;
}
// 3. 文件名只能包含字母数字中文连字符(-)下划线(_)(.)
// 且不能有连续多个点或非法字符
if (!filename.matches("^[\\w\\u4e00-\\u9fff.-]+$")) {
response.sendError(400, "Invalid characters in filename");
return;
}
// 4. 防止超长文件名可选
if (filename.length() > 255) {
response.sendError(400, "Filename too long");
return;
}
Path zipPath = Paths.get("/tmp/downloads/", filename);
if (!Files.exists(zipPath)) {
response.sendError(404);
return;
}
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=" + filename);
response.setContentLengthLong(Files.size(zipPath));
try (InputStream is = Files.newInputStream(zipPath);
OutputStream os = response.getOutputStream()) {
byte[] buffer = new byte[8192];
int n;
while ((n = is.read(buffer)) > 0) {
os.write(buffer, 0, n);
}
}
}
/**
* 使用 Apache Commons Compress MinIO 打包 ZIP支持 UTF-8 中文
*/
public static void generateZipFromMinIO(MinioClient minioClient, String bucket, List<ZipFileMapping> fileMappings, String zipPath) throws Exception {
Path outputPath = Paths.get(zipPath);
Files.createDirectories(outputPath.getParent());
try (FileOutputStream fos = new FileOutputStream(outputPath.toFile());
BufferedOutputStream bos = new BufferedOutputStream(fos, 64 * 1024);
ZipArchiveOutputStream zipOut = new ZipArchiveOutputStream(bos)) {
zipOut.setEncoding("UTF-8");
zipOut.setUseZip64(Zip64Mode.AsNeeded);
for (ZipFileMapping mapping : fileMappings) {
String objectName = mapping.getObjectName();
String targetPath = mapping.getTargetPath();
// 安全校验
if (objectName == null || targetPath == null ||
objectName.contains("..") || targetPath.contains("..") ||
objectName.startsWith("/") || targetPath.startsWith("/")) {
continue; // 跳过非法项
}
// 统一使用正斜杠兼容 Windows
String entryName = targetPath.replace('\\', '/');
try (InputStream objectStream = minioClient.getObject(
GetObjectArgs.builder()
.bucket(bucket)
.object(objectName)
.build())) {
ZipArchiveEntry entry = new ZipArchiveEntry(entryName);
zipOut.putArchiveEntry(entry);
byte[] buffer = new byte[8192];
int len;
while ((len = objectStream.read(buffer)) > 0) {
zipOut.write(buffer, 0, len);
}
zipOut.closeArchiveEntry();
} catch (Exception e) {
System.err.println("Failed to process object: " + objectName + " -> " + targetPath);
// 可选择继续或中断
}
}
zipOut.finish();
}
}
/**
* 定时清理过期文件
*/
@Scheduled(fixedRate = 3600000) // 每小时清理一次
public void cleanOldZips() throws IOException {
try (Stream<Path> files = Files.list(Paths.get("/tmp/downloads"))) {
files.filter(p -> p.toString().endsWith(".zip"))
.filter(p -> {
try {
return System.currentTimeMillis() - Files.getLastModifiedTime(p).toMillis() > 2 * 3600_000; // 2 小时过期
} catch (IOException e) { return false; }
})
.forEach(p -> {
try { Files.delete(p); } catch (IOException ignored) {}
});
}
}
}

View File

@ -0,0 +1,30 @@
package com.bonus.bmw.domain.po;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DownloadTask implements Serializable {
/**
* 任务ID
*/
private String taskId;
/**
* 状态processing / completed / failed
*/
private String status;
private String downloadUrl;
private String errorMessage;
public DownloadTask(String taskId) {
this.taskId = taskId;
this.status = "processing";
}
}

View File

@ -0,0 +1,14 @@
package com.bonus.bmw.domain.po;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ZipFileMapping {
private String objectName; // MinIO 中的对象名 "user123_face.jpg"
private String targetPath; // ZIP 中的目标路径 "工程1/分包1/班组1/张三/人脸/user123_face.jpg"
}

View File

@ -0,0 +1,11 @@
package com.bonus.bmw.mapper;
import com.bonus.bmw.domain.po.ZipFileMapping;
import com.bonus.bmw.domain.vo.BmWorkerAtt;
import java.util.List;
public interface ZipDownloadMapper {
List<ZipFileMapping> getFileList(BmWorkerAtt o);
}

View File

@ -0,0 +1,11 @@
package com.bonus.bmw.service;
import com.bonus.bmw.domain.po.ZipFileMapping;
import com.bonus.bmw.domain.vo.BmWorkerAtt;
import java.util.List;
public interface ZipDownloadService {
List<ZipFileMapping> getFileList(BmWorkerAtt o);
}

View File

@ -0,0 +1,29 @@
package com.bonus.bmw.service.impl;
import com.bonus.bmw.domain.po.ZipFileMapping;
import com.bonus.bmw.domain.vo.BmWorkerAtt;
import com.bonus.bmw.mapper.ZipDownloadMapper;
import com.bonus.bmw.service.ZipDownloadService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* @author 马三炮
* @date 2025/8/14
*/
@Service
@Slf4j
public class ZipDownloadServiceImpl implements ZipDownloadService {
@Resource
private ZipDownloadMapper mapper;
@Override
public List<ZipFileMapping> getFileList(BmWorkerAtt o) {
return mapper.getFileList(o);
}
}

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bonus.bmw.mapper.ZipDownloadMapper">
<select id="selectPro" resultType="com.bonus.bmw.domain.vo.MapBeanVo">
select
distinct
pp.id,
pp.pro_name as `name`
from pm_project pp
<if test="subId != null">
inner join bm_sub_contract bsc on pp.id = bsc.pro_id and bsc.sub_ein_status = 1 and bsc.is_active = 1
</if>
<if test="workerId != null">
Left join bm_worker_ein_msg bwem on pp.id = bwem.pro_id and bwem.is_active = 1 and bwem.worker_id = #{workerId}
</if>
<where>
pp.is_active = 1 and pp.pro_status != 4
<if test="subComId != null">
and pp.sub_com_id = #{subComId}
</if>
<if test="subId != null">
and bsc.sub_id = #{subId}
</if>
<if test="workerId != null">
and bwem.pro_id is null
and pp.is_shanghai = 1
</if>
</where>
</select>
<select id="getFileList" resultType="com.bonus.bmw.domain.po.ZipFileMapping">
SELECT
bf.file_key as objectName,
CONCAT(bwem.pro_name,'/',bwem.sub_name,'/',bwem.team_name,'/',pw.`name`,'/','人脸/',pw.`name`,bf.file_name) as targetPath
FROM
bm_worker_ein_msg bwem
LEFT JOIN pm_worker pw ON pw.id = bwem.worker_id
left JOIN bm_files bf ON bf.source_id = bwem.worker_id and source_table = 'pm_worker'
WHERE
bwem.pro_id = #{proId} and bf.file_key is not null
<if test="subId != null">
and bwem.sub_id = #{subId}
</if>
<if test="teamId != null">
and bwem.team_id = #{teamId}
</if>
UNION
SELECT
bf.file_key AS objectName,
CONCAT(bwem.pro_name,'/',bwem.sub_name,'/',bwem.team_name,'/',pw.`name`,'/','合同/',pw.`name`,bf.file_name) as targetPath
FROM
bm_worker_ein_msg bwem
LEFT JOIN pm_worker pw ON pw.id = bwem.worker_id
LEFT JOIN bm_worker_contract bwc ON bwc.worker_id = bwem.worker_id
AND bwc.pro_id = bwem.pro_id
AND bwc.is_active = 1
LEFT JOIN bm_files bf ON bf.source_id = bwc.id
AND source_table = 'bm_worker_contract'
WHERE
bwem.pro_id = #{proId} and bf.file_key is not null
<if test="subId != null">
and bwem.sub_id = #{subId}
</if>
<if test="teamId != null">
and bwem.team_id = #{teamId}
</if>
UNION
SELECT
bf.file_key AS objectName,
CONCAT(bwem.pro_name,'/',bwem.sub_name,'/',bwem.team_name,'/',pw.`name`,'/','工资卡/',pw.`name`,bf.file_name) as targetPath
FROM
bm_worker_ein_msg bwem
LEFT JOIN pm_worker pw ON pw.id = bwem.worker_id
LEFT JOIN bm_worker_wage_card bwwc ON bwwc.worker_id = bwem.worker_id
AND bwwc.is_active = 1
LEFT JOIN bm_files bf ON bf.source_id = bwwc.id
AND source_table = 'bm_worker_wage_card'
WHERE
bwem.pro_id = #{proId} and bf.file_key is not null
<if test="subId != null">
and bwem.sub_id = #{subId}
</if>
<if test="teamId != null">
and bwem.team_id = #{teamId}
</if>
</select>
</mapper>