人员文件包下载
This commit is contained in:
parent
32eaee2930
commit
aa2828252e
|
|
@ -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) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
Loading…
Reference in New Issue