feat(device): 新增批量PDF导出功能并更新服务配置

- 新增BatchExportRequest和BatchPdfWrapper数据传输对象
- 实现DeviceInstallInfoController中的getPdfBatch接口支持批量PDF下载
- 添加ExportTask类用于管理导出任务状态
- 配置PDFGenerator工具类的批量处理功能
- 注入ExecutorService线程池支持异步任务执行
- 更新application-dev.yml中的数据库、ES和Redis连接地址
- 调整Hibernate SQL日志输出配置
- 优化PDF生成过程中的压缩和文件名处理
- 在告警消息中增加设备位置信息
master
alsszer 17 hours ago
parent 25a8b2150b
commit c9c45cbdf0

@ -38,5 +38,5 @@ public interface DiccidRecordRepository extends JpaRepository<TbDIccidRecordDO,
long count(); long count();
List<TbDIccidRecordDO> findAll(); List<TbDIccidRecordDO> findAll();
TbDIccidRecordDO findByImei(String imei); TbDIccidRecordDO findFirstByImei(String imei);
} }

@ -77,7 +77,7 @@ public class DIccidRecordDataImpl implements IDIccidRecordData, IJPACommData<DIc
@Override @Override
public DIccidRecordDO findByImei(String Imei) { public DIccidRecordDO findByImei(String Imei) {
TbDIccidRecordDO tbDIccidRecordDO = homeRepository.findByImei(Imei); TbDIccidRecordDO tbDIccidRecordDO = homeRepository.findFirstByImei(Imei);
DIccidRecordDO dto = MapstructUtils.convert(tbDIccidRecordDO, DIccidRecordDO.class); DIccidRecordDO dto = MapstructUtils.convert(tbDIccidRecordDO, DIccidRecordDO.class);
return dto; return dto;
} }

@ -18,9 +18,9 @@ import java.nio.file.Paths;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -28,6 +28,9 @@ import java.util.zip.Deflater;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
public class PDFGenerator { public class PDFGenerator {
@ -35,9 +38,6 @@ public class PDFGenerator {
private static volatile PDType0Font cachedFont = null; private static volatile PDType0Font cachedFont = null;
private static final Object fontLock = new Object(); private static final Object fontLock = new Object();
// 缓存已处理的图片以避免重复下载和处理
private static final ConcurrentHashMap<String, PDImageXObject> imageCache = new ConcurrentHashMap<>();
// 线程池用于并发处理图片 // 线程池用于并发处理图片
private static final ExecutorService imageProcessingPool = Executors.newFixedThreadPool(4); private static final ExecutorService imageProcessingPool = Executors.newFixedThreadPool(4);
@ -87,7 +87,7 @@ public class PDFGenerator {
{"报警器编号", entity.getDeviceName()}, {"报警器编号", entity.getDeviceName()},
{"切断阀编号", entity.getShutValueNumber()}, {"切断阀编号", entity.getShutValueNumber()},
{"燃气表号", entity.getGasMeterNumber()}, {"燃气表号", entity.getGasMeterNumber()},
{"备注", entity.getBuildingUnit()} {"备注", entity.getRemarks()}
}; };
drawTable(cs, font, TABLE_MARGIN, currentY, baseData, COLUMN_WIDTHS); drawTable(cs, font, TABLE_MARGIN, currentY, baseData, COLUMN_WIDTHS);
currentY -= (ROW_HEIGHT * baseData.length) + 20f; currentY -= (ROW_HEIGHT * baseData.length) + 20f;
@ -396,25 +396,34 @@ public class PDFGenerator {
return PDType0Font.load(document, new File("/ttf/NotoSansCJK-Regular.ttf")); return PDType0Font.load(document, new File("/ttf/NotoSansCJK-Regular.ttf"));
} }
// 缓存已处理的图片以避免重复下载和处理 // LRU缓存图片字节最多50张30分钟未访问自动淘汰
private static final ConcurrentHashMap<String, byte[]> imageByteCache = new ConcurrentHashMap<>(); private static final Cache<String, byte[]> imageByteCache = CacheBuilder.newBuilder()
.maximumSize(50)
.expireAfterAccess(30, TimeUnit.MINUTES)
.build();
// 预加载图片以避免重复下载 // 预加载图片以避免重复下载
private static PDImageXObject loadImageFromUrl(PDDocument document, String url) throws IOException { private static PDImageXObject loadImageFromUrl(PDDocument document, String url) throws IOException {
byte[] imageBytes = imageByteCache.computeIfAbsent(url, key -> { try {
try { byte[] imageBytes = imageByteCache.get(url, () -> {
return new URL(key).openStream().readAllBytes(); try {
} catch (IOException e) { return new URL(url).openStream().readAllBytes();
return null; // 或者抛出异常 } catch (IOException e) {
throw new RuntimeException(e);
}
});
if (imageBytes == null) {
return null;
} }
});
if (imageBytes == null) { return PDImageXObject.createFromByteArray(document, imageBytes, "");
return null; } catch (Exception e) {
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
}
throw new IOException("Failed to load image: " + url, e);
} }
return PDImageXObject.createFromByteArray(document, imageBytes, "");
} }

