缺陷删除记录功能开发

This commit is contained in:
cwchen 2026-01-07 18:12:30 +08:00
parent 2492d7561b
commit 5bb5940307
23 changed files with 1440 additions and 99 deletions

View File

@ -0,0 +1,113 @@
package com.bonus.boot.manager.defectStatistics.controller;
import com.bonus.boot.manager.defectStatistics.entity.DayDefectRateDto;
import com.bonus.boot.manager.defectStatistics.entity.ExportTask;
import com.bonus.boot.manager.defectStatistics.entity.TowsDelVo;
import com.bonus.boot.manager.defectStatistics.service.IDefectDelRecordService;
import com.bonus.boot.manager.defectStatistics.service.impl.ExportService;
import com.bonus.boot.manager.manager.annotation.DecryptAndVerify;
import com.bonus.boot.manager.manager.annotation.LogAnnotation;
import com.bonus.boot.manager.manager.entity.EncryptedReq;
import com.bonus.boot.manager.manager.utils.ServerResponse;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @className:DefectDelController
* @author:cwchen
* @date:2026-01-07-14:00
* @version:1.0
* @description:缺陷删除记录-web层
*/
@RestController
@RequestMapping("/defectDelRecord")
public class DefectDelRecordController {
@Resource(name = "IDefectDelRecordService")
private IDefectDelRecordService service;
@Resource(name = "ExportService")
private ExportService exportService;
@PostMapping(value = "getList")
@DecryptAndVerify(decryptedClass = DayDefectRateDto.class)//加解密统一管理
@LogAnnotation(operModul = "缺陷删除记录", operation = "查询数据概览", operDesc = "系统级事件", operType = "查询")
public ServerResponse getList(EncryptedReq<DayDefectRateDto> data) {
PageHelper.startPage(data.getData().getPage(), data.getData().getLimit());
List<TowsDelVo> list = service.getList(data.getData());
PageInfo<TowsDelVo> pageInfo = new PageInfo<>(list);
return ServerResponse.createSuccessPage(pageInfo, data.getData().getPage(), data.getData().getLimit());
}
/**
* 1. 启动导出任务
*/
@GetMapping("/export")
@DecryptAndVerify(decryptedClass = DayDefectRateDto.class)//加解密统一管理
public Map<String, Object> startExport(EncryptedReq<DayDefectRateDto> encryptedReq) {
// 启动异步任务
String taskId = exportService.createExportTask(encryptedReq.getData());
Map<String, Object> data = new HashMap<>();
data.put("taskId", taskId);
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "任务已启动");
result.put("data", data);
return result;
}
/**
* 2. 查询进度
* 对应前端: getExportProgressAPI
*/
@GetMapping("/progress")
@DecryptAndVerify(decryptedClass = DayDefectRateDto.class)//加解密统一管理
public Map<String, Object> getProgress(EncryptedReq<DayDefectRateDto> encryptedReq) {
ExportTask task = exportService.getTaskStatus(encryptedReq.getData().getTaskId());
// 修改 404 情况
if (task == null) {
Map<String, Object> errorResult = new HashMap<>();
errorResult.put("code", 404);
errorResult.put("msg", "任务不存在");
return errorResult;
}
// 修改成功情况
Map<String, Object> successResult = new HashMap<>();
successResult.put("code", 200);
successResult.put("data", task);
return successResult;
}
/**
* 3. 下载文件
*/
@GetMapping("/download")
@DecryptAndVerify(decryptedClass = DayDefectRateDto.class)//加解密统一管理
public ResponseEntity<org.springframework.core.io.Resource> downloadFile(
EncryptedReq<DayDefectRateDto> encryptedReq) throws IOException {
File file = exportService.getExportFile(encryptedReq.getData().getTaskId());
if (file == null || !file.exists()) {
return ResponseEntity.notFound().build();
}
org.springframework.core.io.Resource resource = new org.springframework.core.io.FileSystemResource(file);
String fileName = URLEncoder.encode(file.getName(), "UTF-8").replaceAll("\\+", "%20");
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"")
.body(resource);
}
}

View File

@ -1,5 +1,6 @@
package com.bonus.boot.manager.defectStatistics.dao;
import com.bonus.boot.manager.defectStatistics.entity.ComprehensiveVo;
import com.bonus.boot.manager.defectStatistics.entity.DayDefectRateDto;
import com.bonus.boot.manager.defectStatistics.entity.DayDefectRateVo;
import org.apache.ibatis.annotations.Param;
@ -45,4 +46,22 @@ public interface IDayDefectRateDao {
* @date 2025/12/10 15:18
*/
void delTowsData(DayDefectRateDto dto);
/**
* 获取缺陷详情
* @param dto
* @return ComprehensiveVo
* @author cwchen
* @date 2026/1/7 16:48
*/
ComprehensiveVo getDefectDetail(DayDefectRateDto dto);
/**
* 记录删除的图片
* @param vo
* @return void
* @author cwchen
* @date 2026/1/7 16:52
*/
void addTowsDelData(ComprehensiveVo vo);
}

View File

@ -0,0 +1,37 @@
package com.bonus.boot.manager.defectStatistics.dao;
import com.bonus.boot.manager.defectStatistics.entity.DayDefectRateDto;
import com.bonus.boot.manager.defectStatistics.entity.ImageRecord;
import com.bonus.boot.manager.defectStatistics.entity.TowsDelVo;
import org.springframework.stereotype.Repository;
import java.util.List;
/**
* @className:IDefectDelRecordDao
* @author:cwchen
* @date:2026-01-07-14:03
* @version:1.0
* @description:缺陷删除数据层
*/
@Repository(value = "IDefectDelRecordDao")
public interface IDefectDelRecordDao {
/**
* 缺陷删除记录
* @param dto
* @return List<TowsDelVo>
* @author cwchen
* @date 2026/1/7 14:22
*/
List<TowsDelVo> getList(DayDefectRateDto dto);
/**
* 导出缺陷数据查询
* @param dto
* @return List<ImageRecord>
* @author cwchen
* @date 2026/1/7 16:59
*/
List<ImageRecord> getImageData(DayDefectRateDto dto);
}

