This commit is contained in:
xianyi
2025-11-20 17:15:17 +08:00
89 changed files with 475 additions and 121814 deletions

233
electron/idcard-worker.js Normal file
View 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;
}
});

View File

@@ -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();
}

View File

@@ -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");
},
});