feat: 优化设备标识识别与单程票检票逻辑

- 升级项目版本至v1.0.16
- 重构多文件的设备ID标准化逻辑,新增空格修剪、大小写不敏感处理,支持更多设备类型
- 基于事件类型智能区分闸机、充值机与售票机
- 新增站点线路校验工具,修复单程票检票的站点合法性校验
- 修正单程票票价计算使用实际进站站点而非初始起点
- 添加事件元数据格式化的空值安全检查
This commit is contained in:
2026-07-10 12:02:50 +08:00
parent 29d66c43b0
commit 8b10117487
8 changed files with 145 additions and 39 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ticketmachine",
"version": "1.0.15",
"version": "1.0.16",
"main": "server/index.js",
"scripts": {
"start": "node server/index.js",
+35 -3
View File
@@ -1338,8 +1338,35 @@ router.post('/tickets/check', async (req, res) => {
const isSingleTicket = String(cur.train_type || '').toLowerCase() === 'single';
const checkStationOnLine = (lineName, codeRaw, codesSet, codeNorm) => {
if (!lineName) return false;
const target = String(lineName).trim().toLowerCase();
const lines = DataService.getLines() || [];
const line = lines.find(l => {
const ln = String(l.name || l['线路名称'] || '').trim().toLowerCase();
const le = String(l.en_name || l.en || l.enName || '').trim().toLowerCase();
return ln === target || le === target;
});
if (!line) return false; // Line not found
const stops = Array.isArray(line.stations) ? line.stations : (Array.isArray(line.stops) ? line.stops : (line['站点列表'] || line['站点'] || []));
const stopSet = new Set(stops.map(normStationKey).filter(Boolean));
if (codeNorm && stopSet.has(codeNorm)) return true;
if (codesSet.size > 0) {
for (const sc of codesSet) {
if (stopSet.has(sc)) return true;
}
}
return false;
};
if (action === 'entry') {
if (!isSingleTicket && !matchesStation(start)) return deny('wrong_station');
if (isSingleTicket) {
if (!checkStationOnLine(cur.start, station_code_raw, station_codes_norm, station_code_norm)) {
return deny('wrong_station');
}
} else {
if (!matchesStation(start)) return deny('wrong_station');
}
if (entered && !exited) return deny('already_entered');
const entryCode = isSingleTicket ? (station_code_raw || start) : start;
@@ -1352,7 +1379,8 @@ router.post('/tickets/check', async (req, res) => {
exited: false,
last_action: 'entry',
last_station_code: entryCode,
start: isSingleTicket ? entryCode : cur.start,
start: isSingleTicket ? cur.start : cur.start,
entered_station: entryCode,
last_result: 'pass',
last_reason: '',
last_event: 'check',
@@ -1367,7 +1395,11 @@ router.post('/tickets/check', async (req, res) => {
const exitCode = station_code_raw || terminal;
if (isSingleTicket) {
const fareInfo = LogicService.computeFareBoth(cur.start, exitCode);
if (!checkStationOnLine(cur.start, station_code_raw, station_codes_norm, station_code_norm)) {
return deny('wrong_station');
}
const entryCode = cur.entered_station || cur.last_station_code;
const fareInfo = LogicService.computeFareBoth(entryCode, exitCode);
if (!fareInfo || fareInfo.paid_segments !== 1) {
return deny('not_single_segment');
}
+16 -5
View File
@@ -191,11 +191,22 @@
const formatIcCardEventUnified = (ev) => {
const type = String(ev?.type || '').toLowerCase();
const action = String(ev?.action || '').toLowerCase();
let deviceId = ev?.device || ev?.device_id || ev?.["设备编号"] || ev?.["机器ID"] || '---';
if (/^\d+$/.test(deviceId)) deviceId = 'ticket_machine';
if (deviceId === 'ticket_machine') deviceId = '售票机';
if (deviceId === 'gate') deviceId = '闸机';
if (deviceId === 'refill_machine') deviceId = '充值机';
let deviceId = String(ev?.device || ev?.device_id || ev?.["设备编号"] || ev?.["机器ID"] || '---').trim();
const dStr = deviceId.toLowerCase();
if (dStr === 'gate') {
deviceId = '闸机';
} else if (dStr === 'refill_machine') {
deviceId = '充值机';
} else if (dStr === 'ticket_machine' || dStr === 'ticket_vending_machine' || /^\d+$/.test(dStr) || dStr.includes('computer') || dStr === 'unknown') {
if (type === 'entry' || action === 'entry' || type === 'exit' || action === 'exit' || type === 'check') {
deviceId = '闸机';
} else if (type === 'topup' || type === 'refill' || action === 'topup' || type === '充值') {
deviceId = '充值机';
} else {
deviceId = '售票机';
}
}
const timeStr = formatTime(ev?.ts);
const balance = ev?.balance ?? ev?.["余额"] ?? 0;
+16 -5
View File
@@ -90,11 +90,22 @@
const formatIcCardEventUnified = (ev) => {
const type = String(ev?.type || '').toLowerCase();
const action = String(ev?.action || '').toLowerCase();
let deviceId = ev?.device || ev?.device_id || ev?.["设备编号"] || ev?.["机器ID"] || '---';
if (/^\d+$/.test(deviceId)) deviceId = 'ticket_machine';
if (deviceId === 'ticket_machine') deviceId = '售票机';
if (deviceId === 'gate') deviceId = '闸机';
if (deviceId === 'refill_machine') deviceId = '充值机';
let deviceId = String(ev?.device || ev?.device_id || ev?.["设备编号"] || ev?.["机器ID"] || '---').trim();
const dStr = deviceId.toLowerCase();
if (dStr === 'gate') {
deviceId = '闸机';
} else if (dStr === 'refill_machine') {
deviceId = '充值机';
} else if (dStr === 'ticket_machine' || dStr === 'ticket_vending_machine' || /^\d+$/.test(dStr) || dStr.includes('computer') || dStr === 'unknown') {
if (type === 'entry' || action === 'entry' || type === 'exit' || action === 'exit' || type === 'check') {
deviceId = '闸机';
} else if (type === 'topup' || type === 'refill' || action === 'topup' || type === '充值') {
deviceId = '充值机';
} else {
deviceId = '售票机';
}
}
const timeStr = formatTime(ev?.ts);
const balance = ev?.balance ?? ev?.["余额"] ?? 0;
+30 -10
View File
@@ -348,11 +348,20 @@ createApp({
const formatTicketEventUnified = (ev) => {
const type = getTicketEventType(ev);
const action = getTicketEventAction(ev);
let deviceId = ev?.device || ev?.device_id || ev?.["设备编号"] || ev?.["机器ID"] || '---';
if (/^\d+$/.test(deviceId)) deviceId = 'ticket_machine';
if (deviceId === 'ticket_machine') deviceId = '售票机';
if (deviceId === 'gate') deviceId = '闸机';
if (deviceId === 'refill_machine') deviceId = '充值机';
let deviceId = String(ev?.device || ev?.device_id || ev?.["设备编号"] || ev?.["机器ID"] || '---').trim();
const dStr = deviceId.toLowerCase();
if (dStr === 'gate') {
deviceId = '闸机';
} else if (dStr === 'refill_machine') {
deviceId = '充值机';
} else if (dStr === 'ticket_machine' || dStr === 'ticket_vending_machine' || /^\d+$/.test(dStr) || dStr.includes('computer') || dStr === 'unknown') {
if (type === 'entry' || action === 'entry' || type === 'exit' || action === 'exit' || type === 'check') {
deviceId = '闸机';
} else {
deviceId = '售票机';
}
}
const timeStr = formatTime(ev?.ts || ev?.["时间戳"]);
if (type === 'sale' || type === '售票') {
@@ -455,11 +464,22 @@ createApp({
const formatIcEventDetail = (ev) => {
const type = String(ev?.type || '').toLowerCase();
const action = String(ev?.action || '').toLowerCase();
let deviceId = ev?.device || ev?.device_id || ev?.["设备编号"] || ev?.["机器ID"] || '---';
if (/^\d+$/.test(deviceId)) deviceId = 'ticket_machine';
if (deviceId === 'ticket_machine') deviceId = '售票机';
if (deviceId === 'gate') deviceId = '闸机';
if (deviceId === 'refill_machine') deviceId = '充值机';
let deviceId = String(ev?.device || ev?.device_id || ev?.["设备编号"] || ev?.["机器ID"] || '---').trim();
const dStr = deviceId.toLowerCase();
if (dStr === 'gate') {
deviceId = '闸机';
} else if (dStr === 'refill_machine') {
deviceId = '充值机';
} else if (dStr === 'ticket_machine' || dStr === 'ticket_vending_machine' || /^\d+$/.test(dStr) || dStr.includes('computer') || dStr === 'unknown') {
if (type === 'entry' || action === 'entry' || type === 'exit' || action === 'exit' || type === 'check') {
deviceId = '闸机';
} else if (type === 'topup' || type === 'refill' || action === 'topup' || type === '充值') {
deviceId = '充值机';
} else {
deviceId = '售票机';
}
}
const timeStr = formatTime(ev?.ts);
const balance = ev?.balance ?? ev?.["余额"] ?? 0;
+19 -5
View File
@@ -310,18 +310,32 @@
};
const formatEventMeta = (event) => {
if (!event) return '---';
const type = String(event.type || event['类型'] || '').toLowerCase();
const action = String(event.action || event['动作'] || '').toLowerCase();
if (type === 'sale' || type === '售票') {
const amount = event.amount ?? event['售票额'];
if (amount != null && amount !== '') return `票价:¥ ${amount}`;
}
const stationEn = event.station_en || event['站点英文'] || '';
let deviceId = event.device || event.device_id || event['设备编号'] || event['机器ID'] || '';
if (/^\d+$/.test(deviceId)) deviceId = 'ticket_machine';
if (deviceId === 'ticket_machine') deviceId = '售票机';
if (deviceId === 'gate') deviceId = '闸机';
if (deviceId === 'refill_machine') deviceId = '充值机';
let deviceId = String(event.device || event.device_id || event['设备编号'] || event['机器ID'] || '').trim();
const dStr = deviceId.toLowerCase();
if (dStr === 'gate') {
deviceId = '闸机';
} else if (dStr === 'refill_machine') {
deviceId = '充值机';
} else if (dStr === 'ticket_machine' || dStr === 'ticket_vending_machine' || /^\d+$/.test(dStr) || dStr.includes('computer') || dStr === 'unknown') {
if (type === 'entry' || action === 'entry' || type === 'exit' || action === 'exit' || type === 'check') {
deviceId = '闸机';
} else if (type === 'topup' || type === 'refill' || action === 'topup' || type === '充值') {
deviceId = '充值机';
} else {
deviceId = '售票机';
}
}
if (stationEn && deviceId) return `${stationEn} (${deviceId})`;
if (deviceId) return `设备:${deviceId}`;
+14 -5
View File
@@ -228,11 +228,20 @@ createApp({
const formatTicketEventUnified = (ev) => {
const type = getTicketEventType(ev);
const action = getTicketEventAction(ev);
let deviceId = ev?.device || ev?.device_id || ev?.["设备编号"] || ev?.["机器ID"] || '---';
if (/^\d+$/.test(deviceId)) deviceId = 'ticket_machine';
if (deviceId === 'ticket_machine') deviceId = '售票机';
if (deviceId === 'gate') deviceId = '闸机';
if (deviceId === 'refill_machine') deviceId = '充值机';
let deviceId = String(ev?.device || ev?.device_id || ev?.["设备编号"] || ev?.["机器ID"] || '---').trim();
const dStr = deviceId.toLowerCase();
if (dStr === 'gate') {
deviceId = '闸机';
} else if (dStr === 'refill_machine') {
deviceId = '充值机';
} else if (dStr === 'ticket_machine' || dStr === 'ticket_vending_machine' || /^\d+$/.test(dStr) || dStr.includes('computer') || dStr === 'unknown') {
if (type === 'entry' || action === 'entry' || type === 'exit' || action === 'exit' || type === 'check') {
deviceId = '闸机';
} else {
deviceId = '售票机';
}
}
const timeStr = formatTime(ev?.ts || ev?.["时间戳"]);
if (type === 'sale' || type === '售票') {
+14 -5
View File
@@ -110,11 +110,20 @@
const formatTicketEventUnified = (ev) => {
const type = getEventType(ev);
const action = String(ev.action || ev["动作"] || '').toLowerCase();
let deviceId = ev.device || ev.device_id || ev["设备编号"] || ev["机器ID"] || '---';
if (/^\d+$/.test(deviceId)) deviceId = 'ticket_machine';
if (deviceId === 'ticket_machine') deviceId = '售票机';
if (deviceId === 'gate') deviceId = '闸机';
if (deviceId === 'refill_machine') deviceId = '充值机';
let deviceId = String(ev.device || ev.device_id || ev["设备编号"] || ev["机器ID"] || '---').trim();
const dStr = deviceId.toLowerCase();
if (dStr === 'gate') {
deviceId = '闸机';
} else if (dStr === 'refill_machine') {
deviceId = '充值机';
} else if (dStr === 'ticket_machine' || dStr === 'ticket_vending_machine' || /^\d+$/.test(dStr) || dStr.includes('computer') || dStr === 'unknown') {
if (type === 'entry' || action === 'entry' || type === 'exit' || action === 'exit' || type === 'check') {
deviceId = '闸机';
} else {
deviceId = '售票机';
}
}
const timeStr = formatTime(ev.ts || ev["时间戳"]);
if (type === 'sale' || type === '售票') {