@ -58,9 +58,12 @@ public class DeviceInstallInfoExpordVo implements Serializable {
// @ApiModelProperty(value = "用户电话") // @ApiModelProperty(value = "用户电话")
@ExcelProperty(value = "电话") @ExcelProperty(value = "电话")
private String userIpone; private String userIpone;
// @ApiModelProperty(value = "切断阀编号") @ExcelProperty(value = "小区名称")
@ExcelProperty(value = "地址") private String communityName;
private String site; @ExcelProperty(value = "楼栋单元号")
private String buildingUnit;
@ExcelProperty(value = "房间号")
private String roomNo;
@ExcelProperty(value = "切断阀编号") @ExcelProperty(value = "切断阀编号")
private String shutValueNumber; private String shutValueNumber;
@ExcelProperty(value = "报警器编号") @ExcelProperty(value = "报警器编号")
@ -79,8 +82,6 @@ public class DeviceInstallInfoExpordVo implements Serializable {
private String manufacturer; private String manufacturer;
@ExcelProperty(value = "安装情况") @ExcelProperty(value = "安装情况")
private String position; private String position;
/* // @ApiModelProperty(value = "备注") @ExcelProperty(value = "备注")
private String remarks; private String remarks;
private Integer state;*/
} }

@ -61,9 +61,12 @@ public class DeviceInstallInfoVo implements Serializable {
// @ApiModelProperty(value = "用户电话") // @ApiModelProperty(value = "用户电话")
@ExcelProperty(value = "电话") @ExcelProperty(value = "电话")
private String userIpone; private String userIpone;
// @ApiModelProperty(value = "切断阀编号") @ExcelProperty(value = "小区名称")
@ExcelProperty(value = "地址") private String communityName;
private String site; @ExcelProperty(value = "楼栋单元号")
private String buildingUnit;
@ExcelProperty(value = "房间号")
private String roomNo;
@ExcelProperty(value = "切断阀编号") @ExcelProperty(value = "切断阀编号")
private String shutValueNumber; private String shutValueNumber;
@ExcelProperty(value = "报警器编号") @ExcelProperty(value = "报警器编号")
@ -82,8 +85,6 @@ public class DeviceInstallInfoVo implements Serializable {
private String manufacturer; private String manufacturer;
@ExcelProperty(value = "安装情况") @ExcelProperty(value = "安装情况")
private String position; private String position;
/* // @ApiModelProperty(value = "备注") @ExcelProperty(value = "备注")
private String remarks; private String remarks;
private Integer state;*/
} }

