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();
List<TbDIccidRecordDO> findAll();
TbDIccidRecordDO findByImei(String imei);
TbDIccidRecordDO findFirstByImei(String imei);
}

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

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

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

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

@ -225,7 +225,10 @@ public class DeviceInstallInfoServiceImpl implements IDeviceInstallInfoService {
deviceInstallInfoVo.setXuhao(i);
deviceInstallInfoVo.setShutValueNumber(deviceInfo.getShutValueNumber());
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.setPosition("成功");

@ -25,69 +25,65 @@ package cc.iotkit.ruleengine.handler;
import cc.iotkit.common.constant.Constants;
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.MqConsumer;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class RuleDeviceConsumer implements ConsumerHandler<ThingModelMessage>, ApplicationContextAware {
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
public RuleDeviceConsumer(MqConsumer<ThingModelMessage> consumer) {
consumer.consume(Constants.THING_MODEL_MESSAGE_TOPIC, this);
}
/* @Override
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Map<String, DeviceMessageHandler> handlerMap = applicationContext.getBeansOfType(DeviceMessageHandler.class);
// 动态计算核心线程数
int corePoolSize = Math.max(4, Runtime.getRuntime().availableProcessors());
// corePoolSize = corePoolSize > handlerMap.size()?corePoolSize:handlerMap.size();
int maxPoolSize = corePoolSize * 2;
int coreSize = handlerMap.size();
messageHandlerPool = new ThreadPoolExecutor(
corePoolSize,
maxPoolSize,
coreSize,
coreSize * 2,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10), // 限制队列大小
new ThreadFactoryBuilder().setNameFormat("msg-handler-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy() // 饱和策略
new LinkedBlockingQueue<>(QUEUE_CAPACITY),
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());
}
@SneakyThrows
@Override
public void handler(ThingModelMessage msg) {
if (circuitOpenUntil > 0 && circuitOpenUntil > System.currentTimeMillis()) {
return;
}
log.info("received thing model message:{}", msg);
try {
for (DeviceMessageHandler handler : this.handlers) {
@ -97,8 +93,17 @@ public class RuleDeviceConsumer implements ConsumerHandler<ThingModelMessage>, A
msg.setData(new HashMap<>());
}
handler.handle(msg);
// 成功 → 重置熔断
consecutiveFailures.set(0);
circuitOpenUntil = 0;
} catch (Throwable 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