View File

@ -18,4 +18,6 @@ public class DayDefectRateDto extends PageEntity {
private String startDate;
private String endDate;
private String taskId;
}

View File

@ -0,0 +1,22 @@
package com.bonus.boot.manager.defectStatistics.entity;
import lombok.Data;
/**
* @className:ExportTask
* @author:cwchen
* @date:2025-12-26-10:15
* @version:1.0
* @description:导出任务
*/
@Data
public class ExportTask {
private String taskId;
private Integer progress; // 0 - 100
private String status; // "processing", "completed", "failed"
private String message; // 错误信息
private String downloadUrl;
private String fileName;
private String finalFilePath; // 服务器本地存储路径
private String filePath; // 服务器本地临时文件路径
}

View File

@ -0,0 +1,23 @@
package com.bonus.boot.manager.defectStatistics.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* @className:ImageRecord
* @author:cwchen
* @date:2025-12-26-10:39
* @version:1.0
* @description: 下载图片实体类
*/
@Data
public class ImageRecord {
private String url;
@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date createTime;
}

View File

@ -0,0 +1,31 @@
package com.bonus.boot.manager.defectStatistics.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
/**
* @className:TowsDelVo
* @author:cwchen
* @date:2026-01-07-14:11
* @version:1.0
* @description:缺陷删除-vo
*/
@Data
public class TowsDelVo {
private Long id;
/**图片路径*/
private String imgPath;
/**二次分析图片*/
private String twoAnalysisUrl;
/**删除时间*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date delTime;
/**图片编号*/
private String imgNo;
}

View File

@ -0,0 +1,35 @@
package com.bonus.boot.manager.defectStatistics.service;
import com.bonus.boot.manager.defectStatistics.entity.DayDefectRateDto;
import com.bonus.boot.manager.defectStatistics.entity.ImageRecord;
import com.bonus.boot.manager.defectStatistics.entity.TowsDelVo;
import java.util.List;
/**
* @className:IDefectDelRecordService
* @author:cwchen
* @date:2026-01-07-14:02
* @version:1.0
* @description:缺陷删除记录-业务层
*/
public interface IDefectDelRecordService {
/**
* 缺陷删除记录
* @param data
* @return List<TowsDelVo>
* @author cwchen
* @date 2026/1/7 14:20
*/
List<TowsDelVo> getList(DayDefectRateDto data);
/**
* 导出缺陷数据查询
* @param dto
* @return List<ImageRecord>
* @author cwchen
* @date 2026/1/7 16:59
*/
List<ImageRecord> getImageData(DayDefectRateDto dto);
}

View File

