Merge branch 'main' of https://git.ambigrat.com/hom/yuanhe-checkin-electron
This commit is contained in:
233
electron/idcard-worker.js
Normal file
233
electron/idcard-worker.js
Normal file
@@ -0,0 +1,233 @@
|
||||
const { parentPort } = require("worker_threads");
|
||||
const koffi = require("koffi");
|
||||
const path = require("path");
|
||||
const iconv = require("iconv-lite");
|
||||
|
||||
// 定义结构体
|
||||
const IDCardData = koffi.struct("IDCardData", {
|
||||
Name: koffi.array("char", 32),
|
||||
Sex: koffi.array("char", 6),
|
||||
Nation: koffi.array("char", 20),
|
||||
Born: koffi.array("char", 18),
|
||||
Address: koffi.array("char", 72),
|
||||
IDCardNo: koffi.array("char", 38),
|
||||
GrantDept: koffi.array("char", 32),
|
||||
UserLifeBegin: koffi.array("char", 18),
|
||||
UserLifeEnd: koffi.array("char", 18),
|
||||
reserved: koffi.array("char", 38),
|
||||
PhotoFileName: koffi.array("char", 255),
|
||||
});
|
||||
|
||||
let lib = null;
|
||||
let running = false;
|
||||
|
||||
// 解码 GBK 字符串
|
||||
function decodeGBK(buffer) {
|
||||
// 找到第一个 null 字节 (0x00) 的位置
|
||||
let nullIndex = -1;
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
if (buffer[i] === 0) {
|
||||
nullIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 如果找到了 null,截取前面的部分;否则使用整个 buffer
|
||||
const validBuffer = nullIndex !== -1 ? buffer.slice(0, nullIndex) : buffer;
|
||||
return iconv.decode(validBuffer, "gbk").trim();
|
||||
}
|
||||
|
||||
// 加载 DLL
|
||||
function loadDll() {
|
||||
if (lib) return true;
|
||||
try {
|
||||
const fs = require("fs");
|
||||
const dllNames = ["Syn_IDCardRead.dll", "SynIDCardRead.dll"];
|
||||
|
||||
// 确定 DLL 目录:优先检查生产环境 resources/xzx,否则使用开发环境 resources/xzx
|
||||
let dllDir = path.join(process.cwd(), "resources", "xzx");
|
||||
if (process.resourcesPath) {
|
||||
const prodDir = path.join(process.resourcesPath, "xzx");
|
||||
if (fs.existsSync(prodDir)) {
|
||||
dllDir = prodDir;
|
||||
}
|
||||
}
|
||||
|
||||
let dllPath = null;
|
||||
for (const name of dllNames) {
|
||||
const p = path.join(dllDir, name);
|
||||
if (fs.existsSync(p)) {
|
||||
dllPath = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dllPath) {
|
||||
throw new Error(`DLL not found in ${dllDir}`);
|
||||
}
|
||||
|
||||
parentPort.postMessage({
|
||||
type: "log",
|
||||
payload: `Loading DLL from: ${dllPath}`,
|
||||
});
|
||||
lib = koffi.load(dllPath);
|
||||
|
||||
// 尝试使用 stdcall 约定,这对于 Windows 32位 DLL 很常见
|
||||
// koffi 2.x 版本中,stdcall 约定需要在函数名前加 __stdcall
|
||||
// 如果字符串解析失败,可以尝试使用 lib.stdcall() 方法
|
||||
|
||||
// 辅助函数:尝试加载函数,支持原名和修饰名
|
||||
const loadFunc = (name, alias, ret, args) => {
|
||||
try {
|
||||
return lib.stdcall(name, ret, args);
|
||||
} catch (e) {
|
||||
try {
|
||||
return lib.stdcall(alias, ret, args);
|
||||
} catch (e2) {
|
||||
throw new Error(
|
||||
`Cannot find function '${name}' or '${alias}' in DLL`
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
return {
|
||||
Syn_OpenPort: loadFunc("Syn_OpenPort", "_Syn_OpenPort@4", "int", [
|
||||
"int",
|
||||
]),
|
||||
Syn_ClosePort: loadFunc("Syn_ClosePort", "_Syn_ClosePort@4", "int", [
|
||||
"int",
|
||||
]),
|
||||
Syn_StartFindIDCard: loadFunc(
|
||||
"Syn_StartFindIDCard",
|
||||
"_Syn_StartFindIDCard@12",
|
||||
"int",
|
||||
["int", "uint8*", "int"]
|
||||
),
|
||||
Syn_SelectIDCard: loadFunc(
|
||||
"Syn_SelectIDCard",
|
||||
"_Syn_SelectIDCard@12",
|
||||
"int",
|
||||
["int", "uint8*", "int"]
|
||||
),
|
||||
Syn_ReadMsg: loadFunc("Syn_ReadMsg", "_Syn_ReadMsg@12", "int", [
|
||||
"int",
|
||||
"int",
|
||||
koffi.out(koffi.pointer(IDCardData)),
|
||||
]),
|
||||
};
|
||||
} catch (e) {
|
||||
// 如果 lib.stdcall 也不行,尝试回退到 func 但不带 __stdcall (可能不是 stdcall 或者 koffi 版本差异)
|
||||
// 但根据之前的错误,不带 __stdcall 找不到函数,带了又报类型错误,说明很可能是 stdcall 但字符串解析有问题
|
||||
// 这里我们坚持用 lib.stdcall 这种显式 API 调用,它比字符串解析更稳定
|
||||
throw e;
|
||||
}
|
||||
} catch (err) {
|
||||
parentPort.postMessage({
|
||||
type: "error",
|
||||
payload: `Failed to load DLL: ${err.message}`,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function startListen() {
|
||||
if (running) return;
|
||||
running = true;
|
||||
|
||||
const api = loadDll();
|
||||
if (!api) {
|
||||
running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 尝试打开端口,优先尝试 1001 (USB)
|
||||
let port = 1001;
|
||||
let openRes = api.Syn_OpenPort(port);
|
||||
|
||||
if (openRes !== 0) {
|
||||
// 如果 1001 失败,尝试 1001-1016
|
||||
for (let p = 1002; p <= 1016; p++) {
|
||||
if (api.Syn_OpenPort(p) === 0) {
|
||||
port = p;
|
||||
openRes = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (openRes !== 0) {
|
||||
parentPort.postMessage({
|
||||
type: "error",
|
||||
payload: `Open port failed (tried 1001-1016)`,
|
||||
});
|
||||
running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
parentPort.postMessage({
|
||||
type: "log",
|
||||
payload: `Port ${port} opened successfully`,
|
||||
});
|
||||
|
||||
const iin = Buffer.alloc(4);
|
||||
const sn = Buffer.alloc(8);
|
||||
// IDCardData 结构体大小计算: 32+6+20+18+72+38+32+18+18+38+255 = 547 字节
|
||||
// koffi 会自动处理结构体内存分配
|
||||
|
||||
while (running) {
|
||||
// 寻卡
|
||||
api.Syn_StartFindIDCard(port, iin, 0);
|
||||
// 选卡
|
||||
api.Syn_SelectIDCard(port, sn, 0);
|
||||
|
||||
// 读卡
|
||||
const data = {}; // koffi 输出对象
|
||||
const ret = api.Syn_ReadMsg(port, 0, data);
|
||||
|
||||
if (ret === 0 && data) {
|
||||
const payload = {
|
||||
name: decodeGBK(Buffer.from(data.Name)),
|
||||
sex: decodeGBK(Buffer.from(data.Sex)),
|
||||
nation: decodeGBK(Buffer.from(data.Nation)),
|
||||
born: decodeGBK(Buffer.from(data.Born)),
|
||||
address: decodeGBK(Buffer.from(data.Address)),
|
||||
id_card_no: decodeGBK(Buffer.from(data.IDCardNo)),
|
||||
grant_dept: decodeGBK(Buffer.from(data.GrantDept)),
|
||||
life_begin: decodeGBK(Buffer.from(data.UserLifeBegin)),
|
||||
life_end: decodeGBK(Buffer.from(data.UserLifeEnd)),
|
||||
photo_path: decodeGBK(Buffer.from(data.PhotoFileName)),
|
||||
};
|
||||
|
||||
parentPort.postMessage({
|
||||
type: "log",
|
||||
payload: `Read IDCard success: ${payload.name} ${payload.id_card_no}`,
|
||||
});
|
||||
parentPort.postMessage({ type: "data", payload });
|
||||
|
||||
// 读到卡后暂停一下,避免重复读取太快
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
} else {
|
||||
// 没读到卡,稍微等待
|
||||
await new Promise((r) => setTimeout(r, 150));
|
||||
}
|
||||
}
|
||||
|
||||
api.Syn_ClosePort(port);
|
||||
} catch (err) {
|
||||
parentPort.postMessage({
|
||||
type: "error",
|
||||
payload: `Worker error: ${err.message}`,
|
||||
});
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
parentPort.on("message", (msg) => {
|
||||
if (msg === "start") {
|
||||
startListen();
|
||||
} else if (msg === "stop") {
|
||||
running = false;
|
||||
}
|
||||
});
|
||||
@@ -3,7 +3,57 @@ const path = require("path");
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
|
||||
let mainWindow;
|
||||
let mainWindow = null;
|
||||
const { Worker } = require("worker_threads");
|
||||
const log = require("electron-log");
|
||||
|
||||
// 配置日志输出
|
||||
log.transports.file.level = "info";
|
||||
log.transports.file.resolvePath = () =>
|
||||
path.join(path.dirname(app.getPath("exe")), "logs", "main.log");
|
||||
log.info("App starting...");
|
||||
|
||||
// 监听渲染进程日志
|
||||
ipcMain.on("log-message", (event, { level, message }) => {
|
||||
if (log[level]) {
|
||||
log[level](`[Renderer] ${message}`);
|
||||
} else {
|
||||
log.info(`[Renderer] ${message}`);
|
||||
}
|
||||
});
|
||||
|
||||
let idCardWorker = null;
|
||||
|
||||
function createIdCardWorker() {
|
||||
if (idCardWorker) return;
|
||||
|
||||
log.info("Creating IDCard Worker...");
|
||||
idCardWorker = new Worker(path.join(__dirname, "idcard-worker.js"));
|
||||
|
||||
idCardWorker.on("message", (msg) => {
|
||||
if (!mainWindow) return;
|
||||
|
||||
if (msg.type === "data") {
|
||||
log.info("IDCard data received");
|
||||
mainWindow.webContents.send("idcard-data", { payload: msg.payload });
|
||||
} else if (msg.type === "error") {
|
||||
log.error("IDCard worker error message:", msg.payload);
|
||||
mainWindow.webContents.send("idcard-error", { payload: msg.payload });
|
||||
} else if (msg.type === "log") {
|
||||
log.info("[Worker]", msg.payload);
|
||||
}
|
||||
});
|
||||
|
||||
idCardWorker.on("error", (err) => {
|
||||
log.error("Worker thread error:", err);
|
||||
});
|
||||
|
||||
idCardWorker.on("exit", (code) => {
|
||||
if (code !== 0) log.error(`Worker stopped with exit code ${code}`);
|
||||
else log.info("Worker stopped gracefully");
|
||||
idCardWorker = null;
|
||||
});
|
||||
}
|
||||
|
||||
function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
@@ -16,11 +66,16 @@ function createWindow() {
|
||||
},
|
||||
});
|
||||
|
||||
// 开发环境下加载 Vite 开发服务器地址
|
||||
// 生产环境下加载打包后的 index.html
|
||||
mainWindow = win;
|
||||
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD><C2BC><EFBFBD> Vite <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ
|
||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>¼<EFBFBD><C2BC>ش<EFBFBD><D8B4><EFBFBD><EFBFBD><EFBFBD> index.html
|
||||
const isDev = !app.isPackaged;
|
||||
|
||||
if (isDev) {
|
||||
win.loadURL("http://localhost:5173");
|
||||
// <20><EFBFBD><F2BFAABF><EFBFBD><EFBFBD>߹<EFBFBD><DFB9><EFBFBD>
|
||||
win.webContents.openDevTools();
|
||||
mainWindow.loadURL("http://localhost:5173");
|
||||
// 打开开发者工具
|
||||
mainWindow.webContents.openDevTools();
|
||||
@@ -126,6 +181,25 @@ ipcMain.handle("print-pdf", async (event, pdfUrl) => {
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
createIdCardWorker();
|
||||
|
||||
// IPC <20><><EFBFBD><EFBFBD>
|
||||
ipcMain.handle("start_idcard_listen", () => {
|
||||
if (idCardWorker) {
|
||||
idCardWorker.postMessage("start");
|
||||
} else {
|
||||
createIdCardWorker();
|
||||
idCardWorker.postMessage("start");
|
||||
}
|
||||
return "started";
|
||||
});
|
||||
|
||||
ipcMain.handle("stop_idcard_listen", () => {
|
||||
if (idCardWorker) {
|
||||
idCardWorker.postMessage("stop");
|
||||
}
|
||||
return "stopped";
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
@@ -135,6 +209,9 @@ app.whenReady().then(() => {
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
if (idCardWorker) {
|
||||
idCardWorker.terminate();
|
||||
}
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
|
||||
@@ -5,4 +5,15 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
fetchPdf: (pdfUrl) => ipcRenderer.invoke("fetch-pdf", pdfUrl),
|
||||
// 打印PDF
|
||||
printPdf: (pdfUrl) => ipcRenderer.invoke("print-pdf", pdfUrl),
|
||||
startIdCardListen: () => ipcRenderer.invoke("start_idcard_listen"),
|
||||
stopIdCardListen: () => ipcRenderer.invoke("stop_idcard_listen"),
|
||||
onIdCardData: (callback) =>
|
||||
ipcRenderer.on("idcard-data", (event, value) => callback(value)),
|
||||
onIdCardError: (callback) =>
|
||||
ipcRenderer.on("idcard-error", (event, value) => callback(value)),
|
||||
log: (level, message) => ipcRenderer.send("log-message", { level, message }),
|
||||
removeIdCardListeners: () => {
|
||||
ipcRenderer.removeAllListeners("idcard-data");
|
||||
ipcRenderer.removeAllListeners("idcard-error");
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user