rev:拍照
This commit is contained in:
@@ -42,8 +42,8 @@ public class DictConstantPool {
|
||||
@PostConstruct
|
||||
public void initHikvisonConfig(){
|
||||
System.out.println("初始化海康威视配置:"+ this);
|
||||
HikvisionSnapshotUtil.ins = this.ins;
|
||||
HikvisionSnapshotUtil.path = this.path;
|
||||
// HikvisionSnapshotUtil.ins = this.ins;
|
||||
// HikvisionSnapshotUtil.path = this.path;
|
||||
HikvisionSnapshotUtil.ip = this.ip;
|
||||
HikvisionSnapshotUtil.port = this.port;
|
||||
HikvisionSnapshotUtil.username = this.username;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.nl.common.hikvision;
|
||||
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.sun.jna.Library;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Structure;
|
||||
@@ -11,14 +10,21 @@ import org.springframework.stereotype.Service;
|
||||
import java.io.File;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class HikvisionSnapshotUtil {
|
||||
|
||||
public static String ins; //HCNetSDK.dll文件路径
|
||||
public static String path; //文件存储位置
|
||||
public static String ins = "C:\\Users\\Administrator\\Desktop\\hik\\HCNetSDK.dll"; //HCNetSDK.dll文件路径
|
||||
public static String path = "C:\\software\\wms\\images"; //文件存储位置
|
||||
public static String ip; //海康威视设备网络搜索应用中摄像头ip
|
||||
public static Short port;//海康威视设备网络搜索应用中摄像头port
|
||||
public static String username;//海康威视设备网络搜索应用中摄像头管理账号admin
|
||||
@@ -26,27 +32,19 @@ public class HikvisionSnapshotUtil {
|
||||
public static String channelHttp;//默认
|
||||
public static Integer channel;//默认1
|
||||
|
||||
// SDK 初始化状态标记,确保全局只初始化一次
|
||||
private static final AtomicBoolean sdkInitialized = new AtomicBoolean(false);
|
||||
// 用于同步 SDK 初始化和清理操作的锁
|
||||
private static final ReentrantLock sdkLock = new ReentrantLock();
|
||||
// DLL 加载状态标记
|
||||
private static final AtomicBoolean dllLoaded = new AtomicBoolean(false);
|
||||
// DLL 实例,延迟加载
|
||||
private static volatile HCNetSDK sdkInstance = null;
|
||||
// 用于同步 DLL 加载的锁
|
||||
private static final ReentrantLock dllLoadLock = new ReentrantLock();
|
||||
|
||||
// HCNetSDK 接口映射
|
||||
public interface HCNetSDK extends Library {
|
||||
HCNetSDK INSTANCE = Holder.INSTANCE;
|
||||
|
||||
class Holder {
|
||||
private static final HCNetSDK INSTANCE;
|
||||
|
||||
static {
|
||||
if (StringUtils.isBlank(HikvisionSnapshotUtil.ins)) {
|
||||
throw new RuntimeException("HCNetSDK.dll 路径未设置,请检查 hikvison.ins 配置");
|
||||
}
|
||||
try {
|
||||
System.out.println("加载 HCNetSDK.dll => " + HikvisionSnapshotUtil.ins);
|
||||
INSTANCE = Native.load("C:\\Users\\Administrator\\Desktop\\hik\\HCNetSDK.dll", HCNetSDK.class);
|
||||
System.out.println("加载 HCNetSDK.dll 成功");
|
||||
} catch (Throwable e) {
|
||||
System.err.println("加载 HCNetSDK.dll 失败,路径:" + HikvisionSnapshotUtil.ins + "异常信息:" + e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//初始化 SDK 库
|
||||
boolean NET_DVR_Init();
|
||||
@@ -95,57 +93,272 @@ public class HikvisionSnapshotUtil {
|
||||
public short wPicQuality;
|
||||
}
|
||||
|
||||
public static void sync截图(String imageName, String taskCode) {
|
||||
/**
|
||||
* 超时保护 防止 JNA 调用永久阻塞
|
||||
*/
|
||||
private static boolean executeCaptureWithTimeout(HCNetSDK sdk, int userId, int channel,
|
||||
NET_DVR_JPEGPARA jpegPara, String sFileUrl,
|
||||
long timeoutSeconds) throws Throwable {
|
||||
Future<Boolean> future = Executors.newSingleThreadExecutor().submit(() -> {
|
||||
try {
|
||||
return sdk.NET_DVR_CaptureJPEGPicture(userId, channel, jpegPara, sFileUrl);
|
||||
} catch (Throwable e) {
|
||||
log.info("[SDK] 抓图执行时发生异常", e);
|
||||
throw new RuntimeException("抓图执行异常", e);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return future.get(timeoutSeconds, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
future.cancel(true);
|
||||
log.info("[SDK] 抓图操作超时({}秒),已取消。userId={}, channel={}, file={}",
|
||||
timeoutSeconds, userId, channel, sFileUrl);
|
||||
throw new TimeoutException("抓图操作超时: " + timeoutSeconds + "秒");
|
||||
} catch (ExecutionException e) {
|
||||
log.info(e.getMessage());
|
||||
Throwable cause = e.getCause();
|
||||
if (cause != null) {
|
||||
throw cause;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HCNetSDK 实例
|
||||
*/
|
||||
private static HCNetSDK getSdkInstance() {
|
||||
if (sdkInstance == null) {
|
||||
dllLoadLock.lock();
|
||||
try {
|
||||
// 双重检查
|
||||
if (sdkInstance == null) {
|
||||
if (StringUtils.isBlank(ins)) {
|
||||
throw new RuntimeException("HCNetSDK.dll 路径未设置,请检查 hikvison.ins 配置。可能原因:配置未加载或配置项缺失");
|
||||
}
|
||||
try {
|
||||
log.info("[SDK] 开始加载 HCNetSDK.dll => {}", ins);
|
||||
sdkInstance = Native.load(ins, HCNetSDK.class);
|
||||
dllLoaded.set(true);
|
||||
log.info("[SDK] 加载 HCNetSDK.dll 成功");
|
||||
} catch (Throwable e) {
|
||||
log.info("[SDK] 加载 HCNetSDK.dll 失败,路径:{},异常信息: {}", ins, e.getMessage(), e);
|
||||
throw new RuntimeException("加载 HCNetSDK.dll 失败: " + e.getMessage() + ",路径: " + ins, e);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
dllLoadLock.unlock();
|
||||
}
|
||||
}
|
||||
return sdkInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 截图
|
||||
*
|
||||
* @param imageName 图片名称
|
||||
* @param taskCode 任务编码
|
||||
*/
|
||||
public static void syncSnap(String imageName, String taskCode) {
|
||||
if (StringUtils.isEmpty(imageName)) {
|
||||
log.warn("图片名称未定义");
|
||||
log.info("[SDK] 图片名称未定义,跳过截图");
|
||||
return;
|
||||
}
|
||||
|
||||
// 参数校验
|
||||
if (StringUtils.isBlank(ip) || port == null || StringUtils.isBlank(username)
|
||||
|| StringUtils.isBlank(password) || channel == null) {
|
||||
log.info("[SDK] 海康威视配置参数不完整,无法执行截图。ip={}, port={}, username={}, channel={}",
|
||||
ip, port, username, channel);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取 SDK 实例
|
||||
HCNetSDK sdk;
|
||||
try {
|
||||
sdk = getSdkInstance();
|
||||
} catch (RuntimeException e) {
|
||||
log.info("[SDK] 获取 SDK 实例失败,无法执行截图", e);
|
||||
return;
|
||||
}
|
||||
|
||||
LocalDate now = LocalDate.now();
|
||||
String datePath = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
||||
|
||||
String imagePath = "C:" + File.separator + "software" + File.separator + "wms" + File.separator + "images" + File.separator + datePath;
|
||||
// 使用配置的路径,如果没有配置则使用默认路径
|
||||
// String basePath = StringUtils.isNotBlank(path) ? path :
|
||||
// "C:" + File.separator + "software" + File.separator + "wms" + File.separator + "images";
|
||||
String imagePath = path + File.separator + datePath;
|
||||
|
||||
File dir = new File(imagePath);
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs();
|
||||
boolean created = dir.mkdirs();
|
||||
if (!created) {
|
||||
log.info("[SDK] 创建图片目录失败: {}", imagePath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
String sFileUrl = imagePath + File.separator + imageName + "_" + taskCode + ".jpg";
|
||||
log.info("[SDK] 开始截图,保存路径: {}", sFileUrl);
|
||||
|
||||
if (!HCNetSDK.INSTANCE.NET_DVR_Init()) {
|
||||
log.warn("[SDK] 初始化失败");
|
||||
return;
|
||||
}
|
||||
|
||||
NET_DVR_DEVICEINFO_V30 deviceInfo = new NET_DVR_DEVICEINFO_V30();
|
||||
int userId = HCNetSDK.INSTANCE.NET_DVR_Login_V30(ip, port, username, password, deviceInfo);
|
||||
if (userId < 0) {
|
||||
log.warn("[SDK] 登录失败");
|
||||
HCNetSDK.INSTANCE.NET_DVR_Cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
NET_DVR_JPEGPARA jpegPara = new NET_DVR_JPEGPARA();
|
||||
jpegPara.wPicSize = 2;
|
||||
jpegPara.wPicQuality = 0;
|
||||
|
||||
// 使用锁确保 SDK 操作的线程安全
|
||||
boolean lockAcquired = false;
|
||||
try {
|
||||
boolean result = HCNetSDK.INSTANCE.NET_DVR_CaptureJPEGPicture(userId, channel, jpegPara, sFileUrl);
|
||||
if (result) {
|
||||
log.info("[SDK] 抓图成功: " + sFileUrl);
|
||||
// 尝试获取锁,最多等待 5 秒,避免死锁
|
||||
lockAcquired = sdkLock.tryLock(5, TimeUnit.SECONDS);
|
||||
if (!lockAcquired) {
|
||||
log.info("[SDK] 获取 SDK 操作锁超时,跳过本次截图操作。可能存在死锁或长时间占用锁的情况。");
|
||||
return;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.info("[SDK] 获取锁时线程被中断", e);
|
||||
return;
|
||||
}
|
||||
|
||||
int userId = -1;
|
||||
boolean initSuccess = false;
|
||||
try {
|
||||
log.info("[SDK] 已获取锁,开始执行 SDK 操作");
|
||||
|
||||
// 初始化 SDK只初始化一次
|
||||
if (!sdkInitialized.get()) {
|
||||
try {
|
||||
initSuccess = sdk.NET_DVR_Init();
|
||||
if (!initSuccess) {
|
||||
log.info("[SDK] SDK 初始化失败,可能原因:SDK 已被初始化或初始化参数错误");
|
||||
return;
|
||||
}
|
||||
sdkInitialized.set(true);
|
||||
log.info("[SDK] SDK 初始化成功");
|
||||
} catch (Throwable e) {
|
||||
log.info("[SDK] SDK 初始化时发生异常", e);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// 失败重试一次
|
||||
result = HCNetSDK.INSTANCE.NET_DVR_CaptureJPEGPicture(userId, channel, jpegPara, sFileUrl);
|
||||
initSuccess = true;
|
||||
log.info("[SDK] SDK 已初始化,跳过初始化步骤");
|
||||
}
|
||||
|
||||
// 登录设备
|
||||
NET_DVR_DEVICEINFO_V30 deviceInfo = new NET_DVR_DEVICEINFO_V30();
|
||||
try {
|
||||
userId = sdk.NET_DVR_Login_V30(ip, port, username, password, deviceInfo);
|
||||
if (userId < 0) {
|
||||
log.info("[SDK] 登录设备失败,userId={},请检查设备连接和账号密码。ip={}, port={}, username={}",
|
||||
userId, ip, port, username);
|
||||
return;
|
||||
}
|
||||
log.info("[SDK] 设备登录成功,userId={}", userId);
|
||||
} catch (Throwable e) {
|
||||
log.info("[SDK] 登录设备时发生异常,ip={}, port={}, username={}", ip, port, username, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 配置抓图参数
|
||||
NET_DVR_JPEGPARA jpegPara = new NET_DVR_JPEGPARA();
|
||||
jpegPara.wPicSize = 2;
|
||||
jpegPara.wPicQuality = 0;
|
||||
|
||||
// 执行抓图
|
||||
boolean result = false;
|
||||
try {
|
||||
log.info("[SDK] 开始执行抓图操作,userId={}, channel={}", userId, channel);
|
||||
|
||||
// 记录 JNA 调用前的状态
|
||||
try {
|
||||
JnaCrashMonitor.logBeforeJnaCall("NET_DVR_CaptureJPEGPicture", userId, channel, sFileUrl);
|
||||
} catch (Throwable e) {
|
||||
// 忽略监控日志异常
|
||||
}
|
||||
|
||||
// 执行抓图 设置超时保护,避免永久阻塞
|
||||
result = executeCaptureWithTimeout(sdk, userId, channel, jpegPara, sFileUrl, 10);
|
||||
|
||||
// 记录 JNA 调用后的状态
|
||||
try {
|
||||
JnaCrashMonitor.logAfterJnaCall("NET_DVR_CaptureJPEGPicture", result);
|
||||
} catch (Throwable e) {
|
||||
}
|
||||
|
||||
if (result) {
|
||||
log.info("[SDK] 抓图成功(第二次尝试): " + sFileUrl);
|
||||
log.info("[SDK] 抓图成功: {}", sFileUrl);
|
||||
} else {
|
||||
log.warn("[SDK] 抓图失败: " + sFileUrl);
|
||||
// 失败重试一次
|
||||
log.info("[SDK] 第一次抓图失败,进行重试...");
|
||||
try {
|
||||
Thread.sleep(500); // 等待500ms后重试
|
||||
result = executeCaptureWithTimeout(sdk, userId, channel, jpegPara, sFileUrl, 10);
|
||||
if (result) {
|
||||
log.info("[SDK] 抓图成功(第二次尝试): {}", sFileUrl);
|
||||
} else {
|
||||
log.info("[SDK] 抓图失败(重试后仍失败): {},请检查设备连接和通道配置。userId={}, channel={}",
|
||||
sFileUrl, userId, channel);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.info("[SDK] 重试抓图时线程被中断", e);
|
||||
} catch (Throwable e) {
|
||||
log.info("[SDK] 重试抓图时发生异常: {}", sFileUrl, e);
|
||||
try {
|
||||
JnaCrashMonitor.logJnaCallError("NET_DVR_CaptureJPEGPicture(重试)", e, userId, channel, sFileUrl);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
log.info("[SDK] 抓图过程中发生异常: {}, userId={}, channel={}", sFileUrl, userId, channel, e);
|
||||
if (e instanceof Error) {
|
||||
log.info("[SDK] 发生严重错误(Error),可能导致 JVM 不稳定", e);
|
||||
}
|
||||
try {
|
||||
JnaCrashMonitor.logJnaCallError("NET_DVR_CaptureJPEGPicture", e, userId, channel, sFileUrl);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Throwable e) {
|
||||
log.info("[SDK] 截图过程中发生未预期的异常", e);
|
||||
if (e instanceof Error) {
|
||||
log.info("[SDK] 发生严重错误(Error),可能导致 JVM 崩溃", e);
|
||||
}
|
||||
} finally {
|
||||
HCNetSDK.INSTANCE.NET_DVR_Logout(userId);
|
||||
HCNetSDK.INSTANCE.NET_DVR_Cleanup();
|
||||
log.info("[SDK] 进入finally");
|
||||
try {
|
||||
if (userId >= 0 && sdk != null) {
|
||||
try {
|
||||
boolean logoutResult = sdk.NET_DVR_Logout(userId);
|
||||
if (!logoutResult) {
|
||||
log.info("[SDK] 注销登录失败,userId={}", userId);
|
||||
} else {
|
||||
log.info("[SDK] 注销登录成功,userId={}", userId);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
log.info("[SDK] 注销登录时发生异常,userId={}", userId, e);
|
||||
}
|
||||
} else {
|
||||
log.info("[SDK] 无需注销登录,userId={}", userId);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
log.info("[SDK] 清理资源时发生异常", e);
|
||||
}
|
||||
|
||||
// 确保锁被释放
|
||||
if (lockAcquired) {
|
||||
log.info("[SDK] 开始释放锁");
|
||||
try {
|
||||
sdkLock.unlock();
|
||||
log.info("[SDK] 截图操作完成,已释放锁");
|
||||
} catch (IllegalMonitorStateException e) {
|
||||
log.info("[SDK] 释放锁时发生异常,可能锁状态异常", e);
|
||||
} catch (Throwable e) {
|
||||
log.info("[SDK] 释放锁时发生未知异常", e);
|
||||
}
|
||||
log.info("[SDK] 释放锁完成");
|
||||
}
|
||||
log.info("[SDK] 结束finally");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package org.nl.common.hikvision;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryMXBean;
|
||||
import java.lang.management.ThreadMXBean;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* JNA/DLL 崩溃监控和诊断工具
|
||||
* 用于监控 JNA 调用本地 DLL 时的异常情况
|
||||
*
|
||||
* @author System
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class JnaCrashMonitor {
|
||||
|
||||
private static final String CRASH_LOG_DIR = "C:\\software\\wms\\logs\\crash";
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
// 创建崩溃日志目录
|
||||
File logDir = new File(CRASH_LOG_DIR);
|
||||
if (!logDir.exists()) {
|
||||
logDir.mkdirs();
|
||||
}
|
||||
|
||||
// 设置未捕获异常处理器
|
||||
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
|
||||
logCrashInfo("未捕获异常", thread, throwable);
|
||||
});
|
||||
|
||||
// 添加 JVM 关闭钩子,用于记录崩溃信息
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
log.info("[JNA监控] JVM 正在关闭,记录系统状态...");
|
||||
logSystemState("JVM关闭");
|
||||
}));
|
||||
|
||||
log.info("[JNA监控] JNA 崩溃监控已初始化,日志目录: {}", CRASH_LOG_DIR);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录崩溃信息
|
||||
*/
|
||||
public static void logCrashInfo(String event, Thread thread, Throwable throwable) {
|
||||
String timestamp = LocalDateTime.now().format(DATE_FORMATTER);
|
||||
String logFile = CRASH_LOG_DIR + File.separator + "crash_" +
|
||||
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + ".log";
|
||||
|
||||
try (FileWriter fw = new FileWriter(logFile, true);
|
||||
PrintWriter pw = new PrintWriter(fw)) {
|
||||
|
||||
pw.println(repeatString("=", 80));
|
||||
pw.println("时间: " + timestamp);
|
||||
pw.println("事件: " + event);
|
||||
pw.println("线程: " + (thread != null ? thread.getName() : "未知"));
|
||||
pw.println("异常类型: " + (throwable != null ? throwable.getClass().getName() : "未知"));
|
||||
|
||||
if (throwable != null) {
|
||||
pw.println("异常消息: " + throwable.getMessage());
|
||||
pw.println("堆栈跟踪:");
|
||||
throwable.printStackTrace(pw);
|
||||
}
|
||||
|
||||
// 记录系统状态
|
||||
logSystemStateToFile(pw);
|
||||
|
||||
pw.println(repeatString("=", 80));
|
||||
pw.println();
|
||||
|
||||
} catch (IOException e) {
|
||||
log.info("[JNA监控] 写入崩溃日志失败", e);
|
||||
}
|
||||
|
||||
// 同时记录到标准日志
|
||||
log.info("[JNA监控] {} - 线程: {}, 异常: {}",
|
||||
event,
|
||||
thread != null ? thread.getName() : "未知",
|
||||
throwable != null ? throwable.getMessage() : "未知",
|
||||
throwable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录系统状态
|
||||
*/
|
||||
public static void logSystemState(String event) {
|
||||
String logFile = CRASH_LOG_DIR + File.separator + "system_state_" +
|
||||
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")) + ".log";
|
||||
|
||||
try (FileWriter fw = new FileWriter(logFile, true);
|
||||
PrintWriter pw = new PrintWriter(fw)) {
|
||||
|
||||
pw.println(repeatString("=", 80));
|
||||
pw.println("时间: " + LocalDateTime.now().format(DATE_FORMATTER));
|
||||
pw.println("事件: " + event);
|
||||
logSystemStateToFile(pw);
|
||||
pw.println(repeatString("=", 80));
|
||||
pw.println();
|
||||
|
||||
} catch (IOException e) {
|
||||
log.info("[JNA监控] 写入系统状态日志失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将系统状态写入文件
|
||||
*/
|
||||
private static void logSystemStateToFile(PrintWriter pw) {
|
||||
// 内存信息
|
||||
MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
|
||||
pw.println("堆内存使用: " + (memoryBean.getHeapMemoryUsage().getUsed() / 1024 / 1024) + " MB / " +
|
||||
(memoryBean.getHeapMemoryUsage().getMax() / 1024 / 1024) + " MB");
|
||||
pw.println("非堆内存使用: " + (memoryBean.getNonHeapMemoryUsage().getUsed() / 1024 / 1024) + " MB / " +
|
||||
(memoryBean.getNonHeapMemoryUsage().getMax() / 1024 / 1024) + " MB");
|
||||
|
||||
// 线程信息
|
||||
ThreadMXBean threadBean = ManagementFactory.getThreadMXBean();
|
||||
pw.println("活动线程数: " + threadBean.getThreadCount());
|
||||
pw.println("峰值线程数: " + threadBean.getPeakThreadCount());
|
||||
|
||||
// JVM 信息
|
||||
pw.println("JVM 名称: " + ManagementFactory.getRuntimeMXBean().getVmName());
|
||||
pw.println("JVM 版本: " + ManagementFactory.getRuntimeMXBean().getVmVersion());
|
||||
pw.println("运行时间: " + (ManagementFactory.getRuntimeMXBean().getUptime() / 1000) + " 秒");
|
||||
}
|
||||
|
||||
/**
|
||||
* 重复字符串(兼容 Java 8)
|
||||
*/
|
||||
private static String repeatString(String str, int count) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < count; i++) {
|
||||
sb.append(str);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 JNA 调用前的状态
|
||||
*/
|
||||
public static void logBeforeJnaCall(String methodName, Object... params) {
|
||||
log.info("[JNA监控] 准备调用 JNA 方法: {}, 参数: {}", methodName, params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 JNA 调用后的状态
|
||||
*/
|
||||
public static void logAfterJnaCall(String methodName, Object result) {
|
||||
log.info("[JNA监控] JNA 方法调用完成: {}, 结果: {}", methodName, result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 JNA 调用异常
|
||||
*/
|
||||
public static void logJnaCallError(String methodName, Throwable error, Object... params) {
|
||||
log.info("[JNA监控] JNA 方法调用异常: {}, 参数: {}", methodName, params, error);
|
||||
logCrashInfo("JNA调用异常", Thread.currentThread(), error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,12 +194,22 @@ public class AcsToWmsServiceImpl implements AcsToWmsService {
|
||||
Param ParamDao = sysParamService.findByCode("is_pat");
|
||||
if (ParamDao.getValue().equals(IOSConstant.IS_DELETE_YES)) {
|
||||
CompletableFuture.runAsync(() -> {
|
||||
String vehicleCode = vehicleDao.getStoragevehicle_code();
|
||||
String taskCode = taskDao.getTask_code();
|
||||
log.info("[拍照] 开始异步调用海康拍照,vehicleCode={}, taskCode={}", vehicleCode, taskCode);
|
||||
try {
|
||||
HikvisionSnapshotUtil.sync截图(vehicleDao.getStoragevehicle_code(), taskDao.getTask_code());
|
||||
} catch (Exception e) {
|
||||
log.info("调用海康拍照失败:" + e);
|
||||
HikvisionSnapshotUtil.syncSnap(vehicleCode, taskCode);
|
||||
log.info("[拍照] 海康拍照调用完成,vehicleCode={}, taskCode={}", vehicleCode, taskCode);
|
||||
} catch (Throwable e) {
|
||||
log.info("[拍照] 调用海康拍照失败,vehicleCode={}, taskCode={}", vehicleCode, taskCode, e);
|
||||
if (e instanceof Error) {
|
||||
log.info("[拍照] 发生严重错误(Error),可能导致线程或 JVM 不稳定", e);
|
||||
}
|
||||
}
|
||||
}, pool);
|
||||
}, pool).exceptionally(throwable -> {
|
||||
log.info("[拍照] CompletableFuture 执行异常", throwable);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
log.info("ACS向WMS反馈重量,返回参数:--------------------------------------" + BaseResponse.responseOk(resultWeigh).toString());
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
"jquery": "^3.6.0",
|
||||
"js-beautify": "^1.10.2",
|
||||
"js-cookie": "2.2.0",
|
||||
"jsbarcode": "^3.11.5",
|
||||
"jsbarcode": "^3.12.1",
|
||||
"jsencrypt": "^3.0.0-rc.1",
|
||||
"json-bigint": "^1.0.0",
|
||||
"jszip": "3.1.5",
|
||||
|
||||
@@ -53,6 +53,16 @@
|
||||
>
|
||||
打印
|
||||
</el-button>
|
||||
<el-button
|
||||
slot="right"
|
||||
class="filter-item"
|
||||
type="success"
|
||||
icon="el-icon-printer"
|
||||
size="mini"
|
||||
@click="printPreview"
|
||||
>
|
||||
打印预览
|
||||
</el-button>
|
||||
</crudOperation>
|
||||
<!--表单组件-->
|
||||
<el-dialog
|
||||
@@ -79,13 +89,13 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="载具编码" prop="storagevehicle_code">
|
||||
<el-input v-model="form.storagevehicle_code" style="width: 200px;" :disabled="crud.status.edit > 0" />
|
||||
<el-input v-model="form.storagevehicle_code" style="width: 200px;" :disabled="crud.status.edit > 0"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="载具名称" prop="storagevehicle_name">
|
||||
<el-input v-model="form.storagevehicle_name" style="width: 200px;" />
|
||||
<el-input v-model="form.storagevehicle_name" style="width: 200px;"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="载具重量" prop="weigth">
|
||||
<el-input-number v-model="form.weigth" :precision="1" style="width: 200px;" />
|
||||
<el-input-number v-model="form.weigth" :precision="1" style="width: 200px;"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否启用">
|
||||
<el-radio v-model="form.is_used" label="0">否</el-radio>
|
||||
@@ -106,13 +116,13 @@
|
||||
style="width: 100%;"
|
||||
@selection-change="crud.selectionChangeHandler"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column v-if="false" prop="storagevehicle_id" label="载具标识" />
|
||||
<el-table-column prop="storagevehicle_type_name" label="载具类型" :formatter="formattType" />
|
||||
<el-table-column prop="storagevehicle_code" label="载具编码" />
|
||||
<el-table-column prop="storagevehicle_name" label="载具名称" />
|
||||
<el-table-column prop="pcsn" label="绑定物料" />
|
||||
<el-table-column prop="weigth" label="托盘重量" />
|
||||
<el-table-column type="selection" width="55"/>
|
||||
<el-table-column v-if="false" prop="storagevehicle_id" label="载具标识"/>
|
||||
<el-table-column prop="storagevehicle_type_name" label="载具类型" :formatter="formattType"/>
|
||||
<el-table-column prop="storagevehicle_code" label="载具编码"/>
|
||||
<el-table-column prop="storagevehicle_name" label="载具名称"/>
|
||||
<el-table-column prop="pcsn" label="绑定物料"/>
|
||||
<el-table-column prop="weigth" label="托盘重量"/>
|
||||
<el-table-column label="是否启用" align="center" prop="is_used">
|
||||
<template slot-scope="scope">
|
||||
<el-switch
|
||||
@@ -143,19 +153,21 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!--分页组件-->
|
||||
<pagination />
|
||||
<pagination/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import crudStoragevehicleinfo from '@/views/wms/basedata/storagevehicleinfo/storagevehicleinfo'
|
||||
import CRUD, { crud, form, header, presenter } from '@crud/crud'
|
||||
import CRUD, {crud, form, header, presenter} from '@crud/crud'
|
||||
import rrOperation from '@crud/RR.operation'
|
||||
import crudOperation from '@crud/CRUD.operation'
|
||||
import udOperation from '@crud/UD.operation'
|
||||
import pagination from '@crud/Pagination'
|
||||
import { getLodop } from '@/assets/js/lodop/LodopFuncs'
|
||||
import {getLodop} from '@/assets/js/lodop/LodopFuncs'
|
||||
import JsBarcode from 'jsbarcode'
|
||||
|
||||
|
||||
const defaultForm = {
|
||||
storagevehicle_id: null,
|
||||
@@ -186,7 +198,7 @@ const defaultForm = {
|
||||
export default {
|
||||
name: 'Storagevehicleinfo',
|
||||
dicts: ['storagevehicle_type', 'is_used'],
|
||||
components: { pagination, crudOperation, rrOperation, udOperation },
|
||||
components: {pagination, crudOperation, rrOperation, udOperation},
|
||||
mixins: [presenter(), header(), form(defaultForm), crud()],
|
||||
cruds() {
|
||||
return CRUD({
|
||||
@@ -194,7 +206,7 @@ export default {
|
||||
url: 'api/storagevehicleinfo',
|
||||
idField: 'storagevehicle_id',
|
||||
sort: 'storagevehicle_id,desc',
|
||||
crudMethod: { ...crudStoragevehicleinfo },
|
||||
crudMethod: {...crudStoragevehicleinfo},
|
||||
optShow: {
|
||||
add: true,
|
||||
edit: false,
|
||||
@@ -220,26 +232,26 @@ export default {
|
||||
classes1: [],
|
||||
rules: {
|
||||
is_delete: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' }
|
||||
{required: true, message: '不能为空', trigger: 'blur'}
|
||||
],
|
||||
is_used: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' }
|
||||
{required: true, message: '不能为空', trigger: 'blur'}
|
||||
],
|
||||
storagevehicle_type: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' }
|
||||
{required: true, message: '不能为空', trigger: 'blur'}
|
||||
],
|
||||
storagevehicle_code: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' }
|
||||
{required: true, message: '不能为空', trigger: 'blur'}
|
||||
],
|
||||
storagevehicle_name: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' }
|
||||
{required: true, message: '不能为空', trigger: 'blur'}
|
||||
],
|
||||
overstruct_type: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' }
|
||||
{required: true, message: '不能为空', trigger: 'blur'}
|
||||
],
|
||||
num: [
|
||||
{ required: true, message: '不能为空', trigger: 'blur' },
|
||||
{ validator: numberOne }
|
||||
{required: true, message: '不能为空', trigger: 'blur'},
|
||||
{validator: numberOne}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -283,48 +295,92 @@ export default {
|
||||
formattType(row) {
|
||||
return this.dict.label.storagevehicle_type[row.storagevehicle_type]
|
||||
},
|
||||
printPreview() {
|
||||
const _selectData = this.$refs.table.selection
|
||||
if (!_selectData || _selectData.length < 1) {
|
||||
this.crud.notify('请选择一条记录', CRUD.NOTIFICATION_TYPE.INFO)
|
||||
return
|
||||
}
|
||||
|
||||
let container = document.getElementById('barcodePreview')
|
||||
if (!container) {
|
||||
container = document.createElement('div')
|
||||
container.id = 'barcodePreview'
|
||||
document.body.appendChild(container)
|
||||
}
|
||||
|
||||
container.innerHTML = ''
|
||||
container.style.display = 'flex'
|
||||
container.style.flexDirection = 'column'
|
||||
container.style.alignItems = 'center'
|
||||
container.style.background = '#fff'
|
||||
container.style.padding = '20px'
|
||||
|
||||
const mm2px = mm => mm * 3.78
|
||||
const paperWidth = mm2px(80)
|
||||
const paperHeight = mm2px(60)
|
||||
const barcodeHeight = mm2px(40)
|
||||
const textGap = mm2px(1)
|
||||
const barcodeWidth = mm2px(75) // 条码更宽
|
||||
|
||||
_selectData.forEach(item => {
|
||||
const code = item.storagevehicle_code || ''
|
||||
|
||||
const pageDiv = document.createElement('div')
|
||||
pageDiv.style.width = `${paperWidth}px`
|
||||
pageDiv.style.height = `${paperHeight}px`
|
||||
pageDiv.style.display = 'flex'
|
||||
pageDiv.style.flexDirection = 'column'
|
||||
pageDiv.style.alignItems = 'center'
|
||||
pageDiv.style.justifyContent = 'center'
|
||||
pageDiv.style.marginBottom = '20px'
|
||||
pageDiv.style.border = '1px solid #ccc'
|
||||
container.appendChild(pageDiv)
|
||||
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = barcodeWidth
|
||||
canvas.height = barcodeHeight
|
||||
pageDiv.appendChild(canvas)
|
||||
|
||||
JsBarcode(canvas, code, {
|
||||
format: "CODE128",
|
||||
width: 2, // 条码线宽适中
|
||||
height: barcodeHeight,
|
||||
displayValue: false
|
||||
})
|
||||
|
||||
const textDiv = document.createElement('div')
|
||||
textDiv.innerText = code
|
||||
textDiv.style.fontSize = '14px'
|
||||
textDiv.style.fontWeight = 'bold'
|
||||
textDiv.style.textAlign = 'center'
|
||||
textDiv.style.marginTop = `${textGap}px`
|
||||
pageDiv.appendChild(textDiv)
|
||||
})
|
||||
|
||||
this.crud.notify(`已生成 ${_selectData.length} 条预览`, CRUD.NOTIFICATION_TYPE.INFO)
|
||||
},
|
||||
|
||||
print() {
|
||||
const _selectData = this.$refs.table.selection
|
||||
if (!_selectData || _selectData.length < 1) {
|
||||
this.crud.notify('请选择一条记录', CRUD.NOTIFICATION_TYPE.INFO)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 LODOP 对象
|
||||
|
||||
const LODOP = getLodop()
|
||||
if (!LODOP) {
|
||||
// getLodop 内部已经处理了错误提示
|
||||
return
|
||||
}
|
||||
|
||||
if (!LODOP) return
|
||||
|
||||
try {
|
||||
LODOP.PRINT_INIT('载具标签打印')
|
||||
LODOP.SET_SHOW_MODE('HIDE_DISBUTTIN_SETUP', 1)
|
||||
|
||||
// 批量打印选中的记录
|
||||
for (let i = 0; i < _selectData.length; i++) {
|
||||
const code = _selectData[i].storagevehicle_code
|
||||
const name = _selectData[i].storagevehicle_name || code
|
||||
|
||||
// 打印纸张大小:60 * 80
|
||||
// 参数说明:SET_PRINT_PAGESIZE(方向, 宽度, 高度, 单位)
|
||||
// 方向:0-纵向,1-横向
|
||||
LODOP.SET_PRINT_PAGESIZE(0, '60mm', '80mm', '')
|
||||
|
||||
// 添加条码
|
||||
// 条码位置:水平居中 (60-50)/2 = 5mm,垂直居中 (80-30)/2 = 25mm
|
||||
// 条码大小:50mm宽 * 30mm高(可根据实际需要调整)
|
||||
LODOP.ADD_PRINT_BARCODE('25mm', '5mm', '30mm', '50mm', '128Auto', code)
|
||||
|
||||
// 如果需要打印多份,使用 NEWPAGE 分隔
|
||||
if (i < _selectData.length - 1) {
|
||||
LODOP.NEWPAGE()
|
||||
}
|
||||
const code = _selectData[i].storagevehicle_code || ''
|
||||
LODOP.SET_PRINT_PAGESIZE(1, `80mm`, `60mm`, '1')
|
||||
LODOP.ADD_PRINT_BARCODE(`7.5mm`, `10mm`, `62.5mm`, `47.5mm`, '128Auto', code)
|
||||
if (i < _selectData.length - 1) LODOP.NEWPAGE()
|
||||
}
|
||||
|
||||
// 执行打印(会在客户端电脑上执行)
|
||||
LODOP.PRINT()
|
||||
|
||||
this.crud.notify(`成功打印 ${_selectData.length} 条记录`, CRUD.NOTIFICATION_TYPE.SUCCESS)
|
||||
this.crud.toQuery()
|
||||
} catch (error) {
|
||||
@@ -332,6 +388,8 @@ export default {
|
||||
this.crud.notify('打印失败:' + (error.message || '未知错误'), CRUD.NOTIFICATION_TYPE.ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user