@ -4,6 +4,7 @@ import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import com.bonus.boot.manager.defectStatistics.dao.IDayDefectRateDao;
import com.bonus.boot.manager.defectStatistics.entity.ComprehensiveVo;
import com.bonus.boot.manager.defectStatistics.entity.DayDefectRateDto;
import com.bonus.boot.manager.defectStatistics.entity.DayDefectRateVo;
import com.bonus.boot.manager.defectStatistics.service.IDayDefectRateService;
@ -125,6 +126,10 @@ public class DayDefectRateServiceImpl implements IDayDefectRateService {
@Override
public AjaxResult delTowsData(DayDefectRateDto dto) {
try {
ComprehensiveVo vo = dayDefectRateDao.getDefectDetail(dto);
if (Objects.nonNull(vo)) {
dayDefectRateDao.addTowsDelData(vo);
}
dayDefectRateDao.delTowsData(dto);
return AjaxResult.success();
} catch (Exception e) {

View File

@ -0,0 +1,50 @@
package com.bonus.boot.manager.defectStatistics.service.impl;
import com.bonus.boot.manager.defectStatistics.dao.IDefectDelRecordDao;
import com.bonus.boot.manager.defectStatistics.entity.DayDefectRateDto;
import com.bonus.boot.manager.defectStatistics.entity.ImageRecord;
import com.bonus.boot.manager.defectStatistics.entity.TowsDelVo;
import com.bonus.boot.manager.defectStatistics.service.IDefectDelRecordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
/**
* @className:DefectDelRecordService
* @author:cwchen
* @date:2026-01-07-14:02
* @version:1.0
* @description:缺陷删除业务逻辑层
*/
@Service(value = "IDefectDelRecordService")
@Slf4j
public class DefectDelRecordService implements IDefectDelRecordService {
@Resource(name = "IDefectDelRecordDao")
private IDefectDelRecordDao defectDelRecordDao;
@Override
public List<TowsDelVo> getList(DayDefectRateDto dto) {
try {
List<TowsDelVo> list = defectDelRecordDao.getList(dto);
return list == null ? Collections.<TowsDelVo>emptyList() : list;
} catch (Exception e) {
log.error(e.toString(), e);
return Collections.emptyList();
}
}
@Override
public List<ImageRecord> getImageData(DayDefectRateDto dto) {
try {
List<ImageRecord> list = defectDelRecordDao.getImageData(dto);
return list == null ? Collections.<ImageRecord>emptyList() : list;
} catch (Exception e) {
log.error(e.toString(), e);
return Collections.emptyList();
}
}
}

View File

@ -0,0 +1,290 @@
package com.bonus.boot.manager.defectStatistics.service.impl;
import com.bonus.boot.manager.defectStatistics.entity.DayDefectRateDto;
import com.bonus.boot.manager.defectStatistics.entity.ExportTask;
import com.bonus.boot.manager.defectStatistics.entity.ImageRecord;
import com.bonus.boot.manager.defectStatistics.service.IDefectDelRecordService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* @className:ExportService
* @author:cwchen
* @date:2025-12-26-10:21
* @version:1.0
* @description:识别图片导出-业务层
*/
@Slf4j
@Service(value = "ExportService")
public class ExportService {
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource(name = "IDefectDelRecordService")
private IDefectDelRecordService service;
// Redis Key 前缀
private static final String REDIS_KEY_PREFIX = "EXPORT:TASK:";
// 任务在 Redis 中的过期时间
private static final long EXPIRE_TIME = 24;
// 临时文件存放目录
private static final String TEMP_DIR = System.getProperty("java.io.tmpdir") + "/export_task/";
// 缺陷文件前缀路径
private static final String PREFIX_PATH = "http://10.67.21.124:1905/hfTowsBmw/hftowsf/";
/**
* 创建任务并立即返回 TaskID
*/
public String createExportTask(DayDefectRateDto dto) {
String taskId = UUID.randomUUID().toString();
ExportTask task = new ExportTask();
task.setTaskId(taskId);
task.setProgress(0);
task.setStatus("processing");
// 存入 Redis
saveTaskToRedis(task);
// 异步执行
processExportAsync(taskId, dto);
return taskId;
}
/**
* Redis 获取任务状态
*/
public ExportTask getTaskStatus(String taskId) {
Object obj = redisTemplate.opsForValue().get(REDIS_KEY_PREFIX + taskId);
if (obj instanceof ExportTask) {
return (ExportTask) obj;
}
return null;
}
public File getExportFile(String taskId) {
ExportTask task = getTaskStatus(taskId);
// 注意文件路径还是存在本地磁盘的
// 如果是多实例部署这里需要改为上传到 OSS/S3 并返回云存储链接
// 或者使用 NFS/共享存储挂载该目录
// 下面代码假设仍在单机磁盘或挂载盘上
if (task != null && task.getFinalFilePath() != null) {
return new File(task.getFinalFilePath());
}
return null;
}
/**
* 辅助方法保存/更新任务到 Redis
*/
private void saveTaskToRedis(ExportTask task) {
String key = REDIS_KEY_PREFIX + task.getTaskId();
// 设置 Key 过期时间
redisTemplate.opsForValue().set(key, task, EXPIRE_TIME, TimeUnit.HOURS);
}
/**
* 核心异步处理逻辑 (业务逻辑大体不变变的是状态更新方式)
*/
@Async("taskExecutor2")
public void processExportAsync(String taskId, DayDefectRateDto dto) {
// 先从 Redis 拿最新的状态
ExportTask task = getTaskStatus(taskId);
if (task == null) return; // 任务可能已过期或被删
File masterZipFile = null;
try {
List<ImageRecord> allImages = mockDatabaseQuery(dto);
if (allImages.isEmpty()) {
throw new RuntimeException("没有查询到数据");
}
Map<String, List<ImageRecord>> groupedImages = new HashMap<>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (ImageRecord img : allImages) {
String dateStr = sdf.format(img.getCreateTime());
groupedImages.computeIfAbsent(dateStr, k -> new ArrayList<>()).add(img);
}
File tempDir = new File(TEMP_DIR);
if (!tempDir.exists()) tempDir.mkdirs();
String masterFileName = "缺陷数据_" + System.currentTimeMillis() + ".zip";
masterZipFile = new File(tempDir, masterFileName);
int totalDates = groupedImages.size();
int processedCount = 0;
try (FileOutputStream fos = new FileOutputStream(masterZipFile);
ZipOutputStream masterZos = new ZipOutputStream(fos)) {
for (Map.Entry<String, List<ImageRecord>> entry : groupedImages.entrySet()) {
String date = entry.getKey();
List<ImageRecord> dailyImages = entry.getValue();
File dailyZipFile = createDailyZip(date, dailyImages);
ZipEntry zipEntry = new ZipEntry(date + ".zip");
masterZos.putNextEntry(zipEntry);
try (FileInputStream dailyFis = new FileInputStream(dailyZipFile)) {
IOUtils.copy(dailyFis, masterZos);
}
masterZos.closeEntry();
dailyZipFile.delete();
processedCount++;
// 更新 Redis 中的进度
int currentProgress = (int) ((double) processedCount / totalDates * 90);
updateProgressInRedis(taskId, currentProgress);
}
masterZos.finish();
}
// 完成状态更新到 Redis
task.setProgress(100);
task.setStatus("completed");
// 这里生成下载接口的地址
task.setDownloadUrl("/defectDelRecord/download?taskId=" + taskId);
task.setFileName(masterFileName);
task.setFinalFilePath(masterZipFile.getAbsolutePath());
saveTaskToRedis(task);
} catch (Exception e) {
e.printStackTrace();
task.setStatus("failed");
task.setMessage("导出失败: " + e.getMessage());
saveTaskToRedis(task);
if (masterZipFile != null && masterZipFile.exists()) {
masterZipFile.delete();
}
}
}
private void updateProgressInRedis(String taskId, int progress) {
ExportTask task = getTaskStatus(taskId);
if (task != null) {
if (progress > 99) progress = 99;
if (progress > task.getProgress()) {
task.setProgress(progress);
saveTaskToRedis(task); // 写回 Redis
}
}
}
/**
* 生成某一天的压缩包 (已修改支持从服务器本地路径读取文件)
*
* @param dateStr 日期字符串
* @param images 包含本地路径信息的图片记录列表
* @return 压缩后的临时文件
*/
private File createDailyZip(String dateStr, List<ImageRecord> images) throws IOException {
// 1. 确保临时目录存在
File tempDir = new File(TEMP_DIR);
if (!tempDir.exists()) {
tempDir.mkdirs();
}
File dailyZip = new File(TEMP_DIR, "temp_" + dateStr + "_" + UUID.randomUUID() + ".zip");
try (FileOutputStream fos = new FileOutputStream(dailyZip);
ZipOutputStream dailyZos = new ZipOutputStream(fos)) {
for (ImageRecord img : images) {
String imageUrl = PREFIX_PATH + img.getUrl();
// String imageUrl = "http://192.168.0.14:9090/smart-bid/personnelDatabase/2025/11/26/2dcc8d4ad41941c98593446f5eacb536.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=minio%2F20260107%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20260107T091412Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=bc233cd628e21f4f8a62864e461eba04a16d88207ea3b239be9eb774ed958c39";
if (imageUrl == null || !imageUrl.startsWith("http")) {
log.warn("无效的图片URL: {}", imageUrl);
continue;
}
HttpURLConnection connection = null;
try {
// 2. 建立 HTTP 连接
URL url = new URL(imageUrl);
connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setConnectTimeout(5000); // 5秒连接超时
connection.setReadTimeout(10000); // 10秒读取超时
int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
log.error("下载失败, HTTP响应码: {}, URL: {}", responseCode, imageUrl);
writeErrorEntry(dailyZos, imageUrl, "HTTP Error: " + responseCode);
continue;
}
// 3. URL 获取文件名 (或生成唯一名)
String fileName = imageUrl.substring(imageUrl.lastIndexOf('/') + 1);
if (fileName.contains("?")) { // 处理带参数的 URL
fileName = fileName.substring(0, fileName.indexOf('?'));
}
String zipEntryName = UUID.randomUUID().toString().substring(0, 8) + "_" + fileName;
// 4. 创建 Zip 条目并拷贝流
ZipEntry entry = new ZipEntry(zipEntryName);
dailyZos.putNextEntry(entry);
try (InputStream is = connection.getInputStream();
BufferedInputStream bis = new BufferedInputStream(is)) {
// 使用 IOUtils 将网络流直接拷贝到 ZIP
IOUtils.copy(bis, dailyZos);
}
dailyZos.closeEntry();
log.info("成功将远程图片添加到压缩包: {}", imageUrl);
} catch (Exception e) {
log.error("下载远程图片异常: URL={}, Error={}", imageUrl, e.getMessage());
writeErrorEntry(dailyZos, imageUrl, "Exception: " + e.getMessage());
} finally {
if (connection != null) {
connection.disconnect();
}
}
}
}
return dailyZip;
}
/**
* 辅助方法向压缩包写入错误信息文件
*/
private void writeErrorEntry(ZipOutputStream zos, String path, String reason) {
try {
ZipEntry errorEntry = new ZipEntry("error_" + UUID.randomUUID().toString().substring(0, 8) + ".txt");
zos.putNextEntry(errorEntry);
String errorMsg = "读取文件失败: " + path + "\n原因: " + reason;
zos.write(errorMsg.getBytes());
zos.closeEntry();
} catch (IOException ignored) {
}
}
private List<ImageRecord> mockDatabaseQuery(DayDefectRateDto dto) {
return service.getImageData(dto);
}
}

View File

@ -6,6 +6,9 @@ import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池配置启用异步
*
@ -22,4 +25,27 @@ public class AsycTaskExecutorConfig {
return taskExecutor;
}
@Bean("taskExecutor2")
public Executor taskExecutor2() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(5);
// 最大线程数
executor.setMaxPoolSize(10);
// 队列容量
executor.setQueueCapacity(100);
// 线程存活时间
executor.setKeepAliveSeconds(60);
// 线程名称前缀
executor.setThreadNamePrefix("task-executor-");
// 拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
// 等待时间
executor.setAwaitTerminationSeconds(60);
executor.initialize();
return executor;
}
}