@ -225,7 +225,10 @@ public class DeviceInstallInfoServiceImpl implements IDeviceInstallInfoService {
deviceInstallInfoVo.setXuhao(i); deviceInstallInfoVo.setXuhao(i);
deviceInstallInfoVo.setShutValueNumber(deviceInfo.getShutValueNumber()); deviceInstallInfoVo.setShutValueNumber(deviceInfo.getShutValueNumber());
deviceInstallInfoVo.setDeviceName(deviceInfo.getDeviceName()); deviceInstallInfoVo.setDeviceName(deviceInfo.getDeviceName());
deviceInstallInfoVo.setSite(tbDeviceInfo.getCommunityName() + tbDeviceInfo.getBuildingUnit() + tbDeviceInfo.getRoomNo()); deviceInstallInfoVo.setCommunityName(tbDeviceInfo.getCommunityName());
deviceInstallInfoVo.setBuildingUnit(tbDeviceInfo.getBuildingUnit());
deviceInstallInfoVo.setRoomNo(tbDeviceInfo.getRoomNo());
deviceInstallInfoVo.setRemarks(tbDeviceInfo.getRemarks());
deviceInstallInfoVo.setManufacturer("天津费加罗"); deviceInstallInfoVo.setManufacturer("天津费加罗");
deviceInstallInfoVo.setPosition("成功"); deviceInstallInfoVo.setPosition("成功");

@ -25,69 +25,65 @@ package cc.iotkit.ruleengine.handler;
import cc.iotkit.common.constant.Constants; import cc.iotkit.common.constant.Constants;
import cc.iotkit.common.thing.ThingModelMessage; import cc.iotkit.common.thing.ThingModelMessage;
import cc.iotkit.common.utils.JsonUtils;
import cc.iotkit.common.utils.ThreadUtil;
import cc.iotkit.mq.ConsumerHandler; import cc.iotkit.mq.ConsumerHandler;
import cc.iotkit.mq.MqConsumer; import cc.iotkit.mq.MqConsumer;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import javax.annotation.PreDestroy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j @Slf4j
public class RuleDeviceConsumer implements ConsumerHandler<ThingModelMessage>, ApplicationContextAware { public class RuleDeviceConsumer implements ConsumerHandler<ThingModelMessage>, ApplicationContextAware {
private final List<DeviceMessageHandler> handlers = new ArrayList<>(); private final List<DeviceMessageHandler> handlers = new ArrayList<>();
private ScheduledThreadPoolExecutor messageHandlerPool; private ThreadPoolExecutor messageHandlerPool;
/** 连续失败计数器 */
private final AtomicInteger consecutiveFailures = new AtomicInteger(0);
/** 熔断打开截止时间戳0 表示关闭 */
private volatile long circuitOpenUntil = 0;
private static final int FAILURE_THRESHOLD = 10;
private static final long CIRCUIT_COOLDOWN_MS = 30_000;
private static final int QUEUE_CAPACITY = 500;
//private ThreadPoolExecutor messageHandlerPool;
@SneakyThrows @SneakyThrows
public RuleDeviceConsumer(MqConsumer<ThingModelMessage> consumer) { public RuleDeviceConsumer(MqConsumer<ThingModelMessage> consumer) {
consumer.consume(Constants.THING_MODEL_MESSAGE_TOPIC, this); consumer.consume(Constants.THING_MODEL_MESSAGE_TOPIC, this);
} }
/* @Override
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, DeviceMessageHandler> handlerMap = applicationContext.getBeansOfType(DeviceMessageHandler.class); Map<String, DeviceMessageHandler> handlerMap = applicationContext.getBeansOfType(DeviceMessageHandler.class);
int coreSize = handlerMap.size();
// 动态计算核心线程数
int corePoolSize = Math.max(4, Runtime.getRuntime().availableProcessors());
// corePoolSize = corePoolSize > handlerMap.size()?corePoolSize:handlerMap.size();
int maxPoolSize = corePoolSize * 2;
messageHandlerPool = new ThreadPoolExecutor( messageHandlerPool = new ThreadPoolExecutor(
corePoolSize, coreSize,
maxPoolSize, coreSize * 2,
60L, TimeUnit.SECONDS, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10), // 限制队列大小 new LinkedBlockingQueue<>(QUEUE_CAPACITY),
new ThreadFactoryBuilder().setNameFormat("msg-handler-%d").build(), new ThreadPoolExecutor.CallerRunsPolicy()
new ThreadPoolExecutor.CallerRunsPolicy() // 饱和策略
); );
this.handlers.addAll(handlerMap.values());
}*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, DeviceMessageHandler> handlerMap = applicationContext.getBeansOfType(DeviceMessageHandler.class);
messageHandlerPool = ThreadUtil.newScheduled(handlerMap.size() * 2, "messageHandler");
this.handlers.addAll(handlerMap.values()); this.handlers.addAll(handlerMap.values());
} }
@SneakyThrows @SneakyThrows
@Override @Override
public void handler(ThingModelMessage msg) { public void handler(ThingModelMessage msg) {
if (circuitOpenUntil > 0 && circuitOpenUntil > System.currentTimeMillis()) {
return;
}
log.info("received thing model message:{}", msg); log.info("received thing model message:{}", msg);
try { try {
for (DeviceMessageHandler handler : this.handlers) { for (DeviceMessageHandler handler : this.handlers) {
@ -97,8 +93,17 @@ public class RuleDeviceConsumer implements ConsumerHandler<ThingModelMessage>, A
msg.setData(new HashMap<>()); msg.setData(new HashMap<>());
} }
handler.handle(msg); handler.handle(msg);
// 成功 → 重置熔断
consecutiveFailures.set(0);
circuitOpenUntil = 0;
} catch (Throwable e) { } catch (Throwable e) {
log.error("handler message error", e); log.error("handler message error", e);
int failures = consecutiveFailures.incrementAndGet();
if (failures >= FAILURE_THRESHOLD) {
circuitOpenUntil = System.currentTimeMillis() + CIRCUIT_COOLDOWN_MS;
log.error("Circuit breaker OPEN for {}s, {} consecutive failures",
CIRCUIT_COOLDOWN_MS / 1000, failures);
}
} }
}); });
} }
@ -107,4 +112,19 @@ public class RuleDeviceConsumer implements ConsumerHandler<ThingModelMessage>, A
} }
} }
@PreDestroy
public void shutdown() {
if (messageHandlerPool != null) {
messageHandlerPool.shutdown();
try {
if (!messageHandlerPool.awaitTermination(10, TimeUnit.SECONDS)) {
messageHandlerPool.shutdownNow();
}
} catch (InterruptedException e) {
messageHandlerPool.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
} }

Loading…
Cancel
Save