View File

@ -54,7 +54,7 @@ public class PermissionController {
private static final Set<Long> ONLINE_IGNORE_IDS = Stream.of(58L, 60L,95L, 72L, 96L, 82L, 75L)
.collect(Collectors.toSet());
private static final Set<Long> OFFNLINE_IGNORE_IDS = Stream.of(101L, 102L)
private static final Set<Long> OFFNLINE_IGNORE_IDS = Stream.of(101L, 102L,103L)
.collect(Collectors.toSet());

View File

@ -0,0 +1,75 @@
package com.bonus.boot.manager.plan;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.concurrent.TimeUnit;
/**
* @className:FileCleanupTask
* @author:cwchen
* @date:2025-12-30-17:07
* @version:1.0
* @description:文件清理任务类
*/
@Slf4j
@Component
@EnableScheduling
public class FileCleanupTask {
// 使用之前定义的临时目录路径
private static final String TEMP_PATH = System.getProperty("java.io.tmpdir") + File.separator + "export_task" + File.separator;
/**
* 每天凌晨 2 点执行清理任务
* cron 表达式:
*/
@Scheduled(cron = "0 0 2 * * ?")
public void cleanupTempFiles() {
log.info("开始执行导出临时文件清理任务...");
File directory = new File(TEMP_PATH);
if (!directory.exists() || !directory.isDirectory()) {
log.info("临时目录不存在,跳过清理。");
return;
}
File[] files = directory.listFiles();
if (files == null || files.length == 0) {
log.info("临时目录为空,无需清理。");
return;
}
long currentTimeMillis = System.currentTimeMillis();
long twentyFourHoursMillis = TimeUnit.HOURS.toMillis(24);
int deleteCount = 0;
for (File file : files) {
try {
// 获取文件属性中的创建时间
BasicFileAttributes attr = Files.readAttributes(file.toPath(), BasicFileAttributes.class);
long creationTime = attr.creationTime().toMillis();
// 如果当前时间 - 创建时间 > 24小时则删除
if (currentTimeMillis - creationTime > twentyFourHoursMillis) {
if (file.delete()) {
deleteCount++;
log.debug("已删除过期临时文件: {}", file.getName());
} else {
log.warn("无法删除文件: {}", file.getName());
}
}
} catch (Exception e) {
log.error("处理文件时出错: {}, 错误: {}", file.getName(), e.getMessage());
}
}
log.info("清理任务完成,共删除 {} 个过期文件。", deleteCount);
}
}

View File

@ -1,96 +0,0 @@
# 服务器配置
server:
port: 1907
servlet:
context-path: /hfTowsBmw
session:
timeout: 10
tomcat:
uri-encoding: UTF-8
max-http-header-size: 102400
# Spring 配置
spring:
# 数据源配置
datasource:
url: jdbc:mysql://192.168.0.14:2009/hftows?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false
username: root
password: Bonus@admin123!
driver-class-name: com.mysql.cj.jdbc.Driver
max-idle: 10
max-wait: 60000
min-idle: 5
initial-size: 5
dynamic:
primary: mysqldb
# Redis 配置
redis:
host: 127.0.0.1
port: 6379
# password: HAY@xyksj666
# 文件上传配置
servlet:
multipart:
enabled: true
max-file-size: -1
max-request-size: -1
http:
multipart:
maxFileSize: 10Mb
maxRequestSize: 10Mb
# 国际化配置
messages:
basename: i18n.lang
encoding: UTF-8
# Thymeleaf 配置
thymeleaf:
cache: false
encoding: UTF-8
mode: HTML5
suffix: .html
prefix: classpath:/static/
# MyBatis 配置
mybatis:
mapper-locations: classpath:mappers/*/*Mapper.xml
type-aliases-package: com.bonus.boot.manager.*.entity
# 日志配置
log:
config: classpath:logback-boot.xml
level:
root: info
my: debug
file: logs/sys-back.log
maxsize: 30MB
# 应用自定义配置
token:
expire:
seconds: 360000
files:
path: /data/files/
win:
path: d:/files/
hfTowsBmw:
aq:
enable: false
# 登录验证码配置
loginCode:
expiration: 3
prefix: login_code
# SIGAR 库路径配置
sigar:
library-paths:
windows64: L:\\dll\\sigar-amd64-winnt.dll
windows32: L:\\dll\\sigar-x86-winnt.dll
linux64: /opt/libs/sigar/libsigar-amd64-linux.so

View File

@ -316,6 +316,11 @@ Angle = Angle
proportion=占比
quantity=数量
roll_angle_rate=Roll/Angle比例
secondary_analysis_picture=二次分析图片
master_drawing=原图
view=查看
deleteTime=删除时间
# laydate 语言
laydateEn =

View File

@ -322,3 +322,7 @@ roughening_num = the number of brushed hairs
roughening_width = burr length
roughening_width_proportion = roughening length ratio
roll_angle_rate=Roll/AngleProportion
secondary_analysis_picture=secondary analysis picture
master_drawing=master drawing
view=view
deleteTime=delete time

View File

@ -321,3 +321,7 @@ roughening_num = 拉毛个数
roughening_width = 毛边长度
roughening_width_proportion = 拉毛长度比
roll_angle_rate=Roll/Angle比例
secondary_analysis_picture=二次分析图片
master_drawing=原图
view=查看
deleteTime=删除时间

View File

@ -2,8 +2,15 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.bonus.boot.manager.defectStatistics.dao.IDayDefectRateDao">
<!--记录删除的图片-->
<insert id="addTowsDelData">
INSERT INTO bm_tows_del(img_path,img_no,two_analysis_url)
VALUES (
#{imgInitialUrl},#{imgNum},#{imgTwoAnalysisUrl}
)
</insert>
<!--每日缺陷率统计-->
<!--每日缺陷率统计-->
<select id="getList" resultType="com.bonus.boot.manager.defectStatistics.entity.DayDefectRateVo">
SELECT
create_date AS createDate,
@ -29,6 +36,13 @@
AND is_active = '1'
GROUP BY DATE_FORMAT(createTime,'%Y-%m-%d')
</select>
<!--获取缺陷详情-->
<select id="getDefectDetail" resultType="com.bonus.boot.manager.defectStatistics.entity.ComprehensiveVo">
SELECT img_initial_url AS imgInitialUrl,
img_num AS imgNum,
img_two_analysis_url AS imgTwoAnalysisUrl
FROM bm_tows WHERE id = #{id}
</select>
<!--删除缺陷图片-->
<update id="delTowsData">

View File

@ -0,0 +1,35 @@
<?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.boot.manager.defectStatistics.dao.IDefectDelRecordDao">
<!--缺陷删除记录-->
<select id="getList" resultType="com.bonus.boot.manager.defectStatistics.entity.TowsDelVo">
SELECT
id,
img_path AS imgPath,
del_time AS delTime,
img_no AS imgNo,
two_analysis_url AS twoAnalysisUrl
FROM bm_tows_del
<where>
<if test="startDate != null and startDate != '' and endDate != null and endDate != ''">
AND del_time BETWEEN CONCAT(#{startDate}, ' 00:00:00') AND CONCAT(#{endDate},' 23:59:59')
</if>
</where>
ORDER BY del_time DESC
</select>
<!--导出缺陷数据查询-->
<select id="getImageData" resultType="com.bonus.boot.manager.defectStatistics.entity.ImageRecord">
SELECT
img_path AS url,
del_time AS createTime
FROM bm_tows_del
<where>
<if test="startDate != null and startDate != '' and endDate != null and endDate != ''">
AND del_time BETWEEN CONCAT(#{startDate}, ' 00:00:00') AND CONCAT(#{endDate},' 23:59:59')
</if>
</where>
ORDER BY del_time DESC
</select>
</mapper>

View File

@ -0,0 +1,272 @@
var form, layer, table, tableIns, formSelects;
// 应用类别下拉选
var baleNumList;
var pageNum = 1, limitSize = 10; // 默认第一页分页数量为10
const isOnline = localStorage.getItem("online");
layui.use(['form', 'layer', 'table', 'laydate'], function () {
form = layui.form;
layer = layui.layer;
table = layui.table;
formSelects = layui.formSelects;
baleNumList = getBaleNumSelectList();
setSelectValue(baleNumList, 'baleNum', package_number_select);
form.render();
var laydate = layui.laydate;
// 日期范围
laydate.render({
elem: "#operateTime",
range: true,
lang: dataLang,
value:new Date().toISOString().split('T')[0] +" - "+new Date().toISOString().split('T')[0],
shortcuts: [
{
text: last_7_days,
value: function () {
var value = [];
var date1 = new Date();
date1.setDate(date1.getDate() - 7);
date1.setHours(0, 0, 0, 0);
value.push(date1);
var date2 = new Date();
date2.setDate(date2.getDate() - 1);
date2.setHours(23, 59, 59, 999);
value.push(date2);
return value;
}
},
{
text: last_30_days,
value: function () {
var value = [];
var date1 = new Date();
date1.setDate(date1.getDate() - 30);
date1.setHours(0, 0, 0, 0);
value.push(date1);
var date2 = new Date();
date2.setDate(date2.getDate() - 1);
date2.setHours(23, 59, 59, 999);
value.push(date2);
return value;
}
},
{
text: last_3_months,
value: function () {
var value = [];
var date1 = new Date();
date1.setMonth(date1.getMonth() - 3);
date1.setDate(1);
date1.setHours(0, 0, 0, 0);
value.push(date1);
var date2 = new Date();
date2.setDate(1);
date2.setHours(0, 0, 0, 0);
date2 = date2.getTime() - 1;
value.push(new Date(date2));
return value;
}
}
]
});
pages(1, 10, 1);
//重置按钮事件
$("#reset").click(function () {
$("#operateTime").val(getNowTime() + " - " + getNowTime());
query();
form.render();
});
});
function pages(pageNum, pageSize, typeNum) {
var params = getReqParams(pageNum, pageSize, typeNum);
var url = dataUrl + "/defectDelRecord/getList";
ajaxRequest(url, "POST", params, true, function () {
}, function (result) {
console.log(result);
if (result.code === 200) {
if (result.data) {
initTable(result.data, result.limit, result.curr)
laypages(result.count, result.curr, result.limit)
}
} else if (result.code === 500) {
layer.alert(result.msg, {icon: 2})
}
}, function (xhr) {
error(xhr)
});
}
function laypages(total, page, limit) {
layui.use(['laypage'], function () {
var laypage = layui.laypage;
laypage.render({
elem: 'voi-page',
count: total,
curr: page,
limit: limit,
limits: [10, 20, 50, 100, 200, 500],
layout: ['prev', 'page', 'next', 'skip', 'count', 'limit'],
groups: 5,
jump: function (obj, first) {
if (!first) {
pageNum = obj.curr, limitSize = obj.limit;
pages(obj.curr, obj.limit, null);
}
}
});
})
}
/*初始化表格*/
function initTable(dataList, limit, page) {
var loadingMsg = layer.msg(load_tip, {icon: 16, scrollbar: false, time: 0,});
tableIns = table.render({
elem: "#table_data",
height: "full-140",
toolbar: '#toolbarDemo',
defaultToolbar: ['filter'], //本章讲述的参数!!!!!!!!!!
data: dataList,
limit: limit,
cols: [
setTableField(page, limit)
],
done: function (res, curr, count) {
layer.close(loadingMsg);
table.resize("table_data");
count || this.elem.next(".layui-table-view").find(".layui-table-header").css("display", "inline-block");
count || this.elem.next(".layui-table-view").find(".layui-table-box").css("overflow", "auto");
},
});
}
function setTableField(page, limit) {
let cols = [
{
title: order_number,
width: '5%',
rowspan: 2,
unresize: true,
align: "center",
style: "word-break: break-all;",
templet: function (d) {
return (page - 1) * limit + d.LAY_INDEX + 1;
}
},
{
field: 'imgNum', width: '30%', align: "center", title: photo_number,
templet: function (d) {
var html = '';
if (d.id == "" || d.id == "" || d.id == "null" || d.id == null) {
return '';
}
html += "<a style='color: #3E689A;font-weight: bold;' onclick='getImgNum2(" + JSON.stringify(d) + ")'>" + d.id + "</a>";
return html;
}
},
{
width: '20%', align: "center", title: master_drawing,
templet: function (d) {
return "<a style='color: #3E689A;font-weight: bold;' onclick='getImgNum("+JSON.stringify(d)+")'>"+view+"</a>";
}
},
{
width: '20%', align: "center", title: secondary_analysis_picture,
templet: function (d) {
return "<a style='color: #3E689A;font-weight: bold;' onclick='getImgNum("+JSON.stringify(d)+")'>"+view+"</a>";
}
},
{field: 'delTime', width: '25%', align: "center", title: deleteTime},
]
return cols;
}
/**原图 / 二次缺陷图片*/
function getImgNum(obj, type) {
layer.photos({
photos: {
"title": photo_details,
"start": 0,
"data": [
{
"alt": "图片详情",
"pid": 1,
// "src": "https://unpkg.com/outeres@0.1.1/demo/layer.png"
"src": imgpathurl + (type === 1 ? obj.imgPath : obj.twoAnalysisUrl),
}
]
},
// --- 调整背景透明度 ---
shade: [0.3, '#000'], // 0.3 表示 30% 透明度的黑色,背景不会那么黑
// -------------------
maxWidth: 800,
maxHeight: 600,
anim: 5
});
}
// 获取参数
function getReqParams(page, limit, type) {
var obj = {};
if (!type) {
obj = {
page: page + "",
limit: limit + "",
baleNum: $('#baleNum').val(),
operateTime: $('#operateTime').val(),
startDate: $('#operateTime').val() ? $('#operateTime').val().split(' - ')[0] : '',
endDate: $('#operateTime').val() ? $('#operateTime').val().split(' - ')[1] : '',
};
} else {
obj = {
page: '1',
limit: '10',
keyWord: '',
operateTime: '',
startDate: $('#operateTime').val() ? $('#operateTime').val().split(' - ')[0] : '',
endDate: $('#operateTime').val() ? $('#operateTime').val().split(' - ')[1] : '',
};
}
obj = {
encryptedData: encryptCBC(JSON.stringify(obj))
}
return obj;
}
// 查询/重置
function query() {
pages(1, limitSize);
}
function reloadData() {
pages(pageNum, limitSize);
}
function exportData() {
// 1. 模拟获取 queryParams (对应 this.queryParams)
const queryParams = {
startDate: $('#operateTime').val() ? $('#operateTime').val().split(' - ')[0] : '',
endDate: $('#operateTime').val() ? $('#operateTime').val().split(' - ')[1] : '',
};
// 3. 将参数编码
const paramsStr = encodeURIComponent(JSON.stringify(queryParams));
// 4. 拼接 URL
const url = `../../pages/defectStatistics/export-progress.html?params=${paramsStr}`;
// 5. 计算窗口居中位置
const width = 750;
const height = 500;
const left = (window.screen.width - width) / 2;
const top = (window.screen.height - height) / 2;
// 6. 打开新窗口
window.open(
url,
'_blank',
`width=${width},height=${height},left=${left},top=${top},scrollbars=yes,resizable=yes`
);
}

View File

@ -0,0 +1,107 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Roll/Angle比例</title>
<link rel="stylesheet" href="../../js/layui-v2.8.17/css/layui.css">
<link rel="stylesheet" href="../../css/table-common2.css">
<link rel="stylesheet" href="../../js/layui-v2.6.8/formSelects-v4.css">
<script src="../../js/libs/jquery-3.6.0.min.js" charset="UTF-8" type="text/javascript"></script>
<script src="../../js/layui-v2.8.17/layui.js" charset="UTF-8" type="text/javascript"></script>
<script src="../../js/publicJs.js"></script>
<script src="../../js/aes.js"></script>
<script src="../../js/ajaxRequest.js"></script>
<script src="../../js/select.js"></script>
<script src="../../js/commonUtils.js"></script>
<script src="../../js/openIframe.js"></script>
</head>
<body>
<div id="content">
<div class="basic-search-box layout">
<form class="layui-form basic-form" onclick="return false;">
<div class="layui-form-item">
<div class="layui-inline" style="padding: 0 0 0 10px;">
<div class="layui-input-inline baleNum" style="display: none">
<select class="layui-select" id="baleNum" lay-search ></select>
</div>
</div>
<div class="layui-inline">
<div class="layui-input-inline">
<input type="text" class="layui-input" id="operateTime" readonly th:placeholder="#{date_prompt}">
</div>
</div>
<div class="layui-inline btns">
<button type="button" class="layui-btn layui-btn-normal layui-btn-sm btn-1" onclick="query(1)">[[#{query_button}]]
</button>
<button type="reset" class="layui-btn layui-btn-primary layui-btn-sm btn-4" id="reset">[[#{resetting_button}]]</button>
<button id="exportBt" class="layui-btn" style="width: 80px" onclick="exportData()">
[[#{export_button}]]
</button>
</div>
</div>
</form>
</div>
<div class="table-box" table-responsive style="z-index: 1;">
<table id="table_data" class="table" lay-filter="table_data">
</table>
<div id="voi-page" class="layout"></div>
</div>
</div>
<script src="../../js/defectStatistics/defectDelRecord.js" charset="UTF-8" type="text/javascript"></script>
<script type="text/javascript" th:inline="javascript">
let package_number_select =[[#{package_number_select}]];
let last_7_days =[[#{last_7_days}]];
let last_30_days =[[#{last_30_days}]];
let last_3_months =[[#{last_3_months}]];
let load_tip =[[#{load_tip}]];
let package_details =[[#{package_details}]];
let export_data_tip =[[#{export_data_tip}]];
let export_error_tip =[[#{export_error_tip}]];
let order_number =[[#{order_number}]];//序号
let roll_Angle_ratio_excel =[[#{roll_Angle_ratio_excel}]];//Roll/Angle比例.xlsx
let photo_details=[[#{photo_details}]];//照片详情
let basic_information=[[#{basic_information}]];//基本信息
let packet_number=[[#{packet_number}]];//包号
let specifications=[[#{specifications}]];//规格
let test_date=[[#{test_date}]];//检测日期
let number_of_defects=[[#{number_of_defects}]];//缺陷数量
let photo_number=[[#{photo_number}]];//照片编号
let detection_area_size=[[#{detection_area_size}]];//检测面积大小
let mean_curl_ratio=[[#{mean_curl_ratio}]];//卷曲比例均值
let maximum_curl_ratio=[[#{maximum_curl_ratio}]];//卷曲比例最大值
let minimum_curl_ratio=[[#{minimum_curl_ratio}]];//卷曲比例最小值
let number_of_bristles=[[#{number_of_bristles}]];//拉毛个数
let burr_length=[[#{burr_length}]];//毛边长度
let betection_length=[[#{betection_length}]];//检测长度
let roughing_length_ratio=[[#{roughing_length_ratio}]];//拉毛长度比
let polluted_colo=[[#{polluted_colo}]];//污染颜色
let polluted_area=[[#{polluted_area}]];//污染面积
let Proportion_of_pollution_area=[[#{Proportion_of_pollution_area}]];//污染面积占比
let number_of_cuts=[[#{number_of_cuts}]];//切断个数
let cut_off_area=[[#{cut_off_area}]];//切断面积
let proportion_of_cutting_area=[[#{proportion_of_cutting_area}]];//切断面积占比
let roll_angle_rate=[[#{roll_angle_rate}]];//rollAngle占比
let operate =[[#{operate}]]; // 操作
let delete_button =[[#{delete_button}]]; // 删除
let delete_tip =[[#{delete_tip}]]; // 删除提示
let message =[[#{message}]];
let confirm =[[#{confirm}]];
let cancel =[[#{cancel}]];
let delete_tip2 =[[#{delete_tip2}]];
let secondary_analysis_picture =[[#{secondary_analysis_picture}]];
let master_drawing =[[#{master_drawing}]];
let view =[[#{view}]];
let deleteTime =[[#{deleteTime}]];
</script>
</body>
</html>

View File

@ -0,0 +1,268 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>导出进度</title>
<script src="../../js/publicJs.js"></script>
<script src="../../js/libs/jquery-3.6.0.min.js" charset="UTF-8" type="text/javascript"></script>
<script src="../../js/aes.js"></script>
<style>
/* 基础样式模拟 */
body { margin: 0; font-family: "Helvetica Neue", Helvetica, "PingFang SC", sans-serif; }
.export-progress-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(180deg, #f1f6ff 20%, #e5efff 100%);
padding: 20px;
}
.progress-card {
width: 500px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.card-header {
padding: 20px 24px;
border-bottom: 1px solid #ebeef5;
}
.card-header .title { font-size: 18px; font-weight: 600; color: #333; margin: 0; }
.card-body { padding: 40px 24px; text-align: center; }
/* 进度条样式 */
.progress-bar-container {
width: 100%;
background-color: #ebeef5;
border-radius: 100px;
margin: 20px 0;
height: 8px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background-color: #1f72ea;
width: 0%;
transition: width 0.3s ease;
}
/* 按钮样式 */
.btn {
display: inline-block;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
border: 1px solid #dcdfe6;
background: #fff;
color: #606266;
text-decoration: none;
font-size: 14px;
}
.btn-primary {
background: #1f72ea;
color: #fff;
border: none;
box-shadow: 0px 4px 8px 0px rgba(51, 135, 255, 0.5);
}
.btn-primary:hover { background: #4a8bff; }
/* 状态控制 */
.status-section { display: none; }
.active { display: block; }
/* 动画模拟 Element Icon */
.loading-icon { display: inline-block; width: 20px; height: 20px; border: 2px solid #1f72ea; border-top-color: transparent; border-radius: 50%; animation: spin 1s linear infinite; vertical-align: middle; margin-right: 10px;}
@keyframes spin { to { transform: rotate(360deg); } }
.error-text { color: #f56c6c; margin-bottom: 20px; }
.success-text { color: #333; margin-bottom: 20px; font-weight: 500; }
</style>
</head>
<body>
<div class="export-progress-container">
<div class="progress-card">
<div class="card-header">
<h3 class="title">导出进度</h3>
</div>
<div class="card-body">
<div id="section-progress" class="status-section active">
<div class="progress-info">
<span class="loading-icon"></span>
<span id="progress-text">0%</span>
</div>
<div class="progress-bar-container">
<div id="progress-fill" class="progress-bar-fill"></div>
</div>
<div class="progress-detail">正在生成压缩包,请稍候...</div>
</div>
<div id="section-completed" class="status-section">
<div style="font-size: 50px; color: #67c23a; margin-bottom: 15px;"></div>
<div class="success-text">压缩包生成完成!</div>
<button id="btn-download" class="btn btn-primary">下载文件</button>
</div>
<div id="section-error" class="status-section">
<div style="font-size: 50px; color: #f56c6c; margin-bottom: 15px;"></div>
<div id="error-message" class="error-text">导出失败,请稍后重试</div>
<button id="btn-close" class="btn">关闭窗口</button>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function() {
// --- 数据定义 ---
let state = {
taskId: null,
downloadUrl: null,
fileName: null,
params: {},
pollTimer: null
};
// --- 初始化 ---
init();
function init() {
const urlParams = new URLSearchParams(window.location.search);
const paramsStr = urlParams.get('params');
if (paramsStr) {
try {
state.params = JSON.parse(decodeURIComponent(paramsStr));
startExport();
} catch (e) {
showError('参数解析失败');
}
} else {
showError('缺少导出参数');
}
}
// --- 方法逻辑 ---
function startExport() {
const params = {
encryptedData: encryptCBC(JSON.stringify(state.params))
}
$.ajax({
url: dataUrl + '/defectDelRecord/export',
method: 'GET',
headers: {
"token": tokens
},
data: params,
contentType: 'application/json',
success: function(res) {
if (res.code === 200) {
// 兼容多种返回格式
state.taskId = res.data?.taskId || res.data || res.taskId;
if (state.taskId) {
startPolling();
} else if (res.data?.downloadUrl) {
showCompleted(res.data.downloadUrl, res.data.fileName);
} else {
showError('未获取到任务ID');
}
} else {
showError(res.msg || '启动导出任务失败');
}
},
error: function() {
showError('网络请求失败');
}
});
}
function startPolling() {
checkProgress(); // 立即执行一次
state.pollTimer = setInterval(checkProgress, 2000);
}
function checkProgress() {
if (!state.taskId) return;
const params = {
encryptedData: encryptCBC(JSON.stringify({
taskId: state.taskId
}))
}
$.ajax({
url: dataUrl + '/defectDelRecord/progress', // 替换为真实URL
method: 'GET',
headers: {
"token": tokens
},
// data: { taskId: state.taskId },
data: params,
success: function(res) {
if (res.code === 200) {
const data = res.data;
updateProgressBar(data.progress || 0);
if (data.progress >= 100 || data.status === 'completed' || data.status === 'success') {
stopPolling();
showCompleted(data.downloadUrl, data.fileName);
} else if (data.status === 'failed' || data.status === 'error') {
stopPolling();
showError(data.message || '导出任务失败');
}
}
}
});
}
function updateProgressBar(percent) {
$('#progress-text').text(percent + '%');
$('#progress-fill').css('width', percent + '%');
}
function showCompleted(url, name) {
state.downloadUrl = url;
state.fileName = name || `export_${Date.now()}.zip`;
switchSection('completed');
}
function showError(msg) {
$('#error-message').text(msg);
switchSection('error');
}
function switchSection(type) {
$('.status-section').removeClass('active');
$(`#section-${type}`).addClass('active');
}
function stopPolling() {
if (state.pollTimer) {
clearInterval(state.pollTimer);
state.pollTimer = null;
}
}
// --- 事件绑定 ---
$('#btn-download').on('click', function() {
if (state.downloadUrl && state.downloadUrl.startsWith('http')) {
window.open(state.downloadUrl, '_blank');
} else {
// 这里可以实现类似 Blob 下载逻辑jQuery 需配合原生 fetch 或 xhr
const params = encryptCBC(JSON.stringify({
taskId: state.taskId
}))
window.location.href = dataUrl + `/defectDelRecord/download?encryptedData=${params}&token=${tokens}`;
}
});
$('#btn-close').on('click', function() {
window.close();
});
});
</script>
</body>
</html>